diff --git a/.prettierignore b/.prettierignore index 2797bf5c..24675894 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,3 +9,4 @@ pyscript.core/types/ pyscript.core/esm/worker/xworker.js pyscript.core/cjs/package.json pyscript.core/min.js +pyscript.core/pyscript.js diff --git a/pyscript.core/.gitignore b/pyscript.core/.gitignore index a970cd0d..3150ddba 100644 --- a/pyscript.core/.gitignore +++ b/pyscript.core/.gitignore @@ -3,6 +3,6 @@ coverage/ node_modules/ cjs/ !cjs/package.json -min.js +core.js esm/worker/xworker.js types/ diff --git a/pyscript.core/README.md b/pyscript.core/README.md index 2698ae08..b67f31ef 100644 --- a/pyscript.core/README.md +++ b/pyscript.core/README.md @@ -19,7 +19,7 @@ This project requires some automatic artifact creation to: * create a _Worker_ as a _Blob_ based on the same code used by this repo * create automatically the list of runtimes available via the module - * create the `min.js` file used by most integration tests + * create the `core.js` file used by most integration tests * create a sha256 version of the Blob content for CSP cases Accordingly, to build latest project: diff --git a/pyscript.core/esm/custom-types.js b/pyscript.core/esm/custom.js similarity index 51% rename from pyscript.core/esm/custom-types.js rename to pyscript.core/esm/custom.js index 517efd0f..883527d8 100644 --- a/pyscript.core/esm/custom-types.js +++ b/pyscript.core/esm/custom.js @@ -12,8 +12,6 @@ import { getRuntimeID } from "./loader.js"; import { io } from "./interpreter/_utils.js"; import { addAllListeners } from "./listeners.js"; -import workerHooks from "./worker/hooks.js"; - export const CUSTOM_SELECTORS = []; /** @@ -26,7 +24,6 @@ export const CUSTOM_SELECTORS = []; * @prop {(path:string, data:ArrayBuffer) => void} writeFile an utility to write a file in the virtual FS, if available */ -const patched = new Map(); const types = new Map(); const waitList = new Map(); @@ -52,7 +49,7 @@ export const handleCustomType = (node) => { } = options; const name = getRuntimeID(runtime, version); const id = env || `${name}${config ? `|${config}` : ""}`; - const { interpreter: engine, XWorker } = getDetails( + const { interpreter: engine, XWorker: Worker } = getDetails( runtime, id, name, @@ -60,87 +57,79 @@ export const handleCustomType = (node) => { config, ); engine.then((interpreter) => { - if (!patched.has(id)) { - const module = create(defaultRegistry.get(runtime)); - const { - onBeforeRun, - onBeforeRunAsync, - onAfterRun, - onAfterRunAsync, - codeBeforeRunWorker, - codeBeforeRunWorkerAsync, - codeAfterRunWorker, - codeAfterRunWorkerAsync, - } = options; + const module = create(defaultRegistry.get(runtime)); - // These two loops mimic a `new Map(arrayContent)` without needing - // the new Map overhead so that [name, [before, after]] can be easily destructured - // and new sync or async patches become easy to add (when the logic is the same). + const { + onBeforeRun, + onBeforeRunAsync, + onAfterRun, + onAfterRunAsync, + codeBeforeRunWorker, + codeBeforeRunWorkerAsync, + codeAfterRunWorker, + codeAfterRunWorkerAsync, + } = options; - // patch sync - for (const [name, [before, after]] of [ - ["run", [onBeforeRun, onAfterRun]], - ]) { - const method = module[name]; - module[name] = function (interpreter, code) { - if (before) before.call(this, resolved, node); - const result = method.call( - this, - interpreter, - code, - ); - if (after) after.call(this, resolved, node); - return result; - }; - } + const hooks = { + beforeRun: codeBeforeRunWorker?.(), + beforeRunAsync: codeBeforeRunWorkerAsync?.(), + afterRun: codeAfterRunWorker?.(), + afterRunAsync: codeAfterRunWorkerAsync?.(), + }; - // patch async - for (const [name, [before, after]] of [ - ["runAsync", [onBeforeRunAsync, onAfterRunAsync]], - ]) { - const method = module[name]; - module[name] = async function (interpreter, code) { - if (before) - await before.call(this, resolved, node); - const result = await method.call( - this, - interpreter, - code, - ); - if (after) - await after.call(this, resolved, node); - return result; - }; - } + const XWorker = function XWorker(...args) { + return Worker.apply(hooks, args); + }; - // setup XWorker hooks, allowing strings to be forwarded to the worker - // whenever it's created, as functions can't possibly be serialized - // unless these are pure with no outer scope access (or globals vars) - // so that making it strings disambiguate about their running context. - workerHooks.set(XWorker, { - beforeRun: codeBeforeRunWorker, - beforeRunAsync: codeBeforeRunWorkerAsync, - afterRun: codeAfterRunWorker, - afterRunAsync: codeAfterRunWorkerAsync, - }); + // These two loops mimic a `new Map(arrayContent)` without needing + // the new Map overhead so that [name, [before, after]] can be easily destructured + // and new sync or async patches become easy to add (when the logic is the same). - module.setGlobal(interpreter, "XWorker", XWorker); - - const resolved = { - type, - interpreter, - XWorker, - io: io.get(interpreter), - config: structuredClone(configs.get(name)), - run: module.run.bind(module, interpreter), - runAsync: module.runAsync.bind(module, interpreter), + // patch sync + for (const [name, [before, after]] of [ + ["run", [onBeforeRun, onAfterRun]], + ]) { + const method = module[name]; + module[name] = function (interpreter, code) { + if (before) before.call(this, resolved, node); + const result = method.call(this, interpreter, code); + if (after) after.call(this, resolved, node); + return result; }; - - patched.set(id, resolved); - resolve(resolved); } - onRuntimeReady?.(patched.get(id), node); + // patch async + for (const [name, [before, after]] of [ + ["runAsync", [onBeforeRunAsync, onAfterRunAsync]], + ]) { + const method = module[name]; + module[name] = async function (interpreter, code) { + if (before) await before.call(this, resolved, node); + const result = await method.call( + this, + interpreter, + code, + ); + if (after) await after.call(this, resolved, node); + return result; + }; + } + + module.setGlobal(interpreter, "XWorker", XWorker); + + const resolved = { + type, + interpreter, + XWorker, + io: io.get(interpreter), + config: structuredClone(configs.get(name)), + run: module.run.bind(module, interpreter), + runAsync: module.runAsync.bind(module, interpreter), + }; + + resolve(resolved); + + onRuntimeReady?.(resolved, node); }); } } diff --git a/pyscript.core/esm/custom/pyscript.js b/pyscript.core/esm/custom/pyscript.js new file mode 100644 index 00000000..6da58df5 --- /dev/null +++ b/pyscript.core/esm/custom/pyscript.js @@ -0,0 +1,195 @@ +import "@ungap/with-resolvers"; +import { $ } from "basic-devtools"; + +import { define } from "../index.js"; +import { queryTarget } from "../script-handler.js"; +import { defineProperty } from "../utils.js"; +import { getText } from "../fetch-utils.js"; + +// TODO: should this utility be in core instead? +import { robustFetch as fetch } from "./pyscript/fetch.js"; + +// append ASAP CSS to avoid showing content +document.head.appendChild(document.createElement("style")).textContent = ` + py-script, py-config { + display: none; + } +`; + +(async () => { + // create a unique identifier when/if needed + let id = 0; + const getID = (prefix = "py") => `${prefix}-${id++}`; + + // find the shared config for all py-script elements + let config; + let pyConfig = $("py-config"); + if (pyConfig) config = pyConfig.getAttribute("src") || pyConfig.textContent; + else { + pyConfig = $('script[type="py"]'); + config = pyConfig?.getAttribute("config"); + } + + if (/^https?:\/\//.test(config)) config = await fetch(config).then(getText); + + // generic helper to disambiguate between custom element and script + const isScript = (element) => element.tagName === "SCRIPT"; + + // helper for all script[type="py"] out there + const before = (script) => { + defineProperty(document, "currentScript", { + configurable: true, + get: () => script, + }); + }; + + const after = () => { + delete document.currentScript; + }; + + /** + * Given a generic DOM Element, tries to fetch the 'src' attribute, if present. + * It either throws an error if the 'src' can't be fetched or it returns a fallback + * content as source. + */ + const fetchSource = async (tag) => { + if (tag.hasAttribute("src")) { + try { + const response = await fetch(tag.getAttribute("src")); + return response.then(getText); + } catch (error) { + // TODO _createAlertBanner(err) instead ? + alert(error.message); + throw error; + } + } + return tag.textContent; + }; + + // common life-cycle handlers for any node + const bootstrapNodeAndPlugins = (pyodide, element, callback, hook) => { + if (isScript(element)) callback(element); + for (const fn of hooks[hook]) fn(pyodide, element); + }; + + const addDisplay = (element) => { + const id = isScript(element) ? element.target.id : element.id; + return ` + # this code is just for demo purpose but the basics work + def _display(what, target="${id}", append=True): + from js import document + element = document.getElementById(target) + element.textContent = what + display = _display + `; + }; + + // define the module as both ` + + + + + - + - + - + - + +
diff --git a/pyscript.core/test/micropython.html b/pyscript.core/test/micropython.html index e94794b7..205001f3 100644 --- a/pyscript.core/test/micropython.html +++ b/pyscript.core/test/micropython.html @@ -6,7 +6,7 @@ python - + - + diff --git a/pyscript.core/test/plugins/index.html b/pyscript.core/test/plugins/index.html index 4a23d509..99e24815 100644 --- a/pyscript.core/test/plugins/index.html +++ b/pyscript.core/test/plugins/index.html @@ -10,7 +10,7 @@ } + - +
+ + Something something about something ... +
[[fetch]] from = "../" to_folder = "./" files = ["a.py", "b.py"] + + import js import a, b @@ -29,8 +35,24 @@ 'Hello Web!' + # note the target is this element itself + display('second <py-script>') + + + # note this is late to the party simply because + # pyodide needs to be bootstrapped in the Worker too XWorker('../a.py') 'OK' + + + + diff --git a/pyscript.core/test/plugins/py-script.js b/pyscript.core/test/plugins/py-script.js deleted file mode 100644 index 8b05d7b2..00000000 --- a/pyscript.core/test/plugins/py-script.js +++ /dev/null @@ -1,72 +0,0 @@ -import { define } from "@pyscript/core"; - -// append ASAP CSS to avoid showing content -document.head.appendChild(document.createElement("style")).textContent = ` - py-script, py-config { - display: none; - } -`; - -// create a unique identifier when/if needed -let id = 0; -const getID = (prefix = "py-script") => `${prefix}-${id++}`; - -let bootstrap = true, - XWorker, - sharedRuntime; -const sharedPyodide = new Promise((resolve) => { - const pyConfig = document.querySelector("py-config"); - const config = pyConfig?.getAttribute("src") || pyConfig?.textContent; - define("py", { - config, - interpreter: "pyodide", - codeBeforeRunWorker: `print('codeBeforeRunWorker')`, - codeAfterRunWorker: `print('codeAfterRunWorker')`, - onBeforeRun(pyodide, node) { - pyodide.interpreter.globals.set("XWorker", XWorker); - console.log("onBeforeRun", sharedRuntime === pyodide, node); - }, - onAfterRun(pyodide, node) { - console.log("onAfterRun", sharedRuntime === pyodide, node); - }, - async onRuntimeReady(pyodide) { - // bootstrap the shared runtime once - // as each node as plugin gets onRuntimeReady called once - // because no custom-element is strictly needed - if (bootstrap) { - bootstrap = false; - sharedRuntime = pyodide; - XWorker = pyodide.XWorker; - pyodide.io.stdout = (message) => { - console.log("🐍", pyodide.type, message); - }; - // do any module / JS injection in here such as - // Element, display, and friends ... then: - resolve(pyodide); - } - }, - }); -}); - -/** @type {WeakSet} */ -const known = new WeakSet(); - -class PyScriptElement extends HTMLElement { - constructor() { - if (!super().id) this.id = getID(); - } - async connectedCallback() { - if (!known.has(this)) { - known.add(this); - // sharedPyodide contains various helpers including run and runAsync - const { run } = await sharedPyodide; - // do any stuff needed to finalize this element bootstrap - // (i.e. check src attribute and so on) - this.replaceChildren(run(this.textContent) || ""); - // reveal the node on the page - this.style.display = "block"; - } - } -} - -customElements.define("py-script", PyScriptElement); diff --git a/pyscript.core/test/py-events.html b/pyscript.core/test/py-events.html index 836c2086..89474441 100644 --- a/pyscript.core/test/py-events.html +++ b/pyscript.core/test/py-events.html @@ -6,7 +6,7 @@ python events - + - + - + + diff --git a/pyscript.core/test/test.html b/pyscript.core/test/test.html index e74fefef..e9e0b2e6 100644 --- a/pyscript.core/test/test.html +++ b/pyscript.core/test/test.html @@ -9,8 +9,9 @@ { "imports": { "basic-devtools": "../node_modules/basic-devtools/esm/index.js", - "coincident/structured": "../node_modules/coincident/structured.js", - "@ungap/with-resolvers": "../node_modules/@ungap/with-resolvers/index.js" + "coincident/window": "../node_modules/coincident/window.js", + "@ungap/with-resolvers": "../node_modules/@ungap/with-resolvers/index.js", + "@ungap/structured-clone/json": "../node_modules/@ungap/structured-clone/esm/json.js" } } diff --git a/pyscript.core/test/wasmoon.html b/pyscript.core/test/wasmoon.html index 3da2551e..a2ea615d 100644 --- a/pyscript.core/test/wasmoon.html +++ b/pyscript.core/test/wasmoon.html @@ -9,8 +9,9 @@ { "imports": { "basic-devtools": "../node_modules/basic-devtools/esm/index.js", - "coincident/structured": "../node_modules/coincident/structured.js", - "@ungap/with-resolvers": "../node_modules/@ungap/with-resolvers/index.js" + "coincident/window": "../node_modules/coincident/window.js", + "@ungap/with-resolvers": "../node_modules/@ungap/with-resolvers/index.js", + "@ungap/structured-clone/json": "../node_modules/@ungap/structured-clone/esm/json.js" } } diff --git a/pyscript.core/test/worker/index.html b/pyscript.core/test/worker/index.html index 6bd8d3b1..eb5d882d 100644 --- a/pyscript.core/test/worker/index.html +++ b/pyscript.core/test/worker/index.html @@ -8,8 +8,9 @@ { "imports": { "basic-devtools": "../../node_modules/basic-devtools/esm/index.js", - "coincident/structured": "../../node_modules/coincident/structured.js", + "coincident/window": "../../node_modules/coincident/window.js", "@ungap/with-resolvers": "../../node_modules/@ungap/with-resolvers/index.js", + "@ungap/structured-clone/json": "../../node_modules/@ungap/structured-clone/esm/json.js", "@pyscript/core": "../../esm/index.js" } } diff --git a/pyscript.core/test/worker/input.html b/pyscript.core/test/worker/input.html index a0282e42..53af9dc0 100644 --- a/pyscript.core/test/worker/input.html +++ b/pyscript.core/test/worker/input.html @@ -8,8 +8,9 @@ { "imports": { "basic-devtools": "../../node_modules/basic-devtools/esm/index.js", - "coincident/structured": "../../node_modules/coincident/structured.js", + "coincident/window": "../../node_modules/coincident/window.js", "@ungap/with-resolvers": "../../node_modules/@ungap/with-resolvers/index.js", + "@ungap/structured-clone/json": "../../node_modules/@ungap/structured-clone/esm/json.js", "@pyscript/core": "../../esm/index.js" } }