Skip to content

Commit 987277c

Browse files
committed
WebR - Expreimental
1 parent 82433df commit 987277c

File tree

12 files changed

+144
-8
lines changed

12 files changed

+144
-8
lines changed

docs/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,10 +772,14 @@ Please note that if a worker is created explicitly, there won't be any element,
772772
| micropython | • | • | • | • | • | • |
773773
| ruby-wasm-wasi | • | • | • | ! | | |
774774
| wasmoon | • | • | • | ! | • | |
775+
| webr | r | • | re | | | |
775776
776777
* **run** allows code to run synchronously and optionally return value
777778
* **runAsync** allows code to run asynchronously and optionally return value
778779
* **runEvent** allows events to be invoked and receive the `event` object
779780
* **registerJSModule** allows `from polyscript import Xworker` or registration of arbitrary modules for *custom types*. It currently fallback to globally defined reference (the module name) whenever it's not possible to register a module (i.e. `polyscriptXWorker` in Lua or `$polyscript.XWorker` in Ruby).
780781
* **writeFile** it's used to save *fetch* config files into virtual FS (usually the one provided by Emscripten). It is then possible to import those files as module within the evaluated code.
781782
* **transform** allows `xworker.sync` related invokes to pass as argument internal objects without issues, simplifying as example the dance needed with *pyodide* and the `ffi.PyProxy` interface, automatically using `.toJs()` for better DX.
783+
784+
* issue **r**: the runtime exposes the `run` utility but this is *not synchronous*
785+
* issue **re**: the event or its listener somehow run but it's not possible to `stopPropagation()` or do other regular *event* operations even on the main thread

docs/index.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

esm/interpreter/webr.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { create } from 'gc-hook';
2+
import { dedent } from '../utils.js';
3+
import { fetchFiles, fetchJSModules, fetchPaths } from './_utils.js';
4+
import { io, stdio } from './_io.js';
5+
6+
const type = 'webr';
7+
const r = new WeakMap();
8+
9+
// REQUIRES INTEGRATION TEST
10+
/* c8 ignore start */
11+
const run = async (interpreter, code) => {
12+
const { shelter, destroy, io } = r.get(interpreter);
13+
const { output, result } = await shelter.captureR(dedent(code));
14+
for (const { type, data } of output) io[type](data);
15+
// this is a double proxy but it's OK as the consumer
16+
// of the result here needs to invoke explicitly a conversion
17+
// or trust the `(await p.toJs()).values` returns what's expected.
18+
return create(result, destroy, { token: false });
19+
};
20+
21+
export default {
22+
type,
23+
experimental: true,
24+
module: (version = '0.3.2') =>
25+
`https://cdn.jsdelivr.net/npm/webr@${version}/dist/webr.mjs`,
26+
async engine(module, config) {
27+
const { get } = stdio();
28+
const interpreter = new module.WebR();
29+
await get(interpreter.init().then(() => interpreter));
30+
const shelter = await new interpreter.Shelter();
31+
r.set(interpreter, {
32+
module,
33+
shelter,
34+
destroy: shelter.destroy.bind(shelter),
35+
io: io.get(interpreter),
36+
});
37+
if (config.files) await fetchFiles(this, interpreter, config.files);
38+
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
39+
if (config.js_modules) await fetchJSModules(config.js_modules);
40+
return interpreter;
41+
},
42+
// Fallback to globally defined module fields (i.e. $xworker)
43+
registerJSModule(_, name) {
44+
console.warn(`Experimental interpreter: module ${name} is not supported (yet)`);
45+
// TODO: as complex JS objects / modules are not allowed
46+
// it's not clear how we can bind anything or import a module
47+
// in a context that doesn't understand methods from JS
48+
// https://docs.r-wasm.org/webr/latest/convert-js-to-r.html#constructing-r-objects-from-javascript-objects
49+
},
50+
run,
51+
runAsync: run,
52+
async runEvent(interpreter, code, event) {
53+
// TODO: WebR cannot convert exoteric objects or any literal
54+
// to an easy to reason about data/frame ... that convertion
55+
// is reserved for the future:
56+
// https://docs.r-wasm.org/webr/latest/convert-js-to-r.html#constructing-r-objects-from-javascript-objects
57+
await interpreter.evalRVoid(`${code}(event)`, {
58+
env: { event: { type: [ event.type ] } }
59+
});
60+
},
61+
transform: (_, value) => {
62+
console.log('transforming', value);
63+
return value;
64+
},
65+
writeFile: () => {
66+
// MAYBE ???
67+
},
68+
};
69+
/* c8 ignore stop */

