diff --git a/examples.md b/examples.md index 04f7757..3d7c7fe 100644 --- a/examples.md +++ b/examples.md @@ -136,6 +136,83 @@ fun main(args: Array) { +You can use Compose Wasm. + +```html +
+``` +
+ +```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.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.ui.Alignment +import androidx.compose.ui.Modifier + +//sampleStart +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + CanvasBasedWindow { 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) + } + AnimatedVisibility(showImage) { + Text(counter.toString()) + } + } + } +} + +private val platform = object : Platform { + + override val name: String + get() = "Web with Kotlin/Wasm" +} + +fun getPlatform(): Platform = platform + +class Greeting { + private val platform = getPlatform() + + fun greet(): String { + return "Hello, ${platform.name}!" + } +} + +interface Platform { + val name: String +} +//sampleEnd + +``` + +
+ Use `data-target-platform` attribute with value `junit` for creating examples with tests: diff --git a/src/config.js b/src/config.js index 4a05751..0bc99f9 100644 --- a/src/config.js +++ b/src/config.js @@ -31,6 +31,9 @@ export const API_URLS = { case TargetPlatforms.WASM: url = `${this.server}/api/${version}/compiler/translate?ir=true&compiler=wasm`; break; + case TargetPlatforms.COMPOSE_WASM: + url = `${this.server}/api/${version}/${TargetPlatforms.COMPOSE_WASM.id}/compiler/translate?compiler=compose-wasm`; + break; case TargetPlatforms.JUNIT: url = `${this.server}/api/${version}/compiler/test`; break; @@ -52,6 +55,12 @@ export const API_URLS = { get VERSIONS() { return `${this.server}/versions`; }, + SKIKO_MJS(version) { + return `${this.server}/api/${version}/${TargetPlatforms.COMPOSE_WASM.id}/resource/skiko.mjs`; + }, + SKIKO_WASM(version) { + return `${this.server}/api/${version}/${TargetPlatforms.COMPOSE_WASM.id}/resource/skiko.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 ffbebad..415b0cb 100644 --- a/src/executable-code/executable-fragment.js +++ b/src/executable-code/executable-fragment.js @@ -6,7 +6,7 @@ import directives from 'monkberry-directives'; import 'monkberry-events'; import ExecutableCodeTemplate from './executable-fragment.monk'; import WebDemoApi from '../webdemo-api'; -import {TargetPlatforms, isJsRelated, isJavaRelated} from "../utils/platforms"; +import {TargetPlatforms, isJsRelated, isJavaRelated, isWasmRelated} from "../utils/platforms"; import JsExecutor from "../js-executor" import { @@ -22,6 +22,8 @@ import { countLines, THEMES } from "../utils"; import debounce from 'debounce'; import CompletionView from "../view/completion-view"; import {processErrors} from "../view/output-view"; +import {fetch} from "whatwg-fetch"; +import {API_URLS} from "../config"; const IMPORT_NAME = 'import'; const KEY_CODES = { @@ -96,7 +98,7 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { let sample; let hasMarkers = false; let platform = state.targetPlatform; - if (state.compilerVersion && isJsRelated(platform)) { + if (state.compilerVersion && isJsRelated(platform) || isWasmRelated(platform)) { this.jsExecutor = new JsExecutor(state.compilerVersion); } @@ -257,10 +259,10 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { } onConsoleCloseButtonEnter() { - const {jsLibs, onCloseConsole, targetPlatform } = this.state; + const {jsLibs, onCloseConsole, targetPlatform, compilerVersion } = this.state; // creates a new iframe and removes the old one, thereby stops execution of any running script - if (isJsRelated(targetPlatform)) - this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe(), targetPlatform); + if (isJsRelated(targetPlatform) || isWasmRelated(targetPlatform)) + this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe(), targetPlatform, compilerVersion); this.update({output: "", openConsole: false, exception: null}); if (onCloseConsole) onCloseConsole(); } @@ -326,9 +328,19 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { } ) } else { - this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe(), targetPlatform); - WebDemoApi.translateKotlinToJs(this.getCode(), compilerVersion, targetPlatform, args, hiddenDependencies).then( - state => { + this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe(), targetPlatform, compilerVersion); + const additionalRequests = []; + if (targetPlatform === TargetPlatforms.COMPOSE_WASM) { + if (!this.jsExecutor.skikoImport) { + additionalRequests.push(this.jsExecutor.skikoImport); + } + } + + Promise.all([ + WebDemoApi.translateKotlinToJs(this.getCode(), compilerVersion, targetPlatform, args, hiddenDependencies), + ...additionalRequests + ]).then( + ([state]) => { state.waitingForOutput = false; const jsCode = state.jsCode; const wasm = state.wasm; diff --git a/src/executable-code/index.js b/src/executable-code/index.js index d38b8ce..6075804 100644 --- a/src/executable-code/index.js +++ b/src/executable-code/index.js @@ -23,7 +23,7 @@ import WebDemoApi from "../webdemo-api"; import ExecutableFragment from './executable-fragment'; import { generateCrosslink } from '../lib/crosslink'; import '../styles.scss'; -import {getTargetById, isJsRelated, TargetPlatforms} from "../utils/platforms"; +import {getTargetById, isJsRelated, isWasmRelated, TargetPlatforms} from "../utils/platforms"; const INITED_ATTRIBUTE_NAME = 'data-kotlin-playground-initialized'; const DEFAULT_INDENT = 4; @@ -196,10 +196,10 @@ export default class ExecutableCode { * @returns {Set} - set of additional libraries */ getJsLibraries(targetNode, platform) { + if (isWasmRelated(platform)) { + return new Set() + } if (isJsRelated(platform)) { - if (platform === TargetPlatforms.WASM) { - return new Set() - } const jsLibs = targetNode.getAttribute(ATTRIBUTES.JS_LIBS); let additionalLibs = new Set(API_URLS.JQUERY.split()); if (jsLibs) { diff --git a/src/js-executor/execute-es-module.js b/src/js-executor/execute-es-module.js new file mode 100644 index 0000000..0df7961 --- /dev/null +++ b/src/js-executor/execute-es-module.js @@ -0,0 +1,44 @@ +export async function executeWasmCode(container, jsCode, wasmCode) { + const newCode = prepareJsCode(jsCode); + return execute(container, newCode, wasmCode); +} + +export async function executeWasmCodeWithSkiko(container, jsCode, wasmCode) { + const newCode = prepareJsCode(jsCode) + .replaceAll( + "imports['./skiko.mjs']", + "window.skikoImports" + ); + return execute(container, newCode, 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) { + return container.eval(`import(/* webpackIgnore: true */ '${'data:text/javascript;base64,' + btoa(jsCode)}');`) +} + +function prepareJsCode(jsCode) { + return ` + class BufferedOutput { + constructor() { + this.buffer = "" + } + } + export const bufferedOutput = new BufferedOutput() + ` + + jsCode + .replace( + "instantiateStreaming(fetch(wasmFilePath), importObject)).instance;", + "instantiate(window.wasmCode, importObject)).instance;\nwindow.wasmCode = undefined;" + ) + .replace( + "const importObject = {", + "js_code['kotlin.io.printImpl'] = (message) => bufferedOutput.buffer += message\n" + + "js_code['kotlin.io.printlnImpl'] = (message) => {bufferedOutput.buffer += message;bufferedOutput.buffer += \"\\n\"}\n" + + "const importObject = {" + ); +} diff --git a/src/js-executor/index.js b/src/js-executor/index.js index 9b4ff4e..19b27bd 100644 --- a/src/js-executor/index.js +++ b/src/js-executor/index.js @@ -2,7 +2,8 @@ import './index.scss' import {API_URLS} from "../config"; import {showJsException} from "../view/output-view"; import {processingHtmlBrackets} from "../utils"; -import { TargetPlatforms } from "../utils/platforms"; +import {isWasmRelated, TargetPlatforms} from "../utils/platforms"; +import {fetch} from "whatwg-fetch"; const INIT_SCRIPT = "if(kotlin.BufferedOutput!==undefined){kotlin.out = new kotlin.BufferedOutput()}" + "else{kotlin.kotlin.io.output = new kotlin.kotlin.io.BufferedOutput()}"; @@ -23,6 +24,7 @@ const normalizeJsVersion = version => { export default class JsExecutor { constructor(kotlinVersion) { this.kotlinVersion = kotlinVersion; + this.skikoImport = undefined; } async executeJsCode(jsCode, wasm, jsLibs, platform, outputHeight, theme, onError) { @@ -30,8 +32,28 @@ export default class JsExecutor { this.iframe.style.display = "block"; if (outputHeight) this.iframe.style.height = `${outputHeight}px`; } - if (platform === TargetPlatforms.WASM) { - return await this.executeWasm(jsCode, wasm, theme, onError) + if (isWasmRelated(platform)) { + const executeEsModule = (await import("./execute-es-module")) + if (platform === TargetPlatforms.WASM) { + return await this.executeWasm( + jsCode, + wasm, + executeEsModule.executeWasmCode, + theme, + onError + ) + } + if (platform === TargetPlatforms.COMPOSE_WASM) { + this.iframe.style.display = "block"; + if (outputHeight) this.iframe.style.height = `${outputHeight}px`; + return await this.executeWasm( + jsCode, + wasm, + executeEsModule.executeWasmCodeWithSkiko, + theme, + onError, + ) + } } return await this.execute(jsCode, jsLibs, theme, onError, platform); } @@ -60,28 +82,15 @@ export default class JsExecutor { return await this.execute(jsCode, jsLibs, theme, onError, platform); } - async executeWasm(jsCode, wasmCode, theme, onError) { + async executeWasm( + jsCode, + wasmCode, + executor, + theme, + onError, + ) { try { - const newCode = ` - class BufferedOutput { - constructor() { - this.buffer = "" - } - } - export const bufferedOutput = new BufferedOutput() - ` + - jsCode - .replace( - "instantiateStreaming(fetch(wasmFilePath)", - "instantiate(Uint8Array.from(atob(" + "'" + wasmCode + "'" + "), c => c.charCodeAt(0))" - ) - .replace( - "const importObject = {", - "js_code['kotlin.io.printImpl'] = (message) => bufferedOutput.buffer += message\n" + - "js_code['kotlin.io.printlnImpl'] = (message) => {bufferedOutput.buffer += message;bufferedOutput.buffer += \"\\n\"}\n" + - "const importObject = {" - ) - const exports = await import(/* webpackIgnore: true */ 'data:text/javascript;base64,' + btoa(newCode)); + const exports = await executor(this.iframe.contentWindow, jsCode, wasmCode); await exports.instantiate() const output = exports.bufferedOutput.buffer exports.bufferedOutput.buffer = "" @@ -97,7 +106,7 @@ export default class JsExecutor { return new Promise(resolve => setTimeout(resolve, ms)) } - reloadIframeScripts(jsLibs, node, targetPlatform) { + reloadIframeScripts(jsLibs, node, targetPlatform, compilerVersion) { if (this.iframe !== undefined) { node.removeChild(this.iframe) } @@ -110,7 +119,7 @@ export default class JsExecutor { const kotlinScript = API_URLS.KOTLIN_JS + `${normalizeJsVersion(this.kotlinVersion)}/kotlin.js`; iframeDoc.write(""); } - if (targetPlatform !== TargetPlatforms.WASM) { + if (!isWasmRelated(targetPlatform)) { for (let lib of jsLibs) { iframeDoc.write(""); } @@ -120,6 +129,30 @@ export default class JsExecutor { iframeDoc.write(``); } } + if (targetPlatform === TargetPlatforms.COMPOSE_WASM) { + this.skikoImport = fetch(API_URLS.SKIKO_MJS(compilerVersion), { + 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(compilerVersion)}'` + )) + .then(async skikoCode => { + const module = await import("../js-executor/execute-es-module") + return await module.executeJs(this.iframe.contentWindow, skikoCode) + } + ) + .then(skikoImports => { + this.iframe.contentWindow.skikoImports = skikoImports; + }); + + this.iframe.height = "1000" + iframeDoc.write(``); + } iframeDoc.write(''); iframeDoc.close(); } diff --git a/src/utils/platforms/TargetPlatforms.ts b/src/utils/platforms/TargetPlatforms.ts index 68b1b7d..659e2ce 100644 --- a/src/utils/platforms/TargetPlatforms.ts +++ b/src/utils/platforms/TargetPlatforms.ts @@ -4,6 +4,7 @@ export const TargetPlatforms = { JS: new TargetPlatform('js', 'JavaScript'), JS_IR: new TargetPlatform('js-ir', 'JavaScript IR'), WASM: new TargetPlatform('wasm', 'Wasm'), + COMPOSE_WASM: new TargetPlatform('compose-wasm', 'Compose Wasm'), JAVA: new TargetPlatform('java', 'JVM'), JUNIT: new TargetPlatform('junit', 'JUnit'), CANVAS: new TargetPlatform('canvas', 'JavaScript(canvas)'), diff --git a/src/utils/platforms/index.ts b/src/utils/platforms/index.ts index c60ee7d..4c869b3 100644 --- a/src/utils/platforms/index.ts +++ b/src/utils/platforms/index.ts @@ -18,8 +18,14 @@ export function isJsRelated(platform: TargetPlatform) { return ( platform === TargetPlatforms.JS || platform === TargetPlatforms.JS_IR || - platform === TargetPlatforms.CANVAS || - platform === TargetPlatforms.WASM + platform === TargetPlatforms.CANVAS + ); +} + +export function isWasmRelated(platform: TargetPlatform) { + return ( + platform === TargetPlatforms.WASM || + platform === TargetPlatforms.COMPOSE_WASM ); } diff --git a/src/webdemo-api.js b/src/webdemo-api.js index e1b8e27..d785d9a 100644 --- a/src/webdemo-api.js +++ b/src/webdemo-api.js @@ -1,7 +1,7 @@ import {fetch} from 'whatwg-fetch'; import {API_URLS} from "./config"; import flatten from 'flatten'; -import {TargetPlatforms} from './utils/platforms'; +import {isWasmRelated, TargetPlatforms} from './utils/platforms'; import { findSecurityException, getExceptionCauses, @@ -64,7 +64,7 @@ export default class WebDemoApi { }) } - if (platform === TargetPlatforms.WASM && compilerVersion < MINIMAL_VERSION_WASM) { + if (isWasmRelated(platform) && compilerVersion < MINIMAL_VERSION_WASM) { return Promise.resolve({ output: "", errors: [{