From 73802292cecf9db4bb972421ea8a19a1301c6820 Mon Sep 17 00:00:00 2001 From: Oleksandr Karpovich Date: Tue, 24 Oct 2023 15:54:15 +0200 Subject: [PATCH] CfW: Allow web resource routing configuration (#3852) This commit changes the default resource routing behaviour: - It used to search for a file in the root directory (on a domain level) - After this change, it will search for a file relatively to the current url segment Besides that, we add a small configuration to let developers change the default behaviour when needed. ___ usage examples: ```kotlin // 1 configureWebResources { setResourceFactory { path -> urlResource("/myApp1/resources/$path") } } // 2 configureWebResources { setResourcelFactory { path -> urlResource("https://mycdn.com/myApp1/res/$path") } } ``` ___ This will fix https://github.com/JetBrains/compose-multiplatform/issues/3413 (currently it bothers our users) --- components/gradle.properties | 2 +- .../resources/demo/shared/build.gradle.kts | 5 + .../demo/shared/src/jsMain/kotlin/main.js.kt | 10 ++ .../compose/resources/Resource.js.kt | 101 ++++++++++++++---- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/components/gradle.properties b/components/gradle.properties index a681f351fc..c52d6fe71c 100644 --- a/components/gradle.properties +++ b/components/gradle.properties @@ -5,7 +5,7 @@ kotlin.code.style=official # __KOTLIN_COMPOSE_VERSION__ kotlin.version=1.8.22 # __LATEST_COMPOSE_RELEASE_VERSION__ -compose.version=1.5.0-dev1112 +compose.version=1.5.10-rc01 agp.version=7.3.1 org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.macos.enabled=true diff --git a/components/resources/demo/shared/build.gradle.kts b/components/resources/demo/shared/build.gradle.kts index 306e748cba..ea6b6072cc 100644 --- a/components/resources/demo/shared/build.gradle.kts +++ b/components/resources/demo/shared/build.gradle.kts @@ -109,3 +109,8 @@ android { compose.experimental { web.application {} } + +// TODO: remove this block after we update on a newer kotlin. Currently there is an error: `error:0308010C:digital envelope routines::unsupported` +rootProject.plugins.withType { + rootProject.the().nodeVersion = "16.0.0" +} \ No newline at end of file diff --git a/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt b/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt index 2eb351f69a..3377e36df4 100644 --- a/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt +++ b/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt @@ -11,10 +11,20 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.configureWebResources import org.jetbrains.compose.resources.demo.shared.UseResources +import org.jetbrains.compose.resources.urlResource import org.jetbrains.skiko.wasm.onWasmReady + fun main() { + + @OptIn(ExperimentalResourceApi::class) + configureWebResources { + // Not necessary - It's the same as the default. We add it here just to present this feature. + setResourceFactory { urlResource("./$it") } + } onWasmReady { Window("Resources demo") { MainView() diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt index efbf35a011..e982712ccb 100644 --- a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt @@ -5,40 +5,97 @@ package org.jetbrains.compose.resources +import kotlinx.browser.window +import kotlinx.coroutines.await import org.jetbrains.compose.resources.vector.xmldom.Element import org.jetbrains.compose.resources.vector.xmldom.ElementImpl import org.jetbrains.compose.resources.vector.xmldom.MalformedXMLException import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.Int8Array import org.w3c.dom.parsing.DOMParser -import org.w3c.xhr.ARRAYBUFFER -import org.w3c.xhr.XMLHttpRequest -import org.w3c.xhr.XMLHttpRequestResponseType -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine +/** + * Represents the configuration object for web resources. + * + * @see configureWebResources - for overriding the default configuration. + */ +@Suppress("unused") +@ExperimentalResourceApi +object WebResourcesConfiguration { + + /** + * An internal default factory method for creating [Resource] from a given path. + * It can be changed at runtime by using [setResourceFactory]. + */ + @ExperimentalResourceApi + internal var jsResourceImplFactory: (path: String) -> Resource = { urlResource("./$it") } + + /** + * Sets a custom factory for the [resource] function to create [Resource] instances. + * Once set, the [factory] will effectively define the implementation of the [resource] function. + * + * @param factory A lambda that accepts a path and produces a [Resource] instance. + * @see configureWebResources for examples on how to use this function. + */ + @ExperimentalResourceApi + fun setResourceFactory(factory: (path: String) -> Resource) { + jsResourceImplFactory = factory + } +} + +/** + * Configures the web resources behavior. + * + * Allows users to override default behavior and provide custom logic for generating [Resource] instances. + * + * @param configure Configuration lambda applied to [WebResourcesConfiguration]. + * @see WebResourcesConfiguration For detailed configuration options. + * + * Examples: + * ``` + * configureWebResources { + * setResourceFactory { path -> urlResource("/myApp1/resources/$path") } + * } + * configureWebResources { + * setResourceFactory { path -> urlResource("https://mycdn.com/myApp1/res/$path") } + * } + * ``` + */ +@Suppress("unused") +@ExperimentalResourceApi +fun configureWebResources(configure: WebResourcesConfiguration.() -> Unit) { + WebResourcesConfiguration.configure() +} + +/** + * Generates a [Resource] instance based on the provided [path]. + * + * By default, the path is treated as relative to the current URL segment. + * The default behaviour can be overridden by using [configureWebResources]. + * + * @param path The path or resource id used to generate the [Resource] instance. + * @return A [Resource] instance corresponding to the provided path. + */ +@ExperimentalResourceApi +actual fun resource(path: String): Resource = WebResourcesConfiguration.jsResourceImplFactory(path) + +/** + * Creates a [Resource] instance based on the provided [url]. + * + * @param url The URL used to access the [Resource]. + * @return A [Resource] instance accessible by the given URL. + */ @ExperimentalResourceApi -actual fun resource(path: String): Resource = JSResourceImpl(path) +fun urlResource(url: String): Resource = JSUrlResourceImpl(url) @ExperimentalResourceApi -private class JSResourceImpl(path: String) : AbstractResourceImpl(path) { +private class JSUrlResourceImpl(url: String) : AbstractResourceImpl(url) { override suspend fun readBytes(): ByteArray { - return suspendCoroutine { continuation -> - val req = XMLHttpRequest() - req.open("GET", "/$path", true) - req.responseType = XMLHttpRequestResponseType.ARRAYBUFFER - - req.onload = { event -> - val arrayBuffer = req.response - if (arrayBuffer is ArrayBuffer) { - continuation.resume(arrayBuffer.toByteArray()) - } else { - continuation.resumeWithException(MissingResourceException(path)) - } - } - req.send(null) + val response = window.fetch(path).await() + if (!response.ok) { + throw MissingResourceException(path) } + return response.arrayBuffer().await().toByteArray() } }