esm/interpreters.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,6 @@ import micropython from './interpreter/micropython.js';
6262
import pyodide from './interpreter/pyodide.js';
6363
import ruby_wasm_wasi from './interpreter/ruby-wasm-wasi.js';
6464
import wasmoon from './interpreter/wasmoon.js';
65-
for (const interpreter of [micropython, pyodide, ruby_wasm_wasi, wasmoon])
65+
import webr from './interpreter/webr.js';
66+
for (const interpreter of [micropython, pyodide, ruby_wasm_wasi, wasmoon, webr])
6667
register(interpreter);

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@
2323
"size:module": "echo module is $(cat dist/index.js | brotli | wc -c) bytes once compressed",
2424
"size:worker": "echo worker is $(cat esm/worker/xworker.js | brotli | wc -c) bytes once compressed",
2525
"ts": "rm -rf types && tsc -p .",
26-
"update:interpreters": "npm run version:pyodide && npm run version:wasmoon && npm run version:micropython && npm run version:ruby-wasm-wasi && node rollup/update_versions.cjs && npm run build && npm run test",
26+
"update:interpreters": "npm run version:pyodide && npm run version:wasmoon && npm run version:webr && npm run version:micropython && npm run version:ruby-wasm-wasi && node rollup/update_versions.cjs && npm run build && npm run test",
2727
"version:micropython": "npm view @micropython/micropython-webassembly-pyscript version>versions/micropython",
2828
"version:pyodide": "npm view pyodide version>versions/pyodide",
2929
"version:ruby-wasm-wasi": "git ls-remote --tags --refs --sort='v:refname' https://github.com/ruby/ruby.wasm.git | grep 'tags/[[:digit:]]\\.' | tail -n1 | sed 's/.*\\///'>versions/ruby-wasm-wasi",
30-
"version:wasmoon": "npm view wasmoon version>versions/wasmoon"
30+
"version:wasmoon": "npm view wasmoon version>versions/wasmoon",
31+
"version:webr": "npm view webr version>versions/webr"
3132
},
3233
"keywords": [
3334
"polyscript",
@@ -89,6 +90,6 @@
8990
"to-json-callback": "^0.1.1"
9091
},
9192
"worker": {
92-
"blob": "sha256-x9FnCN9Oz1l5i3eTcOPg2IrqbdU7VfskuTHKznW4/L0="
93+
"blob": "sha256-LfRMRHNR4OuZRpfqszG9crCkgf5MX6IPDaoCdHjnzls="
9394
}
9495
}

