Skip to content

Commit 4392bab

Browse files
authored
Handle exceptions when loading resources and fetching assets (#5627)
Handle exceptions when loading resources and fetching assets Fixes https://youtrack.jetbrains.com/issue/CMP-10325 ## Testing Add related test cases ## Release Notes ### Fixes - Resources - Web: fixed a app crash when a resource fetch is failed
1 parent 07d1e1d commit 4392bab

8 files changed

Lines changed: 289 additions & 84 deletions

File tree

components/resources/library/build.gradle.kts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import kotlinx.validation.ExperimentalBCVApi
2-
import org.jetbrains.compose.ExperimentalComposeLibrary
32
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
43
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
54

@@ -28,9 +27,9 @@ kotlin {
2827
iosSimulatorArm64()
2928
js {
3029
browser {
31-
testTask(Action {
32-
enabled = false
33-
})
30+
testTask {
31+
useKarma { useChromeHeadless() }
32+
}
3433
}
3534
}
3635

@@ -41,12 +40,9 @@ kotlin {
4140
compilerOptions.freeCompilerArgs.add("-Xwasm-enable-array-range-checks")
4241
}
4342
browser {
44-
testTask(Action {
45-
useKarma {
46-
useChromeHeadless()
47-
useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
48-
}
49-
})
43+
testTask {
44+
useKarma { useChromeHeadless() }
45+
}
5046
}
5147
binaries.executable()
5248
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
2+
// This file provides karma.config.d configuration to run tests with k/wasm and k/js.
3+
//
4+
// The whole body is wrapped in an IIFE so that local declarations (e.g. `path`) do not
5+
// leak into the shared karma.conf.js scope. For k/js the Compose plugin concatenates this
6+
// file with `compose-skiko-runtime.js`, which also declares `const path`, and top-level
7+
// re-declarations would throw "Identifier 'path' has already been declared".
8+
(function (config) {
9+
const path = require("path");
10+
11+
config.browserConsoleLogOptions.level = "debug";
12+
13+
const basePath = config.basePath;
14+
const projectPath = path.resolve(basePath, "..", "..", "..", "..");
15+
const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
16+
17+
const debug = message => console.log(`[karma-config] ${message}`);
18+
19+
debug(`karma basePath: ${basePath}`);
20+
debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
21+
22+
config.proxies["/"] = path.resolve(basePath, "kotlin");
23+
24+
config.files = [
25+
{pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
26+
{pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
27+
{pattern: path.resolve(basePath, "kotlin", "**/*.cvr"), included: false, served: true, watched: false},
28+
{pattern: path.resolve(basePath, "kotlin", "**/*.otf"), included: false, served: true, watched: false},
29+
{pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
30+
{pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
31+
{pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
32+
{pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
33+
{pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
34+
path.resolve(basePath, "kotlin", "test_setup.js"),
35+
].concat(config.files);
36+
37+
function KarmaWebpackOutputFramework(config) {
38+
// This controller is instantiated and set during the preprocessor phase.
39+
const controller = config.__karmaWebpackController;
40+
41+
// only if webpack has instantiated its controller
42+
if (!controller) {
43+
console.warn(
44+
"Webpack has not instantiated controller yet.\n" +
45+
"Check if you have enabled webpack preprocessor and framework before this framework"
46+
)
47+
return
48+
}
49+
50+
config.files.push({
51+
pattern: `${controller.outputPath}/**/*`,
52+
included: false,
53+
served: true,
54+
watched: false
55+
})
56+
}
57+
58+
const KarmaWebpackOutputPlugin = {
59+
'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
60+
};
61+
62+
config.plugins.push(KarmaWebpackOutputPlugin);
63+
config.frameworks.push("webpack-output");
64+
})(config);

components/resources/library/karma.config.d/wasm/config.js

Lines changed: 0 additions & 58 deletions
This file was deleted.

components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ internal object DefaultJsResourceReader : ResourceReader {
5353
private suspend fun readAsBlob(path: String): Blob {
5454
val resPath = WebResourcesConfiguration.getResourcePath(path)
5555
val response = ResourceWebCache.load(resPath) {
56-
cancellableFetch(resPath)
56+
try {
57+
cancellableFetch(resPath)
58+
} catch (_: Throwable) {
59+
throw MissingResourceException(resPath)
60+
}
5761
}
5862
if (!response.ok) {
5963
throw MissingResourceException(resPath)

components/resources/library/src/skikoTest/kotlin/org/jetbrains/compose/resources/VariationFontCacheTest.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ class VariationFontCacheTest {
1919
val setting = FontVariation.Setting("wght", 700f)
2020
val settings = FontVariation.Settings(setting)
2121
val cacheKey = settings.getCacheKey()
22-
assertEquals("SettingFloat(wght,700.0)", cacheKey, "Cache key for a single setting is incorrect")
22+
assertEquals(
23+
"SettingFloat(wght,700)",
24+
cacheKey.replace("700.0", "700"),
25+
"Cache key for a single setting is incorrect"
26+
)
2327
}
2428

2529
@Test
@@ -29,8 +33,8 @@ class VariationFontCacheTest {
2933
val settings = FontVariation.Settings(setting1, setting2)
3034
val cacheKey = settings.getCacheKey()
3135
assertEquals(
32-
"SettingFloat(ital,1.0),SettingFloat(wght,400.0)",
33-
cacheKey,
36+
"SettingFloat(ital,1),SettingFloat(wght,400)",
37+
cacheKey.replace("400.0", "400").replace("1.0", "1"),
3438
"Cache key should sort settings by class name and axis name"
3539
)
3640
}

components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
package org.jetbrains.compose.resources
44

55
import kotlinx.browser.window
6-
import kotlinx.coroutines.CancellationException
76
import kotlinx.coroutines.await
87
import kotlinx.coroutines.suspendCancellableCoroutine
98
import org.khronos.webgl.ArrayBuffer
@@ -12,7 +11,6 @@ import org.w3c.files.Blob
1211
import kotlin.coroutines.resume
1312
import kotlin.coroutines.resumeWithException
1413
import kotlin.js.Promise
15-
import kotlin.js.unsafeCast
1614

1715
@JsFun("(blob) => blob.arrayBuffer()")
1816
private external fun jsExportBlobAsArrayBuffer(blob: Blob): Promise<ArrayBuffer>
@@ -59,7 +57,11 @@ internal object DefaultWasmResourceReader : ResourceReader {
5957
private suspend fun readAsBlob(path: String): Blob {
6058
val resPath = WebResourcesConfiguration.getResourcePath(path)
6159
val response = ResourceWebCache.load(resPath) {
62-
cancellableFetch(resPath)
60+
try {
61+
cancellableFetch(resPath)
62+
} catch (_: Throwable) {
63+
throw MissingResourceException(resPath)
64+
}
6365
}
6466
if (!response.ok) {
6567
throw MissingResourceException(resPath)

components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
package org.jetbrains.compose.resources
22

3-
import androidx.compose.runtime.Composable
4-
import androidx.compose.runtime.State
5-
import androidx.compose.runtime.mutableStateOf
6-
import androidx.compose.runtime.remember
7-
import androidx.compose.runtime.rememberCoroutineScope
3+
import androidx.compose.runtime.*
84
import kotlinx.coroutines.CoroutineStart
95
import kotlinx.coroutines.launch
106

@@ -19,7 +15,11 @@ internal actual fun <T> rememberResourceState(
1915
return remember(key1, environment) {
2016
val mutableState = mutableStateOf(getDefault())
2117
scope.launch(start = CoroutineStart.UNDISPATCHED) {
22-
mutableState.value = block(environment)
18+
try {
19+
mutableState.value = block(environment)
20+
} catch (_: Exception) {
21+
//resource loading failed
22+
}
2323
}
2424
mutableState
2525
}
@@ -37,7 +37,11 @@ internal actual fun <T> rememberResourceState(
3737
return remember(key1, key2, environment) {
3838
val mutableState = mutableStateOf(getDefault())
3939
scope.launch(start = CoroutineStart.UNDISPATCHED) {
40-
mutableState.value = block(environment)
40+
try {
41+
mutableState.value = block(environment)
42+
} catch (_: Exception) {
43+
//resource loading failed
44+
}
4145
}
4246
mutableState
4347
}
@@ -56,7 +60,11 @@ internal actual fun <T> rememberResourceState(
5660
return remember(key1, key2, key3, environment) {
5761
val mutableState = mutableStateOf(getDefault())
5862
scope.launch(start = CoroutineStart.UNDISPATCHED) {
59-
mutableState.value = block(environment)
63+
try {
64+
mutableState.value = block(environment)
65+
} catch (_: Exception) {
66+
//resource loading failed
67+
}
6068
}
6169
mutableState
6270
}
@@ -76,7 +84,11 @@ internal actual fun <T> rememberResourceState(
7684
return remember(key1, key2, key3, key4, environment) {
7785
val mutableState = mutableStateOf(getDefault())
7886
scope.launch(start = CoroutineStart.UNDISPATCHED) {
79-
mutableState.value = block(environment)
87+
try {
88+
mutableState.value = block(environment)
89+
} catch (_: Exception) {
90+
//resource loading failed
91+
}
8092
}
8193
mutableState
8294
}

0 commit comments

Comments
 (0)