Skip to content

Commit 8993bf7

Browse files
Merge pull request #69 from pyscript/js_modules
Added a way to register JS modules directly
2 parents 7f84887 + 6d19d97 commit 8993bf7

17 files changed

+347
-151
lines changed

docs/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,59 @@ Please read the [XWorker](#xworker) dedicated section to know more.
254254
</details>
255255

256256

257+
## Extra `config` features
258+
259+
It is possible to land in either the *main* world or the *worker* one native *JS* modules (aka: *ESM*).
260+
261+
In *polyscript*, this is possible by defining one or more `[js_modules.X]` fields in the config, where `X` is either *main* or *worker*:
262+
263+
* `[js_modules.main]` is a list of *source* -> *module name* pairs, similarly to how `[files]` field work, where the *module* name will then be reachable via `polyscript.js_modules.actual_name` in both *main* and *worker* world. As the *main* module lands on the main thread, where there is also likely some UI, it is also possible to define one or more related *CSS* to that module, as long as they target the very same name (see the example to better understand).
264+
* `[js_modules.worker]` is a list of *source* -> *module name* pairs that actually land only in `<script type="x" worker>` cases. These modules are still reachable through the very same `polyscript.js_modules.actual_name` convention and this feature is meant to be used for modules that only works best, or work regardless, outside the *main* world. As example, if your *JS* module implies that `document` or `window` references, among other *DOM* related APIs, are globally available, it means that that module should be part of the `[js_modules.main]` list instead ... however, if the module works out of the box in a *Worker* environment, it is best for performance reasons to explicitly define such module under this field. Please note that *CSS* files are not accepted within this list because there's no way *CSS* can be useful or land in any meaningful way within a *Worker* environment.
265+
266+
### js_modules config example
267+
268+
```toml
269+
[js_modules.main]
270+
# this modules work best on main
271+
"https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet-src.esm.js" = "leaflet"
272+
"https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet.css" = "leaflet" # CSS
273+
# this works in both main and worker
274+
"https://cdn.jsdelivr.net/npm/html-escaper" = "html_escaper"
275+
276+
[js_modules.worker]
277+
# this works out of the box in a worker too
278+
"https://cdn.jsdelivr.net/npm/html-escaper" = "html_escaper"
279+
# this works only in a worker
280+
"https://cdn.jsdelivr.net/npm/worker-only" = "worker_only"
281+
```
282+
283+
```html
284+
<!-- main case -->
285+
<script type="pyodide" config="./that.toml">
286+
# these both works
287+
from polyscript.js_modules import leaflet as L
288+
from polyscript.js_modules import html_escaper
289+
290+
# this fails as it's not reachable in main
291+
from polyscript.js_modules import worker_only
292+
</script>
293+
294+
<!-- worker case -->
295+
<script type="pyodide" config="./that.toml" worker>
296+
# these works by proxying the main module and landing
297+
# on main only when accessed, never before
298+
# the CSS file also lands automatically on demand
299+
from polyscript.js_modules import leaflet as L
300+
301+
# this works out of the box in the worker
302+
from polyscript.js_modules import html_escaper
303+
304+
# this works only in a worker 👍
305+
from polyscript.js_modules import worker_only
306+
</script>
307+
```
308+
309+
257310
## How Events Work
258311
259312
The event should contain the *interpreter* or *custom type* prefix, followed by the *event* type it'd like to handle.

docs/core.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/core.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/custom.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import '@ungap/with-resolvers';
22
import { $$ } from 'basic-devtools';
33

4-
import { assign, create, createOverload, createResolved, dedent, defineProperty, nodeInfo } from './utils.js';
4+
import { JSModules, assign, create, createOverload, createResolved, dedent, defineProperty, nodeInfo } from './utils.js';
55
import { getDetails } from './script-handler.js';
66
import { registry as defaultRegistry, prefixes, configs } from './interpreters.js';
77
import { getRuntimeID } from './loader.js';
@@ -99,7 +99,10 @@ export const handleCustomType = (node) => {
9999
XWorker,
100100
};
101101

102-
module.registerJSModule(interpreter, 'polyscript', { XWorker });
102+
module.registerJSModule(interpreter, 'polyscript', {
103+
js_modules: JSModules,
104+
XWorker,
105+
});
103106

104107
// patch methods accordingly to hooks (and only if needed)
105108
for (const suffix of ['Run', 'RunAsync']) {

esm/interpreter/_utils.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import '@ungap/with-resolvers';
2+
import { UNDEFINED } from 'proxy-target/types';
23

34
import { getBuffer } from '../fetch-utils.js';
4-
import { absoluteURL } from '../utils.js';
5+
import { absoluteURL, all, entries, importCSS, importJS, isArray, isCSS } from '../utils.js';
56

67
// REQUIRES INTEGRATION TEST
78
/* c8 ignore start */
@@ -73,8 +74,6 @@ const resolve = (FS, path) => {
7374
return [FS.cwd()].concat(tree).join('/').replace(/^\/+/, '/');
7475
};
7576

76-
import { all, isArray } from '../utils.js';
77-
7877
const calculateFetchPaths = (config_fetch) => {
7978
// REQUIRES INTEGRATION TEST
8079
/* c8 ignore start */
@@ -142,7 +141,7 @@ const calculateFilesPaths = files => {
142141
const map = new Map;
143142
const targets = new Set;
144143
const sourceDest = [];
145-
for (const [source, dest] of Object.entries(files)) {
144+
for (const [source, dest] of entries(files)) {
146145
if (/^\{.+\}$/.test(source)) {
147146
if (map.has(source))
148147
throw new SyntaxError(`Duplicated template: ${source}`);
@@ -168,4 +167,21 @@ export const fetchFiles = (module, interpreter, config_files) =>
168167
.then((buffer) => module.writeFile(interpreter, path, buffer)),
169168
),
170169
);
170+
171+
const RUNNING_IN_WORKER = typeof document === UNDEFINED;
172+
173+
export const fetchJSModules = ({ main, worker }) => {
174+
const promises = [];
175+
if (worker && RUNNING_IN_WORKER) {
176+
for (const [source, name] of entries(worker))
177+
promises.push(importJS(source, name));
178+
}
179+
if (main && !RUNNING_IN_WORKER) {
180+
for (const [source, name] of entries(main)) {
181+
if (isCSS(source)) importCSS(source);
182+
else promises.push(importJS(source, name));
183+
}
184+
}
185+
return all(promises);
186+
};
171187
/* c8 ignore stop */

esm/interpreter/micropython.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fetchFiles, fetchPaths, stdio, writeFile } from './_utils.js';
1+
import { fetchFiles, fetchJSModules, fetchPaths, stdio, writeFile } from './_utils.js';
22
import { registerJSModule, run, runAsync, runEvent } from './_python.js';
33

44
const type = 'micropython';
@@ -15,6 +15,7 @@ export default {
1515
const interpreter = await get(loadMicroPython({ stderr, stdout, url }));
1616
if (config.files) await fetchFiles(this, interpreter, config.files);
1717
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
18+
if (config.js_modules) await fetchJSModules(config.js_modules);
1819
return interpreter;
1920
},
2021
registerJSModule,

esm/interpreter/pyodide.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fetchFiles, fetchPaths, stdio, writeFile } from './_utils.js';
1+
import { fetchFiles, fetchJSModules, fetchPaths, stdio, writeFile } from './_utils.js';
22
import { registerJSModule, run, runAsync, runEvent } from './_python.js';
33

44
const type = 'pyodide';
@@ -18,6 +18,7 @@ export default {
1818
);
1919
if (config.files) await fetchFiles(this, interpreter, config.files);
2020
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
21+
if (config.js_modules) await fetchJSModules(config.js_modules);
2122
if (config.packages) {
2223
await interpreter.loadPackage('micropip');
2324
const micropip = await interpreter.pyimport('micropip');

esm/interpreter/ruby-wasm-wasi.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { dedent } from '../utils.js';
2-
import { fetchFiles, fetchPaths } from './_utils.js';
2+
import { fetchFiles, fetchJSModules, fetchPaths } from './_utils.js';
33

44
const type = 'ruby-wasm-wasi';
55
const jsType = type.replace(/\W+/g, '_');
@@ -24,6 +24,7 @@ export default {
2424
const { vm: interpreter } = await DefaultRubyVM(module);
2525
if (config.files) await fetchFiles(this, interpreter, config.files);
2626
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
27+
if (config.js_modules) await fetchJSModules(config.js_modules);
2728
return interpreter;
2829
},
2930
// Fallback to globally defined module fields (i.e. $xworker)

esm/interpreter/wasmoon.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { dedent } from '../utils.js';
2-
import { fetchFiles, fetchPaths, io, stdio, writeFileShim } from './_utils.js';
2+
import { fetchFiles, fetchJSModules, fetchPaths, io, stdio, writeFileShim } from './_utils.js';
33

44
const type = 'wasmoon';
55

@@ -21,6 +21,7 @@ export default {
2121
});
2222
if (config.files) await fetchFiles(this, interpreter, config.files);
2323
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
24+
if (config.js_modules) await fetchJSModules(config.js_modules);
2425
return interpreter;
2526
},
2627
// Fallback to globally defined module fields

esm/script-handler.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import $xworker from './worker/class.js';
44
import workerURL from './worker/url.js';
55
import { getRuntime, getRuntimeID } from './loader.js';
66
import { registry } from './interpreters.js';
7-
import { all, dispatch, resolve, defineProperty, nodeInfo } from './utils.js';
7+
import { JSModules, all, dispatch, resolve, defineProperty, nodeInfo } from './utils.js';
88
import { getText } from './fetch-utils.js';
99

1010
const getRoot = (script) => {
@@ -60,7 +60,10 @@ const execute = async (script, source, XWorker, isAsync) => {
6060
configurable: true,
6161
get: () => script,
6262
});
63-
module.registerJSModule(interpreter, 'polyscript', { XWorker });
63+
module.registerJSModule(interpreter, 'polyscript', {
64+
js_modules: JSModules,
65+
XWorker,
66+
});
6467
dispatch(script, type, 'ready');
6568
const result = module[isAsync ? 'runAsync' : 'run'](interpreter, content);
6669
const done = dispatch.bind(null, script, type, 'done');

0 commit comments

Comments
 (0)