test/integration.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>polyscript integration tests</title>
77
</head>
8-
<body><ul><li><strong>micropython</strong><ul><li><a href="/test/integration/interpreter/micropython/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/micropython/config-json.html">config-json</a></li><li><a href="/test/integration/interpreter/micropython/config-object.html">config-object</a></li><li><a href="/test/integration/interpreter/micropython/current-script.html">current-script</a></li><li><a href="/test/integration/interpreter/micropython/custom-hooks.html">custom-hooks</a></li><li><a href="/test/integration/interpreter/micropython/fetch.html">fetch</a></li><li><a href="/test/integration/interpreter/micropython/interpreter-local.html">interpreter-local</a></li><li><a href="/test/integration/interpreter/micropython/mip.html">mip</a></li><li><a href="/test/integration/interpreter/micropython/no-type.html">no-type</a></li><li><a href="/test/integration/interpreter/micropython/ready-done.html">ready-done</a></li><li><a href="/test/integration/interpreter/micropython/worker-attribute.html">worker-attribute</a></li><li><a href="/test/integration/interpreter/micropython/worker-bad.html">worker-bad</a></li><li><a href="/test/integration/interpreter/micropython/worker-empty-attribute.html">worker-empty-attribute</a></li><li><a href="/test/integration/interpreter/micropython/worker-error.html">worker-error</a></li><li><a href="/test/integration/interpreter/micropython/worker-lua.html">worker-lua</a></li><li><a href="/test/integration/interpreter/micropython/worker-tag.html">worker-tag</a></li><li><a href="/test/integration/interpreter/micropython/worker-window.html">worker-window</a></li><li><a href="/test/integration/interpreter/micropython/worker.html">worker</a></li></ul><li><strong>pyodide</strong><ul><li><a href="/test/integration/interpreter/pyodide/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/pyodide/button.html">button</a></li><li><a href="/test/integration/interpreter/pyodide/config-json.html">config-json</a></li><li><a href="/test/integration/interpreter/pyodide/fetch.html">fetch</a></li><li><a href="/test/integration/interpreter/pyodide/sync.html">sync</a></li><li><a href="/test/integration/interpreter/pyodide/worker-error.html">worker-error</a></li><li><a href="/test/integration/interpreter/pyodide/worker-transform.html">worker-transform</a></li><li><a href="/test/integration/interpreter/pyodide/worker.html">worker</a></li></ul><li><strong>ruby-wasm-wasi</strong><ul><li><a href="/test/integration/interpreter/ruby-wasm-wasi/bootstrap.html">bootstrap</a></li></ul><li><strong>wasmoon</strong><ul><li><a href="/test/integration/interpreter/wasmoon/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/wasmoon/worker.html">worker</a></li></ul></ul></body>
8+
<body><ul><li><strong>micropython</strong><ul><li><a href="/test/integration/interpreter/micropython/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/micropython/config-json.html">config-json</a></li><li><a href="/test/integration/interpreter/micropython/config-object.html">config-object</a></li><li><a href="/test/integration/interpreter/micropython/current-script.html">current-script</a></li><li><a href="/test/integration/interpreter/micropython/custom-hooks.html">custom-hooks</a></li><li><a href="/test/integration/interpreter/micropython/fetch.html">fetch</a></li><li><a href="/test/integration/interpreter/micropython/interpreter-local.html">interpreter-local</a></li><li><a href="/test/integration/interpreter/micropython/mip.html">mip</a></li><li><a href="/test/integration/interpreter/micropython/no-type.html">no-type</a></li><li><a href="/test/integration/interpreter/micropython/ready-done.html">ready-done</a></li><li><a href="/test/integration/interpreter/micropython/worker-attribute.html">worker-attribute</a></li><li><a href="/test/integration/interpreter/micropython/worker-bad.html">worker-bad</a></li><li><a href="/test/integration/interpreter/micropython/worker-empty-attribute.html">worker-empty-attribute</a></li><li><a href="/test/integration/interpreter/micropython/worker-error.html">worker-error</a></li><li><a href="/test/integration/interpreter/micropython/worker-lua.html">worker-lua</a></li><li><a href="/test/integration/interpreter/micropython/worker-tag.html">worker-tag</a></li><li><a href="/test/integration/interpreter/micropython/worker-window.html">worker-window</a></li><li><a href="/test/integration/interpreter/micropython/worker.html">worker</a></li></ul><li><strong>pyodide</strong><ul><li><a href="/test/integration/interpreter/pyodide/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/pyodide/button.html">button</a></li><li><a href="/test/integration/interpreter/pyodide/config-json.html">config-json</a></li><li><a href="/test/integration/interpreter/pyodide/fetch.html">fetch</a></li><li><a href="/test/integration/interpreter/pyodide/sync.html">sync</a></li><li><a href="/test/integration/interpreter/pyodide/worker-error.html">worker-error</a></li><li><a href="/test/integration/interpreter/pyodide/worker-transform.html">worker-transform</a></li><li><a href="/test/integration/interpreter/pyodide/worker.html">worker</a></li></ul><li><strong>ruby-wasm-wasi</strong><ul><li><a href="/test/integration/interpreter/ruby-wasm-wasi/bootstrap.html">bootstrap</a></li></ul><li><strong>wasmoon</strong><ul><li><a href="/test/integration/interpreter/wasmoon/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/wasmoon/worker.html">worker</a></li></ul><li><strong>webr</strong><ul><li><a href="/test/integration/interpreter/webr/just-click.html">just-click</a></li></ul></ul></body>
99
</html>

test/integration/_shared.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ exports.shared = {
99
await expect(result.trim()).toBe('OK');
1010
},
1111

12+
justClick: ({ expect }, baseURL) => async ({ page }) => {
13+
// Test that a config passed as object works out of the box.
14+
const logs = [];
15+
page.on('console', msg => logs.push(msg.text()));
16+
await page.goto(`${baseURL}/just-click.html`);
17+
await page.waitForSelector('html.ready');
18+
await page.getByRole('button').click();
19+
// this is ugly ... reaction time really slow on listeners (100 is safe)
20+
await new Promise($ => setTimeout($, 100));
21+
await expect(/\bOK\b/.test(logs.at(-1))).toBe(true);
22+
},
23+
1224
worker: ({ expect }, url) => async ({ page }) => {
1325
const logs = [];
1426
page.on('console', msg => logs.push(msg.text()));
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<script type="module">
7+
import { init } from '../utils.js';
8+
init('webr');
9+
</script>
10+
</head>
11+
<body>
12+
<script type="webr">
13+
show_OK <- function(event) {
14+
if (event["type"] == "click")
15+
print("OK")
16+
}
17+
</script>
18+
<button webr-click="show_OK"></button>
19+
</body>
20+
</html>

test/integration/webr.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
3+
const { shared } = require('./_shared.js');
4+
5+
module.exports = (playwright, baseURL) => {
6+
const { test } = playwright;
7+
8+
test('WebR just click', shared.justClick(playwright, baseURL));
9+
};

test/webr.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
6+
<title>WebR</title>
7+
<link rel="stylesheet" href="style.css">
8+
<script type="module" src="/dist/index.js"></script>
9+
</head>
10+
<body>
11+
<script type="webr">
12+
print_version <- function(event) {
13+
if (event["type"] == "click")
14+
print(R.version.string)
15+
}
16+
</script>
17+
<button webr-click="print_version">webr version</button>
18+
</body>
19+
</html>

versions/webr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.3.2

0 commit comments

Comments
 (0)