Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support compose wasm #201

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,83 @@ fun main(args: Array<String>) {

</div>

You can use Compose Wasm.

```html
<div class="kotlin-code" data-target-platform="compose-wasm"></div>
```
<div class="kotlin-code" data-target-platform="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.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

```

</div>


Use `data-target-platform` attribute with value `junit` for creating examples with tests:

Expand Down
9 changes: 9 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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`;
},
Expand Down
28 changes: 20 additions & 8 deletions src/executable-code/executable-fragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 = {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions src/executable-code/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
44 changes: 44 additions & 0 deletions src/js-executor/execute-es-module.js
Original file line number Diff line number Diff line change
@@ -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 = {"
);
}
85 changes: 59 additions & 26 deletions src/js-executor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()}";
Expand All @@ -23,15 +24,36 @@ const normalizeJsVersion = version => {
export default class JsExecutor {
constructor(kotlinVersion) {
this.kotlinVersion = kotlinVersion;
this.skikoImport = undefined;
}

async executeJsCode(jsCode, wasm, jsLibs, platform, outputHeight, theme, onError) {
if (platform === TargetPlatforms.CANVAS) {
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);
}
Expand Down Expand Up @@ -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 = ""
Expand All @@ -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)
}
Expand All @@ -110,7 +119,7 @@ export default class JsExecutor {
const kotlinScript = API_URLS.KOTLIN_JS + `${normalizeJsVersion(this.kotlinVersion)}/kotlin.js`;
iframeDoc.write("<script src='" + kotlinScript + "'></script>");
}
if (targetPlatform !== TargetPlatforms.WASM) {
if (!isWasmRelated(targetPlatform)) {
for (let lib of jsLibs) {
iframeDoc.write("<script src='" + lib + "'></script>");
}
Expand All @@ -120,6 +129,30 @@ export default class JsExecutor {
iframeDoc.write(`<script>${INIT_SCRIPT}</script>`);
}
}
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(`<canvas height="1000" id="ComposeTarget"></canvas>`);
}
iframeDoc.write('<body style="margin: 0; overflow: hidden;"></body>');
iframeDoc.close();
}
Expand Down
1 change: 1 addition & 0 deletions src/utils/platforms/TargetPlatforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)'),
Expand Down
10 changes: 8 additions & 2 deletions src/utils/platforms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down
Loading
Loading