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:
Andrea Giammarchi
2023-06-22 17:29:07 +02:00
committed by GitHub
parent 0a7e1ce0d7
commit f6dfc5361e
49 changed files with 634 additions and 283 deletions

View File

@@ -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

View File

@@ -3,6 +3,6 @@ coverage/
node_modules/
cjs/
!cjs/package.json
min.js
core.js
esm/worker/xworker.js
types/

View File

@@ -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:

View File

@@ -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);
});
}
}

View 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(),
};

View File

@@ -0,0 +1,80 @@
const CLOSEBUTTON = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill="currentColor" width="12px"><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>`;
/**
* These error codes are used to identify the type of error that occurred.
* @see https://docs.pyscript.net/latest/reference/exceptions.html?highlight=errors
*/
export const ErrorCode = {
GENERIC: "PY0000", // Use this only for development then change to a more specific error code
FETCH_ERROR: "PY0001",
FETCH_NAME_ERROR: "PY0002",
// Currently these are created depending on error code received from fetching
FETCH_UNAUTHORIZED_ERROR: "PY0401",
FETCH_FORBIDDEN_ERROR: "PY0403",
FETCH_NOT_FOUND_ERROR: "PY0404",
FETCH_SERVER_ERROR: "PY0500",
FETCH_UNAVAILABLE_ERROR: "PY0503",
BAD_CONFIG: "PY1000",
MICROPIP_INSTALL_ERROR: "PY1001",
BAD_PLUGIN_FILE_EXTENSION: "PY2000",
NO_DEFAULT_EXPORT: "PY2001",
TOP_LEVEL_AWAIT: "PY9000",
};
export class UserError extends Error {
constructor(errorCode, message = "", messageType = "text") {
super(`(${errorCode}): ${message}`);
this.errorCode = errorCode;
this.messageType = messageType;
this.name = "UserError";
}
}
export class FetchError extends UserError {
constructor(errorCode, message) {
super(errorCode, message);
this.name = "FetchError";
}
}
export class InstallError extends UserError {
constructor(errorCode, message) {
super(errorCode, message);
this.name = "InstallError";
}
}
export function _createAlertBanner(
message,
level,
messageType = "text",
logMessage = true,
) {
switch (`log-${level}-${logMessage}`) {
case "log-error-true":
console.error(message);
break;
case "log-warning-true":
console.warn(message);
break;
}
const content = messageType === "html" ? "innerHTML" : "textContent";
const banner = Object.assign(document.createElement("div"), {
className: `alert-banner py-${level}`,
[content]: message,
});
if (level === "warning") {
const closeButton = Object.assign(document.createElement("button"), {
id: "alert-close-button",
innerHTML: CLOSEBUTTON,
});
banner.appendChild(closeButton).addEventListener("click", () => {
banner.remove();
});
}
document.body.prepend(banner);
}

View File

@@ -0,0 +1,63 @@
import { FetchError, ErrorCode } from "./exceptions";
/**
* This is a fetch wrapper that handles any non 200 responses and throws a
* FetchError with the right ErrorCode. This is useful because our FetchError
* will automatically create an alert banner.
*
* @param {string} url - URL to fetch
* @param {Request} [options] - options to pass to fetch
* @returns {Promise<Response>}
*/
export async function robustFetch(url, options) {
let response;
// Note: We need to wrap fetch into a try/catch block because fetch
// throws a TypeError if the URL is invalid such as http://blah.blah
try {
response = await fetch(url, options);
} catch (err) {
const error = err;
let errMsg;
if (url.startsWith("http")) {
errMsg =
`Fetching from URL ${url} failed with error ` +
`'${error.message}'. Are your filename and path correct?`;
} else {
errMsg = `PyScript: Access to local files
(using [[fetch]] configurations in &lt;py-config&gt;)
is not available when directly opening a HTML file;
you must use a webserver to serve the additional files.
See <a style="text-decoration: underline;" href="https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062">this reference</a>
on starting a simple webserver with Python.
`;
}
throw new FetchError(ErrorCode.FETCH_ERROR, errMsg);
}
// Note that response.ok is true for 200-299 responses
if (!response.ok) {
const errorMsg = `Fetching from URL ${url} failed with error ${response.status} (${response.statusText}). Are your filename and path correct?`;
switch (response.status) {
case 404:
throw new FetchError(ErrorCode.FETCH_NOT_FOUND_ERROR, errorMsg);
case 401:
throw new FetchError(
ErrorCode.FETCH_UNAUTHORIZED_ERROR,
errorMsg,
);
case 403:
throw new FetchError(ErrorCode.FETCH_FORBIDDEN_ERROR, errorMsg);
case 500:
throw new FetchError(ErrorCode.FETCH_SERVER_ERROR, errorMsg);
case 503:
throw new FetchError(
ErrorCode.FETCH_UNAVAILABLE_ERROR,
errorMsg,
);
default:
throw new FetchError(ErrorCode.FETCH_ERROR, errorMsg);
}
}
return response;
}

View File

@@ -4,10 +4,10 @@ import xworker from "./worker/class.js";
import { handle } from "./script-handler.js";
import { assign } from "./utils.js";
import { selectors, prefixes } from "./interpreters.js";
import { CUSTOM_SELECTORS, handleCustomType } from "./custom-types.js";
import { CUSTOM_SELECTORS, handleCustomType } from "./custom.js";
import { listener, addAllListeners } from "./listeners.js";
export { define, whenDefined } from "./custom-types.js";
export { define, whenDefined } from "./custom.js";
export const XWorker = xworker();
const INTERPRETER_SELECTORS = selectors.join(",");

View File

@@ -7,6 +7,12 @@ export const run = (interpreter, code) => interpreter.runPython(clean(code));
export const runAsync = (interpreter, code) =>
interpreter.runPythonAsync(clean(code));
export const setGlobal = (interpreter, name, value) =>
interpreter.globals.set(name, value);
export const deleteGlobal = (interpreter, name) =>
interpreter.globals.delete(name);
export const writeFile = ({ FS }, path, buffer) =>
writeFileUtil(FS, path, buffer);
/* c8 ignore stop */

View File

@@ -1,5 +1,11 @@
import { fetchPaths, stdio } from "./_utils.js";
import { run, runAsync, writeFile } from "./_python.js";
import {
run,
runAsync,
setGlobal,
deleteGlobal,
writeFile,
} from "./_python.js";
const type = "micropython";
@@ -7,7 +13,7 @@ const type = "micropython";
/* c8 ignore start */
export default {
type,
module: (version = "1.20.0-239") =>
module: (version = "1.20.0-253") =>
`https://cdn.jsdelivr.net/npm/@micropython/micropython-webassembly-pyscript@${version}/micropython.mjs`,
async engine({ loadMicroPython }, config, url) {
const { stderr, stdout, get } = stdio();
@@ -16,16 +22,8 @@ export default {
if (config.fetch) await fetchPaths(this, runtime, config.fetch);
return runtime;
},
setGlobal(interpreter, name, value) {
const id = `__pyscript_${this.type}_${name}`;
globalThis[id] = value;
this.run(interpreter, `from js import ${id};${name}=${id};`);
},
deleteGlobal(interpreter, name) {
const id = `__pyscript_${this.type}_${name}`;
this.run(interpreter, `del ${id};del ${name}`);
delete globalThis[id];
},
setGlobal,
deleteGlobal,
run,
runAsync,
writeFile,

View File

@@ -1,5 +1,11 @@
import { fetchPaths, stdio } from "./_utils.js";
import { run, runAsync, writeFile } from "./_python.js";
import {
run,
runAsync,
setGlobal,
deleteGlobal,
writeFile,
} from "./_python.js";
const type = "pyodide";
@@ -24,12 +30,8 @@ export default {
}
return interpreter;
},
setGlobal(interpreter, name, value) {
interpreter.globals.set(name, value);
},
deleteGlobal(interpreter, name) {
interpreter.globals.delete(name);
},
setGlobal,
deleteGlobal,
run,
runAsync,
writeFile,

