From fd85f799b32e2c3c707f432da84c295ebc80b04b Mon Sep 17 00:00:00 2001 From: Ilya Goncharov Date: Tue, 11 Nov 2025 13:25:56 +0100 Subject: [PATCH] Adopt multi-module compose wasm compilation --- examples.md | 45 ++++--- package.json | 2 +- src/config.js | 22 +--- src/executable-code/executable-fragment.js | 16 +-- src/js-executor/execute-es-module.js | 24 ++-- src/js-executor/index.js | 141 +-------------------- 6 files changed, 54 insertions(+), 196 deletions(-) diff --git a/examples.md b/examples.md index bc1abf4d..16beaf21 100644 --- a/examples.md +++ b/examples.md @@ -144,45 +144,48 @@ You can use Compose Wasm.
```kotlin -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.window.CanvasBasedWindow import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.window.ComposeViewport +import kotlinx.browser.document //sampleStart @OptIn(ExperimentalComposeUiApi::class) fun main() { - CanvasBasedWindow { App() } + ComposeViewport(viewportContainer = document.body!!, content = { + App() + }) } @Composable fun App() { MaterialTheme { - var greetingText by remember { mutableStateOf("Hello World!") } - var showImage by remember { mutableStateOf(false) } - var counter by remember { mutableStateOf(0) } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = { - counter++ - greetingText = "Compose: ${Greeting().greet()}" - showImage = !showImage - }) { - Text(greetingText) + var showContent by remember { mutableStateOf(false) } + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Button(onClick = { showContent = !showContent }) { + Text("Click me!") } - AnimatedVisibility(showImage) { - Text(counter.toString()) + AnimatedVisibility(showContent) { + val greeting = remember { Greeting().greet() } + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Compose: $greeting") + } } } } diff --git a/package.json b/package.json index 1d9a8a70..efbb76e0 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "copy-examples": "node utils/copy-examples", "release:ci": "rm -rf dist && npm run build:all && $NPM_TOKEN=%env.NPM_TOKEN% npm publish", "start": "webpack-dev-server --port 9002", - "start-with-local-compiler": "webpack-dev-server --port 9002 --env webDemoUrl='//localhost:8080' webDemoResourcesUrl='//localhost:8081'", + "start-with-local-compiler": "webpack-dev-server --port 9002 --env webDemoUrl='http://localhost:8080' webDemoResourcesUrl='http://localhost:8081'", "lint": "eslint . --ext .ts", "fix": "eslint --fix --ext .ts .", "test": "npm run build:all && npm run test:run", diff --git a/src/config.js b/src/config.js index 6faf9234..44873696 100644 --- a/src/config.js +++ b/src/config.js @@ -11,11 +11,14 @@ export const RUNTIME_CONFIG = { ...getConfigFromElement(currentScript) }; * @type {{COMPILE: string, COMPLETE: string, VERSIONS: string, JQUERY: string, KOTLIN_JS: string}} */ export const API_URLS = { - server: (RUNTIME_CONFIG.server || __WEBDEMO_URL__).replace(/\/$/, ''), - composeServer: 'https://compose-stage.sandbox.intellij.net'.replace( + server: (__WEBDEMO_URL__ || RUNTIME_CONFIG.server).replace(/\/$/, ''), + composeServer: (__WEBDEMO_URL__ || 'https://compose-stage.sandbox.intellij.net').replace( /\/$/, '', ), + composeResources: (__WEBDEMO_RESOURCES_URL__ || 'https://compose-stage.sandbox.intellij.net').replace( + /\/$/, '' + ), COMPILE(platform, version) { let url; @@ -60,21 +63,6 @@ export const API_URLS = { get VERSIONS() { return `${this.server}/versions`; }, - RESOURCE_VERSIONS() { - return `${this.composeServer}/api/resource/compose-wasm-versions`; - }, - SKIKO_MJS(version) { - return `${this.composeServer}/api/resource/skiko-${version}.mjs`; - }, - SKIKO_WASM(version) { - return `${this.composeServer}/api/resource/skiko-${version}.wasm`; - }, - STDLIB_MJS(hash) { - return `${this.composeServer}/api/resource/stdlib-${hash}.mjs`; - }, - STDLIB_WASM(hash) { - return `${this.composeServer}/api/resource/stdlib-${hash}.wasm`; - }, get JQUERY() { return `https://cdn.jsdelivr.net/npm/jquery@1/dist/jquery.min.js`; }, diff --git a/src/executable-code/executable-fragment.js b/src/executable-code/executable-fragment.js index c1cd477f..5ae3c8e5 100644 --- a/src/executable-code/executable-fragment.js +++ b/src/executable-code/executable-fragment.js @@ -412,24 +412,15 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { targetPlatform, compilerVersion, ); - const additionalRequests = []; - if (targetPlatform === TargetPlatforms.COMPOSE_WASM) { - if (this.jsExecutor.stdlibExports) { - additionalRequests.push(this.jsExecutor.stdlibExports); - } - } - Promise.all([ - WebDemoApi.translateKotlinToJs( + WebDemoApi.translateKotlinToJs( this.getCode(), compilerVersion, targetPlatform, args, hiddenDependencies, - ), - ...additionalRequests, - ]).then( - ([state, ...additionalRequestsResults]) => { + ).then( + (state) => { state.waitingForOutput = false; const jsCode = state.jsCode; const wasm = state.wasm; @@ -454,7 +445,6 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { outputHeight, theme, onError, - additionalRequestsResults, compilerVersion, ) .then((output) => { diff --git a/src/js-executor/execute-es-module.js b/src/js-executor/execute-es-module.js index 3a49daf0..14d13b96 100644 --- a/src/js-executor/execute-es-module.js +++ b/src/js-executor/execute-es-module.js @@ -1,26 +1,22 @@ +import {API_URLS} from "../config"; + export async function executeWasmCode(container, jsCode, wasmCode) { const newCode = prepareJsCode(jsCode); return execute(container, newCode, wasmCode); } -export async function executeWasmCodeWithSkiko(container, jsCode) { - return executeJs(container, prepareJsCode(jsCode)); -} - -export async function executeWasmCodeWithStdlib(container, jsCode, wasmCode) { - return execute(container, prepareJsCode(jsCode), wasmCode); -} - function execute(container, jsCode, wasmCode) { container.wasmCode = Uint8Array.from(atob(wasmCode), c => c.charCodeAt(0)); return executeJs(container, jsCode); } -export function executeJs(container, jsCode) { +function executeJs(container, jsCode) { return container.eval(`import(/* webpackIgnore: true */ '${'data:text/javascript;base64,' + btoa(jsCode)}');`) } function prepareJsCode(jsCode) { + const re = /instantiateStreaming\(fetch\(new URL\('([^']*)',\s*import\.meta\.url\)\.href\),\s*importObject\s*,\s*\{\s*builtins\s*:\s*\[''\]\s*\}\s*\)\)\.instance;/g; + return ` class BufferedOutput { constructor() { @@ -30,12 +26,20 @@ function prepareJsCode(jsCode) { export const bufferedOutput = new BufferedOutput() ` + jsCode + .replaceAll( + "await import('./", + "await import('" + API_URLS.composeResources + "/" + ) + .replaceAll( + "%3", + "%253" + ) .replace( "instantiateStreaming(fetch(wasmFilePath), importObject)).instance;", "instantiate(window.wasmCode, importObject)).instance;\nwindow.wasmCode = undefined;" ) .replace( - "instantiateStreaming(fetch(new URL('./playground.wasm',import.meta.url).href), importObject)).instance;", + re, "instantiate(window.wasmCode, importObject)).instance;\nwindow.wasmCode = undefined;" ) .replace( diff --git a/src/js-executor/index.js b/src/js-executor/index.js index a080a48b..dd7eec3f 100644 --- a/src/js-executor/index.js +++ b/src/js-executor/index.js @@ -3,13 +3,7 @@ import { API_URLS } from '../config'; import { showJsException } from '../view/output-view'; import { processingHtmlBrackets } from '../utils'; import { isJsLegacy, isWasmRelated, TargetPlatforms } from '../utils/platforms'; -import { - executeJs, - executeWasmCode, - executeWasmCodeWithSkiko, - executeWasmCodeWithStdlib, -} from './execute-es-module'; -import { fetch } from 'whatwg-fetch'; +import { executeWasmCode } from './execute-es-module'; const INIT_SCRIPT = 'if(kotlin.BufferedOutput!==undefined){kotlin.out = new kotlin.BufferedOutput()}' + @@ -31,7 +25,6 @@ const normalizeJsVersion = (version) => { export default class JsExecutor { constructor(kotlinVersion) { this.kotlinVersion = kotlinVersion; - this.stdlibExports = undefined; } async executeJsCode( @@ -42,7 +35,6 @@ export default class JsExecutor { outputHeight, theme, onError, - additionalRequestsResults, compilerVersion, ) { if (platform === TargetPlatforms.SWIFT_EXPORT) { @@ -75,15 +67,12 @@ export default class JsExecutor { // for some reason resize function in Compose does not work in Firefox in invisible block this.iframe.style.display = 'block'; - const additionalRequestsResult = additionalRequestsResults[0]; const result = await this.executeWasm( jsCode, wasm, - executeWasmCodeWithStdlib, + executeWasmCode, theme, - processError, - additionalRequestsResult.stdlib, - additionalRequestsResult.output, + processError ); if (exception) { @@ -146,8 +135,6 @@ export default class JsExecutor { executor, theme, onError, - imports, - output, ) { try { const exports = await executor( @@ -155,8 +142,8 @@ export default class JsExecutor { jsCode, wasmCode, ); - await exports.instantiate({ 'playground.master': imports }); - const bufferedOutput = output ?? exports.bufferedOutput; + await exports.instantiate(); + const bufferedOutput = this.iframe.contentWindow.bufferedOutput ?? exports.bufferedOutput; const outputString = bufferedOutput.buffer; bufferedOutput.buffer = ''; return outputString @@ -202,124 +189,10 @@ export default class JsExecutor { ); } if (targetPlatform === TargetPlatforms.COMPOSE_WASM) { - const skikoStdlib = fetch(API_URLS.RESOURCE_VERSIONS(), { - method: 'GET', - }) - .then((response) => response.json()) - .then((versions) => { - const skikoVersion = versions['skiko']; - - const skikoExports = fetch(API_URLS.SKIKO_MJS(skikoVersion), { - method: 'GET', - headers: { - 'Content-Type': 'text/javascript', - }, - }) - .then((script) => script.text()) - .then((script) => - script.replace( - 'new URL("skiko.wasm",import.meta.url).href', - `'${API_URLS.SKIKO_WASM(skikoVersion)}'`, - ), - ) - .then((skikoCode) => - executeJs(this.iframe.contentWindow, skikoCode), - ) - .then((skikoExports) => fixedSkikoExports(skikoExports)); - - const stdlibVersion = versions['stdlib']; - - const stdlibExports = fetch(API_URLS.STDLIB_MJS(stdlibVersion), { - method: 'GET', - headers: { - 'Content-Type': 'text/javascript', - }, - }) - .then((script) => script.text()) - .then((script) => - // necessary to load stdlib.wasm before its initialization to parallelize - // language=JavaScript - ( - `const stdlibWasm = fetch('${API_URLS.STDLIB_WASM(stdlibVersion)}'); ` + - script - ) - .replace( - "fetch(new URL('./stdlib_master.wasm',import.meta.url).href)", - 'stdlibWasm', - ) - .replace( - '(extends) => { return { extends }; }', - '(extends_) => { return { extends_ }; }', - ), - ) - .then((stdlibCode) => - executeWasmCodeWithSkiko(this.iframe.contentWindow, stdlibCode), - ); - - return Promise.all([skikoExports, stdlibExports]); - }); - - this.stdlibExports = skikoStdlib - .then(async ([skikoExportsResult, stdlibExportsResult]) => { - return [ - await stdlibExportsResult.instantiate({ - './skiko.mjs': skikoExportsResult, - }), - stdlibExportsResult, - ]; - }) - .then(([stdlibResult, outputResult]) => { - return { - stdlib: stdlibResult.exports, - output: outputResult.bufferedOutput, - }; - }); - - this.iframe.height = '1000'; - iframeDoc.write(``); + this.iframe.height = "1000" + iframeDoc.write(``); } iframeDoc.write(''); iframeDoc.close(); } } - -function fixedSkikoExports(skikoExports) { - return { - ...skikoExports, - org_jetbrains_skia_Bitmap__1nGetPixmap: function () { - console.log( - 'org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer', - ); - }, - org_jetbrains_skia_Bitmap__1nIsVolatile: function () { - console.log( - 'org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer', - ); - }, - org_jetbrains_skia_Bitmap__1nSetVolatile: function () { - console.log( - 'org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer', - ); - }, - org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer: function () { - console.log( - 'org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer', - ); - }, - org_jetbrains_skia_TextBlobBuilderRunHandler__1nMake: function () { - console.log( - 'org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer', - ); - }, - org_jetbrains_skia_TextBlobBuilderRunHandler__1nMakeBlob: function () { - console.log( - 'org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer', - ); - }, - org_jetbrains_skia_svg_SVGCanvasKt__1nMake: function () { - console.log( - 'org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer', - ); - }, - }; -}