Skip to content

Commit

Permalink
CfW: Allow web resource routing configuration (#3852)
Browse files Browse the repository at this point in the history
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 #3413 (currently it bothers our users)

(cherry picked from commit 7380229)
  • Loading branch information
eymar committed Oct 24, 2023
1 parent 359d75e commit 8fe28d9
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 23 deletions.
2 changes: 1 addition & 1 deletion components/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions components/resources/demo/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().nodeVersion = "16.0.0"
}
10 changes: 10 additions & 0 deletions components/resources/demo/shared/src/jsMain/kotlin/main.js.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand Down

0 comments on commit 8fe28d9

Please sign in to comment.