View File

@@ -12,7 +12,7 @@ const getRoot = (script) => {
return parent;
};
const queryTarget = (script, idOrSelector) => {
export const queryTarget = (script, idOrSelector) => {
const root = getRoot(script);
return root.getElementById(idOrSelector) || $(idOrSelector, root);
};

View File

@@ -4,7 +4,8 @@
// Please check via `npm run size` that worker code is not much
// bigger than it used to be before any changes is applied to this file.
import coincident from "coincident/structured";
import * as JSON from "@ungap/structured-clone/json";
import coincident from "coincident/window";
import { create } from "../utils.js";
import { registry } from "../interpreters.js";
@@ -37,9 +38,15 @@ const add = (type, fn) => {
);
};
const { proxy: sync, window, isWindowProxy } = coincident(self, JSON);
const xworker = {
// allows synchronous utilities between this worker and the main thread
sync: coincident(self),
sync,
// allow access to the main thread world
window,
// allow introspection for foreign (main thread) refrences
isWindowProxy,
// standard worker related events / features
onerror() {},
onmessage() {},

View File

@@ -1,8 +1,8 @@
import coincident from "coincident/structured";
import * as JSON from "@ungap/structured-clone/json";
import coincident from "coincident/window";
import xworker from "./xworker.js";
import { assign, defineProperties, absoluteURL } from "../utils.js";
import { getText } from "../fetch-utils.js";
import workerHooks from "./hooks.js";
/**
* @typedef {Object} WorkerOptions custom configuration
@@ -19,9 +19,9 @@ export default (...args) =>
* @returns {Worker}
*/
function XWorker(url, options) {
const hooks = workerHooks.get(XWorker);
const worker = xworker();
const { postMessage } = worker;
const hooks = this instanceof XWorker ? void 0 : this;
if (args.length) {
const [type, version] = args;
options = assign({}, options || { type, version });
@@ -39,7 +39,7 @@ export default (...args) =>
),
},
sync: {
value: coincident(worker),
value: coincident(worker, JSON).proxy,
},
});
};

