Skip to content

Commit ad04643

Browse files
author
Ted Patrick
authored
Tech-Preview of Pyscript.core (pyscript#43)
1 parent b49009b commit ad04643

File tree

125 files changed

+46031
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

125 files changed

+46031
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.DS_store

tech-preview/pyscript.core/README.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# @pyscript/core
2+
3+
[![build](https://github.com/WebReflection/python/actions/workflows/node.js.yml/badge.svg)](https://github.com/WebReflection/python/actions/workflows/node.js.yml) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/python/badge.svg?branch=api&t=1RBdLX)](https://coveralls.io/github/WebReflection/python?branch=api)
4+
5+
---
6+
7+
## Development
8+
9+
The working folder (source code of truth) is the `./esm` one, while the `./cjs` is populated as dual module and to test (but it's 1:1 code, no trnaspilation except for imports/exports).
10+
11+
```sh
12+
# install all dependencies needed by core
13+
npm i
14+
```
15+
16+
### Build / Artifacts
17+
18+
This project requires some automatic artifact creation to:
19+
20+
* create a _Worker_ as a _Blob_ based on the same code used by this repo
21+
* create automatically the list of runtimes available via the module
22+
* create the `min.js` file used by most integration tests
23+
* create a sha256 version of the Blob content for CSP cases
24+
25+
Accordingly, to build latest project:
26+
27+
```sh
28+
# create all artifacts needed to test core
29+
npm run build
30+
31+
# optionally spin a server with CORS, COOP, and COEP enabled
32+
npm run server
33+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"type":"commonjs"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import "@ungap/with-resolvers";
2+
import { $$ } from "basic-devtools";
3+
4+
import { assign, create } from "./utils.js";
5+
import { getDetails } from "./script-handler.js";
6+
import {
7+
registry as defaultRegistry,
8+
prefixes,
9+
configs,
10+
} from "./interpreters.js";
11+
import { getRuntimeID } from "./loader.js";
12+
import { io } from "./interpreter/_utils.js";
13+
import { addAllListeners } from "./listeners.js";
14+
15+
import workerHooks from "./worker/hooks.js";
16+
17+
export const CUSTOM_SELECTORS = [];
18+
19+
/**
20+
* @typedef {Object} Runtime custom configuration
21+
* @prop {object} interpreter the bootstrapped interpreter
22+
* @prop {(url:string, options?: object) => Worker} XWorker an XWorker constructor that defaults to same interpreter on the Worker.
23+
* @prop {object} config a cloned config used to bootstrap the interpreter
24+
* @prop {(code:string) => any} run an utility to run code within the interpreter
25+
* @prop {(code:string) => Promise<any>} runAsync an utility to run code asynchronously within the interpreter
26+
* @prop {(path:string, data:ArrayBuffer) => void} writeFile an utility to write a file in the virtual FS, if available
27+
*/
28+
29+
const patched = new Map();
30+
const types = new Map();
31+
const waitList = new Map();
32+
33+
// REQUIRES INTEGRATION TEST
34+
/* c8 ignore start */
35+
/**
36+
* @param {Element} node any DOM element registered via define.
37+
*/
38+
export const handleCustomType = (node) => {
39+
for (const selector of CUSTOM_SELECTORS) {
40+
if (node.matches(selector)) {
41+
const type = types.get(selector);
42+
const { resolve } = waitList.get(type);
43+
const { options, known } = registry.get(type);
44+
if (!known.has(node)) {
45+
known.add(node);
46+
const {
47+
interpreter: runtime,
48+
version,
49+
config,
50+
env,
51+
onRuntimeReady,
52+
} = options;
53+
const name = getRuntimeID(runtime, version);
54+
const id = env || `${name}${config ? `|${config}` : ""}`;
55+
const { interpreter: engine, XWorker } = getDetails(
56+
runtime,
57+
id,
58+
name,
59+
version,
60+
config,
61+
);
62+
engine.then((interpreter) => {
63+
if (!patched.has(id)) {
64+
const module = create(defaultRegistry.get(runtime));
65+
const {
66+
onBeforeRun,
67+
onBeforeRunAsync,
68+
onAfterRun,
69+
onAfterRunAsync,
70+
codeBeforeRunWorker,
71+
codeBeforeRunWorkerAsync,
72+
codeAfterRunWorker,
73+
codeAfterRunWorkerAsync,
74+
} = options;
75+
76+
// These two loops mimic a `new Map(arrayContent)` without needing
77+
// the new Map overhead so that [name, [before, after]] can be easily destructured
78+
// and new sync or async patches become easy to add (when the logic is the same).
79+
80+
// patch sync
81+
for (const [name, [before, after]] of [
82+
["run", [onBeforeRun, onAfterRun]],
83+
]) {
84+
const method = module[name];
85+
module[name] = function (interpreter, code) {
86+
if (before) before.call(this, resolved, node);
87+
const result = method.call(
88+
this,
89+
interpreter,
90+
code,
91+
);
92+
if (after) after.call(this, resolved, node);
93+
return result;
94+
};
95+
}
96+
97+
// patch async
98+
for (const [name, [before, after]] of [
99+
["runAsync", [onBeforeRunAsync, onAfterRunAsync]],
100+
]) {
101+
const method = module[name];
102+
module[name] = async function (interpreter, code) {
103+
if (before)
104+
await before.call(this, resolved, node);
105+
const result = await method.call(
106+
this,
107+
interpreter,
108+
code,
109+
);
110+
if (after)
111+
await after.call(this, resolved, node);
112+
return result;
113+
};
114+
}
115+
116+
// setup XWorker hooks, allowing strings to be forwarded to the worker
117+
// whenever it's created, as functions can't possibly be serialized
118+
// unless these are pure with no outer scope access (or globals vars)
119+
// so that making it strings disambiguate about their running context.
120+
workerHooks.set(XWorker, {
121+
beforeRun: codeBeforeRunWorker,
122+
beforeRunAsync: codeBeforeRunWorkerAsync,
123+
afterRun: codeAfterRunWorker,
124+
afterRunAsync: codeAfterRunWorkerAsync,
125+
});
126+
127+
module.setGlobal(interpreter, "XWorker", XWorker);
128+
129+
const resolved = {
130+
type,
131+
interpreter,
132+
XWorker,
133+
io: io.get(interpreter),
134+
config: structuredClone(configs.get(name)),
135+
run: module.run.bind(module, interpreter),
136+
runAsync: module.runAsync.bind(module, interpreter),
137+
};
138+
139+
patched.set(id, resolved);
140+
resolve(resolved);
141+
}
142+
143+
onRuntimeReady?.(patched.get(id), node);
144+
});
145+
}
146+
}
147+
}
148+
};
149+
150+
/**
151+
* @type {Map<string, {options:object, known:WeakSet<Element>}>}
152+
*/
153+
const registry = new Map();
154+
155+
/**
156+
* @typedef {Object} PluginOptions custom configuration
157+
* @prop {'pyodide' | 'micropython' | 'wasmoon' | 'ruby-wasm-wasi'} interpreter the interpreter to use
158+
* @prop {string} [version] the optional interpreter version to use
159+
* @prop {string} [config] the optional config to use within such interpreter
160+
* @prop {(environment: object, node: Element) => void} [onRuntimeReady] the callback that will be invoked once
161+
*/
162+
163+
/**
164+
* Allows custom types and components on the page to receive interpreters to execute any code
165+
* @param {string} type the unique `<script type="...">` identifier
166+
* @param {PluginOptions} options the custom type configuration
167+
*/
168+
export const define = (type, options) => {
169+
if (defaultRegistry.has(type) || registry.has(type))
170+
throw new Error(`<script type="${type}"> already registered`);
171+
172+
if (!defaultRegistry.has(options?.interpreter))
173+
throw new Error(`Unspecified interpreter`);
174+
175+
// allows reaching out the interpreter helpers on events
176+
defaultRegistry.set(type, defaultRegistry.get(options?.interpreter));
177+
178+
// ensure a Promise can resolve once a custom type has been bootstrapped
179+
whenDefined(type);
180+
181+
// allows selector -> registry by type
182+
const selectors = [`script[type="${type}"]`, `${type}-script`];
183+
for (const selector of selectors) types.set(selector, type);
184+
185+
CUSTOM_SELECTORS.push(...selectors);
186+
prefixes.push(`${type}-`);
187+
188+
// ensure always same env for this custom type
189+
registry.set(type, {
190+
options: assign({ env: type }, options),
191+
known: new WeakSet(),
192+
});
193+
194+
addAllListeners(document);
195+
$$(selectors.join(",")).forEach(handleCustomType);
196+
};
197+
198+
/**
199+
* Resolves whenever a defined custom type is bootstrapped on the page
200+
* @param {string} type the unique `<script type="...">` identifier
201+
* @returns {Promise<object>}
202+
*/
203+
export const whenDefined = (type) => {
204+
if (!waitList.has(type)) waitList.set(type, Promise.withResolvers());
205+
return waitList.get(type).promise;
206+
};
207+
/* c8 ignore stop */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @param {Response} response */
2+
export const getBuffer = (response) => response.arrayBuffer();
3+
4+
/** @param {Response} response */
5+
export const getJSON = (response) => response.json();
6+
7+
/** @param {Response} response */
8+
export const getText = (response) => response.text();
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { $$ } from "basic-devtools";
2+
3+
import xworker from "./worker/class.js";
4+
import { handle } from "./script-handler.js";
5+
import { assign } from "./utils.js";
6+
import { selectors, prefixes } from "./interpreters.js";
7+
import { CUSTOM_SELECTORS, handleCustomType } from "./custom-types.js";
8+
import { listener, addAllListeners } from "./listeners.js";
9+
10+
export { define, whenDefined } from "./custom-types.js";
11+
export const XWorker = xworker();
12+
13+
const INTERPRETER_SELECTORS = selectors.join(",");
14+
15+
const mo = new MutationObserver((records) => {
16+
for (const { type, target, attributeName, addedNodes } of records) {
17+
// attributes are tested via integration / e2e
18+
/* c8 ignore next 17 */
19+
if (type === "attributes") {
20+
const i = attributeName.lastIndexOf("-") + 1;
21+
if (i) {
22+
const prefix = attributeName.slice(0, i);
23+
for (const p of prefixes) {
24+
if (prefix === p) {
25+
const type = attributeName.slice(i);
26+
if (type !== "env") {
27+
const method = target.hasAttribute(attributeName)
28+
? "add"
29+
: "remove";
30+
target[`${method}EventListener`](type, listener);
31+
}
32+
break;
33+
}
34+
}
35+
}
36+
continue;
37+
}
38+
for (const node of addedNodes) {
39+
if (node.nodeType === 1) {
40+
addAllListeners(node);
41+
if (node.matches(INTERPRETER_SELECTORS)) handle(node);
42+
else {
43+
$$(INTERPRETER_SELECTORS, node).forEach(handle);
44+
if (!CUSTOM_SELECTORS.length) continue;
45+
handleCustomType(node);
46+
$$(CUSTOM_SELECTORS.join(","), node).forEach(
47+
handleCustomType,
48+
);
49+
}
50+
}
51+
}
52+
}
53+
});
54+
55+
const observe = (root) => {
56+
mo.observe(root, { childList: true, subtree: true, attributes: true });
57+
return root;
58+
};
59+
60+
const { attachShadow } = Element.prototype;
61+
assign(Element.prototype, {
62+
attachShadow(init) {
63+
return observe(attachShadow.call(this, init));
64+
},
65+
});
66+
67+
addAllListeners(observe(document));
68+
$$(INTERPRETER_SELECTORS, document).forEach(handle);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { clean, writeFile as writeFileUtil } from "./_utils.js";
2+
3+
// REQUIRES INTEGRATION TEST
4+
/* c8 ignore start */
5+
export const run = (interpreter, code) => interpreter.runPython(clean(code));
6+
7+
export const runAsync = (interpreter, code) =>
8+
interpreter.runPythonAsync(clean(code));
9+
10+
export const writeFile = ({ FS }, path, buffer) =>
11+
writeFileUtil(FS, path, buffer);
12+
/* c8 ignore stop */

0 commit comments

Comments
 (0)