mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 02:37:41 -05:00
Implement PyScript custom <script type> (#1548)
* updated MicroPython to latest in order to have `globals` API available * reduced code around helpers for both MicroPython and Pyodide as now these are more aligned * updated all dependencies and brought in latest [coincident/window](https://github.com/WebReflection/coincident#coincidentwindow) goodness to any `xworker`, preserving the `sync` previous behavior * using [@ungap/structured-clone/json](https://github.com/ungap/structured-clone#tojson) as *coincident* default `parse` and `stringify` utility to allow recursive and more complex data to travel back from the *Worker* (forward data is still fully [structured clone algorithm compatible](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)) * renamed all *plugin/s* references to *custom/s* as plugin as a word was too misleading * changed *custom types* helpers logic to allow any single node to have its own version of the interpreter wrapper, and all the extra fields it carries with it, including a way to augment every interpreter execution, among as every worker code execution * created a `custom` folder where I've landed the very first `pyscript.js` custom type * created an exhaustive test page to demonstrate the current abilities of *PyScript Next* among its ability to expose utilities that can be used to create *PyScript* plugins
This commit is contained in:
committed by
GitHub
parent
0a7e1ce0d7
commit
f6dfc5361e
195
pyscript.core/esm/custom/pyscript.js
Normal file
195
pyscript.core/esm/custom/pyscript.js
Normal file
@@ -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 `<script type="py">` and `<py-script>`
|
||||
define("py", {
|
||||
config,
|
||||
env: "py-script",
|
||||
interpreter: "pyodide",
|
||||
codeBeforeRunWorker() {
|
||||
const { codeBeforeRunWorker: set } = hooks;
|
||||
const prefix = 'print("codeBeforeRunWorker")';
|
||||
return [prefix].concat(...set).join("\n");
|
||||
},
|
||||
codeAfterRunWorker() {
|
||||
const { codeAfterRunWorker: set } = hooks;
|
||||
const prefix = 'print("codeAfterRunWorker")';
|
||||
return [prefix].concat(...set).join("\n");
|
||||
},
|
||||
onBeforeRun(pyodide, element) {
|
||||
bootstrapNodeAndPlugins(pyodide, element, before, "onBeforeRun");
|
||||
pyodide.interpreter.runPython(addDisplay(element));
|
||||
},
|
||||
onBeforeRunAync(pyodide, element) {
|
||||
pyodide.interpreter.runPython(addDisplay(element));
|
||||
bootstrapNodeAndPlugins(
|
||||
pyodide,
|
||||
element,
|
||||
before,
|
||||
"onBeforeRunAync",
|
||||
);
|
||||
},
|
||||
onAfterRun(pyodide, element) {
|
||||
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRun");
|
||||
},
|
||||
onAfterRunAsync(pyodide, element) {
|
||||
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync");
|
||||
},
|
||||
async onRuntimeReady(pyodide, element) {
|
||||
// allows plugins to do whatever they want with the element
|
||||
// before regular stuff happens in here
|
||||
for (const callback of hooks.onRuntimeReady)
|
||||
callback(pyodide, element);
|
||||
if (isScript(element)) {
|
||||
const {
|
||||
attributes: { async: isAsync, target },
|
||||
} = element;
|
||||
const hasTarget = !!target?.value;
|
||||
const show = hasTarget
|
||||
? queryTarget(target.value)
|
||||
: document.createElement("script-py");
|
||||
|
||||
if (!hasTarget) element.after(show);
|
||||
if (!show.id) show.id = getID();
|
||||
|
||||
// allows the code to retrieve the target element via
|
||||
// document.currentScript.target if needed
|
||||
defineProperty(element, "target", { value: show });
|
||||
|
||||
pyodide[`run${isAsync ? "Async" : ""}`](
|
||||
await fetchSource(element),
|
||||
);
|
||||
} else {
|
||||
// resolve PyScriptElement to allow connectedCallback
|
||||
element._pyodide.resolve(pyodide);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
class PyScriptElement extends HTMLElement {
|
||||
constructor() {
|
||||
if (!super().id) this.id = getID();
|
||||
this._pyodide = Promise.withResolvers();
|
||||
this.srcCode = "";
|
||||
this.executed = false;
|
||||
}
|
||||
async connectedCallback() {
|
||||
if (!this.executed) {
|
||||
this.executed = true;
|
||||
const { run } = await this._pyodide.promise;
|
||||
this.srcCode = await fetchSource(this);
|
||||
this.textContent = "";
|
||||
const result = run(this.srcCode);
|
||||
if (!this.textContent && result) this.textContent = result;
|
||||
this.style.display = "block";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("py-script", PyScriptElement);
|
||||
})();
|
||||
|
||||
export const hooks = {
|
||||
/** @type {Set<function>} */
|
||||
onBeforeRun: new Set(),
|
||||
/** @type {Set<function>} */
|
||||
onBeforeRunAync: new Set(),
|
||||
/** @type {Set<function>} */
|
||||
onAfterRun: new Set(),
|
||||
/** @type {Set<function>} */
|
||||
onAfterRunAsync: new Set(),
|
||||
/** @type {Set<function>} */
|
||||
onRuntimeReady: new Set(),
|
||||
|
||||
/** @type {Set<string>} */
|
||||
codeBeforeRunWorker: new Set(),
|
||||
/** @type {Set<string>} */
|
||||
codeBeforeRunWorkerAsync: new Set(),
|
||||
/** @type {Set<string>} */
|
||||
codeAfterRunWorker: new Set(),
|
||||
/** @type {Set<string>} */
|
||||
codeAfterRunWorkerAsync: new Set(),
|
||||
};
|
||||
Reference in New Issue
Block a user