View File

@@ -1 +0,0 @@
export default new WeakMap();

View File

@@ -2,7 +2,7 @@
"imports": {
"http://pyodide": "./test/mocked/pyodide.mjs",
"https://cdn.jsdelivr.net/pyodide/v0.23.2/full/pyodide.mjs": "./test/mocked/pyodide.mjs",
"https://cdn.jsdelivr.net/npm/@micropython/micropython-webassembly-pyscript@1.20.0-239/micropython.mjs": "./test/mocked/micropython.mjs",
"https://cdn.jsdelivr.net/npm/@micropython/micropython-webassembly-pyscript@1.20.0-253/micropython.mjs": "./test/mocked/micropython.mjs",
"https://cdn.jsdelivr.net/npm/basic-toml@0.3.1/es.js": "./test/mocked/toml.mjs"
}
}

View File

@@ -9,9 +9,10 @@
"version": "0.0.0",
"license": "ISC",
"dependencies": {
"@ungap/structured-clone": "^1.2.0",
"@ungap/with-resolvers": "^0.1.0",
"basic-devtools": "^0.1.6",
"coincident": "^0.4.1"
"coincident": "^0.7.2"
},
"devDependencies": {
"@node-loader/import-maps": "^1.1.0",
@@ -19,10 +20,10 @@
"@rollup/plugin-terser": "^0.4.3",
"ascjs": "^5.0.1",
"c8": "^8.0.0",
"eslint": "^8.42.0",
"eslint": "^8.43.0",
"linkedom": "^0.14.26",
"rollup": "^3.25.1",
"static-handler": "^0.4.1",
"static-handler": "^0.4.2",
"typescript": "^5.1.3"
}
},
@@ -92,9 +93,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz",
"integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz",
"integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -348,9 +349,9 @@
"integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw=="
},
"node_modules/acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
"integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -534,9 +535,9 @@
}
},
"node_modules/coincident": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/coincident/-/coincident-0.4.1.tgz",
"integrity": "sha512-CiHZVx/eUano0G+sSc0S0pXTBH51q0W7pMw1ScRa57BgfBw8l19pMW2Z6sVDHjOih5c9NYrP0budO8SslJv2sA==",
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/coincident/-/coincident-0.7.2.tgz",
"integrity": "sha512-/hNcCDfousAW04kxOeuwzrM+gIQZpFDCYKXvDex/DJxvIcDL57WM32zGO9t8xT34cpBJUl0GNEclpaOx7/rk4A==",
"dependencies": {
"@ungap/structured-clone": "^1.2.0"
}
@@ -764,15 +765,15 @@
}
},
"node_modules/eslint": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz",
"integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz",
"integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.3",
"@eslint/js": "8.42.0",
"@eslint/js": "8.43.0",
"@humanwhocodes/config-array": "^0.11.10",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -1796,12 +1797,12 @@
}
},
"node_modules/static-handler": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/static-handler/-/static-handler-0.4.1.tgz",
"integrity": "sha512-atl+UofbqCECS4Ag1ZZIjC606I1I40CuDsdq4YQ5Vq3vTsUbvqVaNG4evFsKktlnPcEm217vBUSEFKyi9lsJ8Q==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/static-handler/-/static-handler-0.4.2.tgz",
"integrity": "sha512-Szbk521mneb5npxg3SEyoufsHr2osAzxMy71W2zFCzLB8wLhHYvKUDCMkLY6imi+fIqkpfas3rzXGBQf99aeEA==",
"dev": true,
"dependencies": {
"mime-types": "^2.1.34"
"mime-types": "^2.1.35"
},
"bin": {
"static-handler": "static-handler.cjs"
@@ -1873,9 +1874,9 @@
}
},
"node_modules/terser": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.18.0.tgz",
"integrity": "sha512-pdL757Ig5a0I+owA42l6tIuEycRuM7FPY4n62h44mRLRfnOxJkkOHd6i89dOpwZlpF6JXBwaAHF6yWzFrt+QyA==",
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.18.1.tgz",
"integrity": "sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@@ -2118,9 +2119,9 @@
}
},
"@eslint/js": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz",
"integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz",
"integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
"dev": true
},
"@humanwhocodes/config-array": {
@@ -2306,9 +2307,9 @@
"integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw=="
},
"acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
"integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
"dev": true
},
"acorn-jsx": {
@@ -2447,9 +2448,9 @@
}
},
"coincident": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/coincident/-/coincident-0.4.1.tgz",
"integrity": "sha512-CiHZVx/eUano0G+sSc0S0pXTBH51q0W7pMw1ScRa57BgfBw8l19pMW2Z6sVDHjOih5c9NYrP0budO8SslJv2sA==",
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/coincident/-/coincident-0.7.2.tgz",
"integrity": "sha512-/hNcCDfousAW04kxOeuwzrM+gIQZpFDCYKXvDex/DJxvIcDL57WM32zGO9t8xT34cpBJUl0GNEclpaOx7/rk4A==",
"requires": {
"@ungap/structured-clone": "^1.2.0"
}
@@ -2615,15 +2616,15 @@
"dev": true
},
"eslint": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz",
"integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz",
"integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.3",
"@eslint/js": "8.42.0",
"@eslint/js": "8.43.0",
"@humanwhocodes/config-array": "^0.11.10",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -3363,12 +3364,12 @@
}
},
"static-handler": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/static-handler/-/static-handler-0.4.1.tgz",
"integrity": "sha512-atl+UofbqCECS4Ag1ZZIjC606I1I40CuDsdq4YQ5Vq3vTsUbvqVaNG4evFsKktlnPcEm217vBUSEFKyi9lsJ8Q==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/static-handler/-/static-handler-0.4.2.tgz",
"integrity": "sha512-Szbk521mneb5npxg3SEyoufsHr2osAzxMy71W2zFCzLB8wLhHYvKUDCMkLY6imi+fIqkpfas3rzXGBQf99aeEA==",
"dev": true,
"requires": {
"mime-types": "^2.1.34"
"mime-types": "^2.1.35"
}
},
"string-width": {
@@ -3413,9 +3414,9 @@
"dev": true
},
"terser": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.18.0.tgz",
"integrity": "sha512-pdL757Ig5a0I+owA42l6tIuEycRuM7FPY4n62h44mRLRfnOxJkkOHd6i89dOpwZlpF6JXBwaAHF6yWzFrt+QyA==",
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.18.1.tgz",
"integrity": "sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==",
"dev": true,
"requires": {
"@jridgewell/source-map": "^0.3.3",

View File

@@ -1,20 +1,21 @@
{
"name": "@pyscript/core",
"version": "0.0.0",
"version": "0.0.1",
"description": "",
"main": "./cjs/index.js",
"types": "./types/index.d.ts",
"scripts": {
"server": "npx static-handler --cors --coep --coop --corp .",
"build": "npm run rollup:xworker && npm run rollup:min && eslint esm/ && npm run ts && npm run cjs && npm run test",
"build": "npm run rollup:xworker && npm run rollup:core && npm run rollup:pyscript && eslint esm/ && npm run ts && npm run cjs && npm run test",
"cjs": "ascjs --no-default esm cjs",
"rollup:min": "rollup --config rollup/min.config.js",
"rollup:core": "rollup --config rollup/core.config.js",
"rollup:pyscript": "rollup --config rollup/pyscript.config.js",
"rollup:xworker": "rollup --config rollup/xworker.config.js",
"test": "c8 --100 node --experimental-loader @node-loader/import-maps test/index.js ",
"test:html": "npm run test && c8 report -r html",
"coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info",
"size": "npm run size:module && npm run size:worker",
"size:module": "echo module is $(cat min.js | brotli | wc -c) bytes once compressed",
"size:module": "echo module is $(cat core.js | brotli | wc -c) bytes once compressed",
"size:worker": "echo worker is $(cat esm/worker/xworker.js | brotli | wc -c) bytes once compressed",
"ts": "tsc -p ."
},
@@ -27,10 +28,10 @@
"@rollup/plugin-terser": "^0.4.3",
"ascjs": "^5.0.1",
"c8": "^8.0.0",
"eslint": "^8.42.0",
"eslint": "^8.43.0",
"linkedom": "^0.14.26",
"rollup": "^3.25.1",
"static-handler": "^0.4.1",
"static-handler": "^0.4.2",
"typescript": "^5.1.3"
},
"module": "./esm/index.js",
@@ -43,13 +44,14 @@
},
"./package.json": "./package.json"
},
"unpkg": "min.js",
"unpkg": "core.js",
"dependencies": {
"@ungap/structured-clone": "^1.2.0",
"@ungap/with-resolvers": "^0.1.0",
"basic-devtools": "^0.1.6",
"coincident": "^0.4.1"
"coincident": "^0.7.2"
},
"worker": {
"blob": "sha256-obDch1OaxOZKhaCpSW9vEAHJk01N+9kYvSiGkNmeJRU="
"blob": "sha256-llcU6BJSNpU+9XN3CXzTrR1Js0KQaXygnnKiD039CTQ="
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
// This file generates /min.js minified version of the module, which is
// This file generates /core.js minified version of the module, which is
// the default exported as npm entry.
import { nodeResolve } from "@rollup/plugin-node-resolve";
@@ -13,6 +13,6 @@ export default {
plugins: process.env.NO_MIN ? [nodeResolve()] : [nodeResolve(), terser()],
output: {
esModule: true,
file: "./min.js",
file: "./core.js",
},
};

View File

@@ -0,0 +1,18 @@
// This file generates /core.js minified version of the module, which is
// the default exported as npm entry.
import { nodeResolve } from "@rollup/plugin-node-resolve";
import terser from "@rollup/plugin-terser";
import { createRequire } from "node:module";
createRequire(import.meta.url)("./build_xworker.cjs");
export default {
input: "./esm/custom/pyscript.js",
plugins: process.env.NO_MIN ? [nodeResolve()] : [nodeResolve(), terser()],
output: {
esModule: true,
file: "./pyscript.js",
},
};

View File

@@ -1,4 +1,4 @@
// This file generates /min.js minified version of the module, which is
// This file generates /core.js minified version of the module, which is
// the default exported as npm entry.
import { nodeResolve } from "@rollup/plugin-node-resolve";

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="pyodide" async>

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="micropython">

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="micropython" config="./fetch.toml">

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="pyodide">

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="pyodide">

View File

@@ -6,7 +6,7 @@
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="pyodide" version="0.23.2">

View File

@@ -6,7 +6,7 @@
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="pyodide" config="./config.toml">

View File

@@ -6,7 +6,7 @@
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="pyodide" config="./config.json">

View File

@@ -46,6 +46,26 @@ buf = io.BytesIO()
plt.savefig(buf, format="png")
buf.seek(0)
js.xworker.postMessage(
"data:image/png;base64," + base64.b64encode(buf.read()).decode("UTF-8")
)
# how it was (including main thread counter part)
# js.xworker.postMessage(
# "data:image/png;base64," + base64.b64encode(buf.read()).decode("UTF-8")
# )
# how it is now via structured coincident/window
document = xworker.window.document
img = document.createElement("img")
img.style.transform = "scale(.5)"
img.src = "data:image/png;base64," + base64.b64encode(buf.read()).decode("UTF-8")
# document.body.append(img) fails for some reason I don't understand
# same would be for document.querySelector("#image").append(img)
# those would work in any JS counterpart though ... but for demo sake:
document.querySelector("#image").innerHTML = img.outerHTML
# about pyodide issue
print(xworker.window.Array(1, 2)) # this works
xworker.window.console.log(1, 2) # this works too
# xworker.window.console.log(xworker.window.Array(1, 2)) # this doesn't
# xworker.window.console.log([1, 2]) # also this doesn't
# xworker.window.console.log(to_js({})) # this doesn't neither

View File

@@ -6,19 +6,23 @@
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="micropython">
def show_image(event):
from js import document
img = document.createElement("img")
img.style.transform = "scale(.5)"
img.src = event.data
document.body.appendChild(img)
w = XWorker('./matplot.py', **{'type': 'pyodide', 'config': './config.toml'})
w.onmessage = show_image
# xworker.window made the following completely unnecessary
# def show_image(event):
# from js import document
# img = document.createElement("img")
# img.style.transform = "scale(.5)"
# img.src = event.data
# document.querySelector("#image").append(img)
# w.onmessage = show_image
</script>
<div id="image"></div>
</body>
</html>

View File

@@ -6,7 +6,7 @@
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="micropython">

View File

@@ -6,4 +6,12 @@ export const loadMicroPython = () => ({
python.target = document.currentScript.target;
}
},
globals: {
set(name, value) {
globalThis[name] = value;
},
delete(name) {
delete globalThis[name];
},
},
});

