Skip to content

Commit d064995

Browse files
authored
Build files (#44)
* Tech-Preview of Pyscript.core * Build files
1 parent ad04643 commit d064995

20 files changed

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

0 commit comments

Comments
 (0)