View File

@@ -6,7 +6,7 @@
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="pyodide">

View File

@@ -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"
}
}
</script>

View File

@@ -10,7 +10,7 @@
}
</style>
<script type="importmap">
{ "imports": { "@pyscript/core": "../../min.js" } }
{ "imports": { "@pyscript/core": "../../core.js" } }
</script>
<script type="module">
import { define, whenDefined } from "@pyscript/core";

View File

@@ -10,7 +10,7 @@
}
</style>
<script type="importmap">
{ "imports": { "@pyscript/core": "../../min.js" } }
{ "imports": { "@pyscript/core": "../../core.js" } }
</script>
<script type="module">
import { define } from "@pyscript/core";

View File

@@ -3,24 +3,30 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>PyScript</title>
<style>
py-script {
display: none;
}
</style>
<script type="importmap">
{ "imports": { "@pyscript/core": "../../min.js" } }
<title>PyScript Next</title>
<script type="importmap">{ "imports": { "@pyscript/element": "../../pyscript.js" } }</script>
<script type="module">
// how would PyScript plugins add their own behavior?
import { hooks } from "@pyscript/element";
let counter = 0;
hooks.onBeforeRun.add((pyodide, { localName }) => {
// console.log(++counter, 'elements so far', localName, pyodide);
});
</script>
<script type="module" src="py-script.js"></script>
</head>
<body>
<fieldset>
<legend id="dary"></legend>
Something something about something ...
</fieldset>
<py-config>
[[fetch]]
from = "../"
to_folder = "./"
files = ["a.py", "b.py"]
</py-config>
<!-- <py-script next> -->
<py-script>
import js
import a, b
@@ -29,8 +35,24 @@
'Hello Web!'
</py-script>
<py-script>
# note the target is this element itself
display('second &lt;py-script&gt;')
</py-script>
<py-script>
# note this is late to the party simply because
# pyodide needs to be bootstrapped in the Worker too
XWorker('../a.py')
'OK'
</py-script>
<!-- <script type="py"> -->
<script type="py">
# not the target is inferred as companion element
display('first <script type="py">')
</script>
<script type="py">
# note the target here is different
display('second <script type="py">', target="dary")
</script>
</body>
</html>

View File

@@ -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<PyScriptElement>} */
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);

View File

@@ -6,7 +6,7 @@
<title>python events</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script type="pyodide">

View File

@@ -6,7 +6,7 @@
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<script

View File

@@ -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"
}
}
</script>

View File

@@ -10,7 +10,7 @@
<link rel="manifest" href="manifest.json" />
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
<script type="module">
customElements.define(
"shadow-dom",

View File

@@ -14,7 +14,7 @@
}
</style>
<link rel="stylesheet" href="style.css" />
<script type="module" src="../min.js"></script>
<script type="module" src="../core.js"></script>
</head>
<body>
<table cellspacing="2" cellpadding="2">

View File

@@ -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"
}
}
</script>

View File

@@ -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"
}
}
</script>

View File

@@ -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"
}
}

View File

@@ -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"
}
}