WIP: Bringing PyScript.next PoC to the main project (#1507)

* kill unwrapped_remote (#1490)

* kill unwrapped_remote

* linting

* don't use callKwargs for python plugins

* fix tests and improve types

* Bringing PyScript.next PoC to the main project

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Madhur Tandon <20173739+madhur-tandon@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Andrea Giammarchi
2023-06-05 21:52:28 +02:00
committed by GitHub
parent 4467898473
commit 339e40063a
101 changed files with 49520 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"env": {
"browser": true,
"es2022": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {}
}

View File

@@ -0,0 +1,30 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm run build --if-present
- run: npm test
- run: npm run coverage --if-present
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

9
pyscript.core/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.DS_Store
.nyc_output
*.log
coverage/
node_modules/
cjs/
!cjs/package.json
min.js
esm/worker/xworker.js

19
pyscript.core/.npmignore Normal file
View File

@@ -0,0 +1,19 @@
.DS_Store
.nyc_output
.eslintrc.json
.github/
.travis.yml
.eslintrc.json
*.log
coverage/
micropython/
node_modules/
pyscript/
rollup/
test/
index.html
node.importmap
sw.js
tsconfig.json
cjs/worker/_template.js
esm/worker/_template.js

1
pyscript.core/.npmrc Normal file
View File

@@ -0,0 +1 @@
package-lock=true

120
pyscript.core/README.md Normal file
View File

@@ -0,0 +1,120 @@
# pyscript
[![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)
---
## API
Please check the PR around current API and help me out changing or improving it, thank you!
https://github.com/WebReflection/python/blob/c50ab771ffb14d2fbb499219d67200cf02e0cd5f/API.md
---
### Development
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).
Please be sure the following line is prsent in your `.git/hooks/pre-commit` file to always test and check coverage before committing.
```sh
exec npx c8 --100 npm test > /dev/null
```
---
The goal of this repository is to explore a few _PyScript_ related topics, such as:
- **bootstrap time**: do we need to do everything we currently do by default?
- **ESM / npm module**: why are we still attaching things to the global?
- **dependencies**: do we need to include a whole _TOML_ v0.4 parser by default?
- **DX / UX**: what if we simplify most pain points we already know while solving all problems we also will or already have moving forward?
- **YAGNI**: thoughts about current features that look more like a workaround
Following each topic is discussed with findings, ideas, and proof of concepts.
## Bootstrap Time
Once bundled, the current PoC weigths **2.7K** (~1K compressed) as opposite of current _pyscript.js_ which is **1.2MB** (~230K compressed).
By no mean the current PoC can replace all _pyscript.js_ features, but I believe a smaller and better architected core could help creating even more projects around our offer.
**Notable findings**
- even if no `<py-config>` is defined, we include a whole TOML library and we import _micropip_, _pyparsing_ and _pypackaging_ for an "_Hello World_"
- there's a lot of code which only goal is to handle _HTML_ issues with parsed content and entities and _IDs_. This PoC never uses, neither needs, _IDs_ at all and it makes any target avilable within the Python code instead of needing to have discoverable _IDs_ (see recent MR around _IDs_ in Shadow DOM too) ... should we re-consider the need for _IDs_ and drop all the code around this topic in the (hopefully) near future?
- there's no need to normalize any _python_ output with the usage of `<script>` because if the indentation is not broken the interpreter works just fine (this is extra code we could just remove from our codebase)
- despite our possible improvements around previous point, the "_elephant in the room_" is _pyodide_ initialization time. We can save bandwidth and some millisencods but the main thread is blocked for ~1.5 seconds when _pyodide_ kicks in. Is there anything we can do to at least move its initialization elsewhere? (e.g. _Worker_)
- adding a _cache all_ strategy for an extremely simple _Service Worker_ made bootstrap time predictable, either via this PoC or via _pyscript_. Is there any particular reason we are not using a _Service Worker_ everywhere we offer or demo _pyscript_?
- once initialized, both _pyodide_ and _micropython_ expose the version ... why do we attach a version before loading these runtimes?
## As ESM / `npm` module
Both _pyscript_, current _micropython_ file, and (IIRC) _pyodide_ leak something on the _global_ context. Not only this is generally considered a bad practice but it also plays badly with the idea that multiple interpreters _could_ coexist in the same page (already possible with this PoC).
Not being a module also means we don't get to benefit from easy install and all _CDNs_ that for free would allow any developer to use our offer in the wild, including local projects bootstrapped via all the usual/common Web related tools.
What would it take to actually use _pyscript_ as a module or why are we still using these fairly outdated and problematic/conflicting practices instead?
## Dependencies
We all agree that _TOML_ is likely the preferred choice for Python users to setup the environment but it's not clear why we need to embed a 3rd party fork that parses the whole _TOML_ standard within our code out of the box, or why we do initialize a package manager when it's not even needed, asking the core to download extra stuff by default even for cases such stuff was not desired nor required.
Accordingly, if there's no particular reason I am not aware about, should we include via dynamic `import` the _TOML_ parser **and** botstrap `micropip` and the rest only if the config exists and not by default?
If it has to be there by default, why isn't the package manager already embedded within _pyodide_ or _micropython_ (`mip`) and initialized internally?
Last, but not least, as our config consist of 3 or 4 fields and nothing else, do we really need a whole TOML 3rd party parer instead of a super-simple one that just gets the job done, in case we want to embed that in core? Reading _config_'s specs it feels like we're slightly overdoing it in there and we also have issues with the config order (which btw could also be the case for JSON configurations).
## DX / UX
We currently have (imho) some hard to explain limitation around _pyscript_, most notably:
- we have a `<py-config>` custom element where only a single one can be used and everything else is ignored. The reasons for this (I am guessing) are:
- we have a single interpreter at the time ... but how would this scale when multiple interpreteres are wanted/desired?
- there's no relation between a `<py-config>` and a `<py-script>` element ... even their definition order on the page doesn't matter so that if multiple `<py-script>` on a page have as last node a `<py-config>` they all follow that rule, making it impossible to grant either runtime or config per each `<py-script>`. Aren't we trapped by this decoupling of components that are, in fact, strongly coupled and constrained by such _implicit_ coupling? What's the plan forward here?
- in all our live examples but 2 all we need is *a single `<py-script>` tag and, occasionally, a single `<py-config>` tag and I wonder if this is effectily the main use-case to address, while the need for multiple `<py-script>` that all share same config and environment look like needed only to display results in different places of the page, something easy to address via IDs when elements are well known or via the `js.document.querySelector(...)` API we expose through *pyodide* and *micropython*. Shouldn't we instead find a better way to relate same env when more `<py-script>` are meant, so that we bootstrap only one *main* env and we refer to such *env\* via attributes?
- there's no way to display Python results within `<py-script>` custom element if this is within unexpected places: tables, trs, tds, options, sources, and what's not, current PoC offers an escape hatch to assign the target at runtime without any of the caveats we have with custom elements
- we recently introduced `py-*` events for any node but that implicitly blocks multiple runtimes per page ... should we find a compromise/solution to this, since the `env` attribute could be used as `py-env` target too?
The current PoC couple _env_ and _config_ through a single component, isolating every script, runtime, and environment from each other.
Not only that, multiple _python scripts_ can share the same configuration whenever that's desired and in multiple pages or different parts of the page but these don't necessarily need to share the same environment.
The current PoC indeed allows multiple config and multiple interpreters (_pyodide_ or _micropython_) per page, downloading each only once but initializing these per each _script_ when desired.
A unique identifier, that is not an ID, could also relate each script to the same env as it's done now (shared _pyodide_ runtime across all _py-script_ tags) but that'd be an extra feature, not the default, and it can be orchestrated explicitly, example
```html
<script type="py" config="shared-demo.toml">
from js import document
# target here is the related current script node
document.currentScript.target.textContent = 'a'
</script>
<hr />
<script type="py" env="shared-env">
# this env shares nothing with the previous/default one
from js import document
# target here is the related current script node
# which is different from the previous one
document.currentScript.target.textContent = 'b'
</script>
<hr />
<script type="py" env="shared-env">
# this env shares globals only with the previous script
from js import document
# target here is the related current script node
# which is also again different from the previous one
document.currentScript.target.textContent = 'c'
</script>
```
I (personally) believe that optionally coupling _env_, _config_, and _runtime_ to each single script provides the base to distribute _pyscript_ components in the wild and without ever causing issues, so in few words this is not only a more robust and clean approach to what we have already, where two 3rd party _pyscript_ components can't coexist within the same page because of the points previously mentioned, but it's way more expicit than having multiple `<py-config>` potentially ignored by the page because one was already present / parsed / used within another component that landed before.
## YAGNI
This is a super-personal take around current _pyscript_ state of affairs:
- the splash screen reminds me good'ol days with Flash Player ... ideally we should have a fast boostrap that makes such practice irrelevant
- the `interpreters` in the config, when only a config can run, is weird to say the least ... it's theoretically an array of details, with names and stuff, and I think nobody cares about it ... the current `runtime` attribute looks much easier to explain and reason about, plus, as previously mentioned, is confined within the _script_ that is running current python code
- the whole TOML parser for our explicit use case looks like replace-able with a single RegExp or a split line loop (see https://github.com/WebReflection/basic-toml#readme)
- plugins should never be embedded out of the box ... we might want to provide a list of plugins via a `plugins` attribute and handle these ourselves but I think that having a `@pyscript/core` module any plugin can use to register itself would be better at scale and architecture

View File

@@ -0,0 +1,8 @@
/** @param {Response} response */
export const getBuffer = (response) => response.arrayBuffer();
/** @param {Response} response */
export const getJSON = (response) => response.json();
/** @param {Response} response */
export const getText = (response) => response.text();

109
pyscript.core/esm/index.js Normal file
View File

@@ -0,0 +1,109 @@
import { $x, $$ } from "basic-devtools";
import xworker from "./worker/class.js";
import { handle, runtimes } from "./script-handler.js";
import { all, assign, create, defineProperty } from "./utils.js";
import { registry, selectors, prefixes } from "./runtimes.js";
import { PLUGINS_SELECTORS, handlePlugin } from "./plugins.js";
export { registerPlugin } from "./plugins.js";
export const XWorker = xworker();
const RUNTIME_SELECTOR = selectors.join(",");
// ensure both runtime and its queue are awaited then returns the runtime
const awaitRuntime = async (key) => {
const { runtime, queue } = runtimes.get(key);
return (await all([runtime, queue]))[0];
};
defineProperty(globalThis, "pyscript", {
value: {
env: new Proxy(create(null), { get: (_, name) => awaitRuntime(name) }),
},
});
let index = 0;
globalThis.__events = new Map();
// attributes are tested via integration / e2e
/* c8 ignore next 17 */
const listener = async (event) => {
const { type, currentTarget } = event;
for (let { name, value, ownerElement: el } of $x(
`./@*[${prefixes.map((p) => `name()="${p}${type}"`).join(" or ")}]`,
currentTarget,
)) {
name = name.slice(0, -(type.length + 1));
const runtime = await awaitRuntime(
el.getAttribute(`${name}-env`) || name,
);
const i = index++;
try {
globalThis.__events.set(i, event);
registry.get(name).runEvent(runtime, value, i);
} finally {
globalThis.__events.delete(i);
}
}
};
// attributes are tested via integration / e2e
/* c8 ignore next 8 */
for (let { name, ownerElement: el } of $x(
`.//@*[${prefixes.map((p) => `starts-with(name(),"${p}")`).join(" or ")}]`,
)) {
name = name.slice(name.indexOf("-") + 1);
if (name !== "env") el.addEventListener(name, listener);
}
const mo = new MutationObserver((records) => {
for (const { type, target, attributeName, addedNodes } of records) {
// attributes are tested via integration / e2e
/* c8 ignore next 17 */
if (type === "attributes") {
const i = attributeName.indexOf("-") + 1;
if (i) {
const prefix = attributeName.slice(0, i);
for (const p of prefixes) {
if (prefix === p) {
const type = attributeName.slice(i);
if (type !== "env") {
const method = target.hasAttribute(attributeName)
? "add"
: "remove";
target[`${method}EventListener`](type, listener);
}
break;
}
}
}
continue;
}
for (const node of addedNodes) {
if (node.nodeType === 1) {
if (node.matches(RUNTIME_SELECTOR)) handle(node);
else {
$$(RUNTIME_SELECTOR, node).forEach(handle);
if (!PLUGINS_SELECTORS.length) continue;
handlePlugin(node);
$$(PLUGINS_SELECTORS.join(","), node).forEach(handlePlugin);
}
}
}
}
});
const observe = (root) => {
mo.observe(root, { childList: true, subtree: true, attributes: true });
return root;
};
const { attachShadow } = Element.prototype;
assign(Element.prototype, {
attachShadow(init) {
return observe(attachShadow.call(this, init));
},
});
$$(RUNTIME_SELECTOR, observe(document)).forEach(handle);

View File

@@ -0,0 +1,39 @@
import { runtime } from "./runtimes.js";
import { absoluteURL, resolve } from "./utils.js";
import { parse } from "./toml.js";
import { getJSON, getText } from "./fetch-utils.js";
/**
* @param {string} id the runtime name @ version identifier
* @param {string} [config] optional config file to parse
* @returns
*/
export const getRuntime = (id, config) => {
let options = {};
if (config) {
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
if (config.endsWith(".json")) options = fetch(config).then(getJSON);
else if (config.endsWith(".toml"))
options = fetch(config).then(getText).then(parse);
else {
try {
options = JSON.parse(config);
} catch (_) {
options = parse(config);
}
// make the config a URL to be able to retrieve relative paths from it
config = absoluteURL("./config.txt");
}
/* c8 ignore stop */
}
return resolve(options).then((options) => runtime[id](options, config));
};
/**
* @param {string} type the runtime type
* @param {string} [version] the optional runtime version
* @returns
*/
export const getRuntimeID = (type, version = "") =>
`${type}@${version}`.replace(/@$/, "");

View File

@@ -0,0 +1,85 @@
import { $$ } from "basic-devtools";
import { getDetails } from "./script-handler.js";
import { registry, configs } from "./runtimes.js";
import { getRuntimeID } from "./loader.js";
import { io } from "./runtime/_utils.js";
export const PLUGINS_SELECTORS = [];
/**
* @typedef {Object} Runtime plugin configuration
* @prop {string} type the runtime type
* @prop {object} runtime the bootstrapped runtime
* @prop {(url:string, options?: object) => Worker} XWorker an XWorker constructor that defaults to same runtime on the Worker.
* @prop {object} config a cloned config used to bootstrap the runtime
* @prop {(code:string) => any} run an utility to run code within the runtime
* @prop {(code:string) => Promise<any>} runAsync an utility to run code asynchronously within the runtime
* @prop {(path:string, data:ArrayBuffer) => void} writeFile an utility to write a file in the virtual FS, if available
*/
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
/**
* @param {Element} node any DOM element registered via plugin.
*/
export const handlePlugin = (node) => {
for (const name of PLUGINS_SELECTORS) {
if (node.matches(name)) {
const { options, known } = plugins.get(name);
if (!known.has(node)) {
known.add(node);
const { type, version, config, env, onRuntimeReady } = options;
const name = getRuntimeID(type, version);
const id = env || `${name}${config ? `|${config}` : ""}`;
const { runtime: engine, XWorker } = getDetails(
type,
id,
name,
version,
config,
);
engine.then((runtime) => {
const module = registry.get(type);
onRuntimeReady(node, {
type,
runtime,
XWorker,
io: io.get(runtime),
config: structuredClone(configs.get(name)),
run: module.run.bind(module, runtime),
runAsync: module.runAsync.bind(module, runtime),
});
});
}
}
}
};
/**
* @type {Map<string, {options:object, known:WeakSet<Element>}>}
*/
const plugins = new Map();
/**
* @typedef {Object} PluginOptions plugin configuration
* @prop {string} type the runtime/interpreter type to receive
* @prop {string} [version] the optional runtime version to use
* @prop {string} [config] the optional config to use within such runtime
* @prop {string} [env] the optional environment to use
* @prop {(node: Element, runtime: Runtime) => void} onRuntimeReady the callback that will be invoked once
*/
/**
* Allows plugins and components on the page to receive runtimes to execute any code.
* @param {string} name the unique plugin name
* @param {PluginOptions} options the plugin configuration
*/
export const registerPlugin = (name, options) => {
if (PLUGINS_SELECTORS.includes(name))
throw new Error(`plugin ${name} already registered`);
PLUGINS_SELECTORS.push(name);
plugins.set(name, { options, known: new WeakSet() });
$$(name).forEach(handlePlugin);
};
/* c8 ignore stop */

View File

@@ -0,0 +1,123 @@
import { getBuffer } from "../fetch-utils.js";
import { absoluteURL } from "../utils.js";
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
export const io = new WeakMap();
export const stdio = (init) => {
const context = init || console;
const localIO = {
stderr: (context.stderr || console.error).bind(context),
stdout: (context.stdout || console.log).bind(context),
};
return {
stderr: (...args) => localIO.stderr(...args),
stdout: (...args) => localIO.stdout(...args),
async get(engine) {
const runtime = await engine;
io.set(runtime, localIO);
return runtime;
},
};
};
/* c8 ignore stop */
// This should be the only helper needed for all Emscripten based FS exports
export const writeFile = (FS, path, buffer) => {
const { parentPath, name } = FS.analyzePath(path, true);
FS.mkdirTree(parentPath);
return FS.writeFile([parentPath, name].join("/"), new Uint8Array(buffer), {
canOwn: true,
});
};
// This is instead a fallback for Lua or others
export const writeFileShim = (FS, path, buffer) => {
path = resolve(FS, path);
mkdirTree(FS, dirname(path));
return FS.writeFile(path, new Uint8Array(buffer), { canOwn: true });
};
const dirname = (path) => {
const tree = path.split("/");
tree.pop();
return tree.join("/");
};
const mkdirTree = (FS, path) => {
const current = [];
for (const branch of path.split("/")) {
current.push(branch);
if (branch) FS.mkdir(current.join("/"));
}
};
const resolve = (FS, path) => {
const tree = [];
for (const branch of path.split("/")) {
switch (branch) {
case "":
break;
case ".":
break;
case "..":
tree.pop();
break;
default:
tree.push(branch);
}
}
return [FS.cwd()].concat(tree).join("/").replace(/^\/+/, "/");
};
import { all, isArray } from "../utils.js";
const calculateFetchPaths = (config_fetch) => {
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
for (const { files, to_file, from = "" } of config_fetch) {
if (files !== undefined && to_file !== undefined)
throw new Error(
`Cannot use 'to_file' and 'files' parameters together!`,
);
if (files === undefined && to_file === undefined && from.endsWith("/"))
throw new Error(
`Couldn't determine the filename from the path ${from}, please supply 'to_file' parameter.`,
);
}
/* c8 ignore stop */
return config_fetch.flatMap(
({ from = "", to_folder = ".", to_file, files }) => {
if (isArray(files))
return files.map((file) => ({
url: joinPaths([from, file]),
path: joinPaths([to_folder, file]),
}));
const filename = to_file || from.slice(1 + from.lastIndexOf("/"));
return [{ url: from, path: joinPaths([to_folder, filename]) }];
},
);
};
const joinPaths = (parts) => {
const res = parts
.map((part) => part.trim().replace(/(^[/]*|[/]*$)/g, ""))
.filter((p) => p !== "" && p !== ".")
.join("/");
return parts[0].startsWith("/") ? `/${res}` : res;
};
const fetchResolved = (config_fetch, url) =>
fetch(absoluteURL(url, base.get(config_fetch)));
export const base = new WeakMap();
export const fetchPaths = (module, runtime, config_fetch) =>
all(
calculateFetchPaths(config_fetch).map(({ url, path }) =>
fetchResolved(config_fetch, url)
.then(getBuffer)
.then((buffer) => module.writeFile(runtime, path, buffer)),
),
);

View File

@@ -0,0 +1,35 @@
import { fetchPaths, stdio, writeFile } from "./_utils.js";
const type = "micropython";
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
const worker = (method) =>
function (runtime, code, xworker) {
globalThis.xworker = xworker;
return this[method](runtime, `from js import xworker;${code}`);
};
export default {
type: [type, "mpy"],
module: () => `http://localhost:8080/micropython/micropython.mjs`,
async engine({ loadMicroPython }, config, url) {
const { stderr, stdout, get } = stdio();
url = url.replace(/\.m?js$/, ".wasm");
const runtime = await get(loadMicroPython({ stderr, stdout, url }));
if (config.fetch) await fetchPaths(this, runtime, config.fetch);
return runtime;
},
run: (runtime, code) => runtime.runPython(code),
runAsync: (runtime, code) => runtime.runPythonAsync(code),
runEvent(runtime, code, key) {
return this.run(
runtime,
`import js;event=js.__events.get(${key});${code}`,
);
},
runWorker: worker("run"),
runWorkerAsync: worker("runAsync"),
writeFile: ({ FS }, path, buffer) => writeFile(FS, path, buffer),
};
/* c8 ignore stop */

View File

@@ -0,0 +1,41 @@
import { fetchPaths, stdio, writeFile } from "./_utils.js";
const type = "pyodide";
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
const worker = (method) =>
function (runtime, code, xworker) {
globalThis.xworker = xworker;
return this[method](runtime, `from js import xworker;${code}`);
};
export default {
type: [type, "py"],
module: (version = "0.22.1") =>
`https://cdn.jsdelivr.net/pyodide/v${version}/full/pyodide.mjs`,
async engine({ loadPyodide }, config) {
const { stderr, stdout, get } = stdio();
const runtime = await get(loadPyodide({ stderr, stdout }));
if (config.fetch) await fetchPaths(this, runtime, config.fetch);
if (config.packages) {
await runtime.loadPackage("micropip");
const micropip = await runtime.pyimport("micropip");
await micropip.install(config.packages);
micropip.destroy();
}
return runtime;
},
run: (runtime, code) => runtime.runPython(code),
runAsync: (runtime, code) => runtime.runPythonAsync(code),
runEvent(runtime, code, key) {
return this.run(
runtime,
`import js;event=js.__events.get(${key});${code}`,
);
},
runWorker: worker("run"),
runWorkerAsync: worker("runAsync"),
writeFile: ({ FS }, path, buffer) => writeFile(FS, path, buffer),
};
/* c8 ignore stop */

View File

@@ -0,0 +1,49 @@
import { fetchPaths } from "./_utils.js";
const type = "ruby";
// MISSING:
// * there is no VFS apparently or I couldn't reach any
// * I've no idea how to override the stderr and stdout
// * I've no idea how to import packages
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
const worker = (method) =>
function (runtime, code, xworker) {
globalThis.xworker = xworker;
return this[method](
runtime,
`require "js";xworker=JS::eval("return xworker");${code}`,
);
};
export default {
experimental: true,
type: [type, "rb"],
module: (version = "2.0.0") =>
`https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@${version}/dist/browser.esm.js`,
async engine({ DefaultRubyVM }, config, url) {
const response = await fetch(
`${url.slice(0, url.lastIndexOf("/"))}/ruby.wasm`,
);
const module = await WebAssembly.compile(await response.arrayBuffer());
const { vm: runtime } = await DefaultRubyVM(module);
if (config.fetch) await fetchPaths(this, runtime, config.fetch);
return runtime;
},
run: (runtime, code) => runtime.eval(code),
runAsync: (runtime, code) => runtime.evalAsync(code),
runEvent(runtime, code, key) {
return this.run(
runtime,
`require "js";event=JS::eval("return __events.get(${key})");${code}`,
);
},
runWorker: worker("run"),
runWorkerAsync: worker("runAsync"),
writeFile: () => {
throw new Error(`writeFile is not supported in ${type}`);
},
};
/* c8 ignore stop */

View File

@@ -0,0 +1,45 @@
import { fetchPaths, stdio, writeFileShim } from "./_utils.js";
const type = "wasmoon";
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
const worker = (method) =>
function (runtime, code, xworker) {
runtime.global.set("xworker", xworker);
return this[method](runtime, code);
};
export default {
type: [type, "lua"],
module: (version = "1.15.0") =>
`https://cdn.jsdelivr.net/npm/wasmoon@${version}/+esm`,
async engine({ LuaFactory, LuaLibraries }, config) {
const { stderr, stdout, get } = stdio();
const runtime = await get(new LuaFactory().createEngine());
runtime.global.getTable(LuaLibraries.Base, (index) => {
runtime.global.setField(index, "print", stdout);
runtime.global.setField(index, "printErr", stderr);
});
if (config.fetch) await fetchPaths(this, runtime, config.fetch);
return runtime;
},
run: (runtime, code) => runtime.doStringSync(code),
runAsync: (runtime, code) => runtime.doString(code),
runEvent(runtime, code, key) {
runtime.global.set("event", globalThis.__events.get(key));
return this.run(runtime, code);
},
runWorker: worker("run"),
runWorkerAsync: worker("runAsync"),
writeFile: (
{
cmodule: {
module: { FS },
},
},
path,
buffer,
) => writeFileShim(FS, path, buffer),
};
/* c8 ignore stop */

View File

@@ -0,0 +1,57 @@
// ⚠️ Part of this file is automatically generated
// The :RUNTIMES comment is a delimiter and no code should be written/changed after
// See rollup/build_runtimes.cjs to know more
import { base } from "./runtime/_utils.js";
/** @type {Map<string, object>} */
export const registry = new Map();
/** @type {Map<string, object>} */
export const configs = new Map();
/** @type {string[]} */
export const selectors = [];
/** @type {string[]} */
export const prefixes = [];
export const runtime = new Proxy(new Map(), {
get(map, id) {
if (!map.has(id)) {
const [type, ...rest] = id.split("@");
const runtime = registry.get(type);
const url = /^https?:\/\//i.test(rest)
? rest[0]
: runtime.module(...rest);
map.set(id, {
url,
module: import(url),
engine: runtime.engine.bind(runtime),
});
}
const { url, module, engine } = map.get(id);
return (config, baseURL) =>
module.then((module) => {
configs.set(id, config);
const fetch = config?.fetch;
if (fetch) base.set(fetch, baseURL);
return engine(module, config, url);
});
},
});
const register = (runtime) => {
for (const type of [].concat(runtime.type)) {
registry.set(type, runtime);
selectors.push(`script[type="${type}"]`);
prefixes.push(`${type}-`);
}
};
//:RUNTIMES
import micropython from "./runtime/micropython.js";
import pyodide from "./runtime/pyodide.js";
import ruby from "./runtime/ruby.js";
import wasmoon from "./runtime/wasmoon.js";
for (const runtime of [micropython, pyodide, ruby, wasmoon]) register(runtime);

View File

@@ -0,0 +1,136 @@
import { $ } from "basic-devtools";
import xworker from "./worker/class.js";
import { getRuntime, getRuntimeID } from "./loader.js";
import { registry } from "./runtimes.js";
import { all, resolve, defineProperty, absoluteURL } from "./utils.js";
import { getText } from "./fetch-utils.js";
const getRoot = (script) => {
let parent = script;
while (parent.parentNode) parent = parent.parentNode;
return parent;
};
const queryTarget = (script, idOrSelector) => {
const root = getRoot(script);
return root.getElementById(idOrSelector) || $(idOrSelector, root);
};
const targets = new WeakMap();
const targetDescriptor = {
get() {
let target = targets.get(this);
if (!target) {
target = document.createElement(`${this.type}-script`);
targets.set(this, target);
handle(this);
}
return target;
},
set(target) {
if (typeof target === "string")
targets.set(this, queryTarget(this, target));
else {
targets.set(this, target);
handle(this);
}
},
};
const handled = new WeakMap();
export const runtimes = new Map();
const execute = async (script, source, XWorker, isAsync) => {
const module = registry.get(script.type);
/* c8 ignore next */
if (module.experimental)
console.warn(`The ${script.type} runtime is experimental`);
const [runtime, content] = await all([handled.get(script).runtime, source]);
try {
// temporarily override inherited document.currentScript in a non writable way
// but it deletes it right after to preserve native behavior (as it's sync: no trouble)
defineProperty(globalThis, "XWorker", {
configurable: true,
get: () => XWorker,
});
defineProperty(document, "currentScript", {
configurable: true,
get: () => script,
});
return module[isAsync ? "runAsync" : "run"](runtime, content);
} finally {
delete globalThis.XWorker;
delete document.currentScript;
}
};
const getValue = (ref, prefix) => {
const value = ref?.value;
return value ? prefix + value : "";
};
export const getDetails = (type, id, name, version, config) => {
if (!runtimes.has(id)) {
const details = {
runtime: getRuntime(name, config),
queue: resolve(),
XWorker: xworker(type, version),
};
runtimes.set(id, details);
// enable sane defaults when single runtime *of kind* is used in the page
// this allows `xxx-*` attributes to refer to such runtime without `env` around
if (!runtimes.has(type)) runtimes.set(type, details);
}
return runtimes.get(id);
};
/**
* @param {HTMLScriptElement} script a special type of <script>
*/
export const handle = async (script) => {
// known node, move its companion target after
// vDOM or other use cases where the script is a tracked element
if (handled.has(script)) {
const { target } = script;
if (target) {
// if the script is in the head just append target to the body
if (script.closest("head")) document.body.append(target);
// in any other case preserve the script position
else script.after(target);
}
}
// new script to handle ... allow newly created scripts to work
// just exactly like any other script would
else {
// allow a shared config among scripts, beside runtime,
// and/or source code with different config or runtime
const {
attributes: { async: isAsync, config, env, target, version },
src,
type,
} = script;
const versionValue = version?.value;
const name = getRuntimeID(type, versionValue);
const targetValue = getValue(target, "");
let configValue = getValue(config, "|");
const id = getValue(env, "") || `${name}${configValue}`;
configValue = configValue.slice(1);
if (configValue) configValue = absoluteURL(configValue);
const details = getDetails(type, id, name, versionValue, configValue);
handled.set(
defineProperty(script, "target", targetDescriptor),
details,
);
if (targetValue) targets.set(script, queryTarget(script, targetValue));
// start fetching external resources ASAP
const source = src ? fetch(src).then(getText) : script.textContent;
details.queue = details.queue.then(() =>
execute(script, source, details.XWorker, !!isAsync),
);
}
};

View File

@@ -0,0 +1,8 @@
// lazy TOML parser (fast-toml might be a better alternative)
const TOML_LIB = `https://unpkg.com/basic-toml@0.3.1/es.js`;
/**
* @param {string} text TOML text to parse
* @returns {object} the resulting JS object
*/
export const parse = async (text) => (await import(TOML_LIB)).parse(text);

View File

@@ -0,0 +1,11 @@
const { isArray } = Array;
const { assign, create, defineProperty } = Object;
const { all, resolve } = new Proxy(Promise, {
get: ($, name) => $[name].bind($),
});
const absoluteURL = (path, base = location.href) => new URL(path, base).href;
export { isArray, assign, create, defineProperty, all, resolve, absoluteURL };

View File

@@ -0,0 +1,53 @@
// ⚠️ This file is used to generate xworker.js
// That means if any import is circular or brings in too much
// that would be a higher payload for every worker.
// 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 { registry } from "../runtimes.js";
import { getRuntime, getRuntimeID } from "../loader.js";
let engine, run, runtimeEvent;
const add = (type, fn) => {
addEventListener(
type,
fn ||
(async (event) => {
const runtime = await engine;
runtimeEvent = event;
run(runtime, `xworker.on${type}(xworker.event);`, xworker);
}),
!!fn && { once: true },
);
};
const xworker = {
onerror() {},
onmessage() {},
onmessageerror() {},
postMessage: postMessage.bind(self),
// this getter exists so that arbitrarily access to xworker.event
// would always fail once an event has been dispatched, as that's not
// meant to be accessed in the wild, respecting the one-off event nature of JS.
get event() {
const event = runtimeEvent;
if (!event) throw new Error("Unauthorized event access");
runtimeEvent = void 0;
return event;
},
};
add("message", ({ data: { options, code } }) => {
engine = (async () => {
const { type, version, config, async: isAsync } = options;
const engine = await getRuntime(getRuntimeID(type, version), config);
const details = registry.get(type);
(run = details[`runWorker${isAsync ? "Async" : ""}`].bind(details))(
engine,
code,
(globalThis.xworker = xworker),
);
return engine;
})();
add("error");
add("message");
add("messageerror");
});

View File

@@ -0,0 +1,35 @@
import xworker from "./xworker.js";
import { assign, defineProperty, absoluteURL } from "../utils.js";
import { getText } from "../fetch-utils.js";
/**
* @typedef {Object} WorkerOptions plugin configuration
* @prop {string} type the runtime/interpreter type to use
* @prop {string} [version] the optional runtime version to use
* @prop {string} [config] the optional config to use within such runtime
*/
export default (...args) =>
/**
* A XWorker is a Worker facade able to bootstrap a channel with any desired runtime.
* @param {string} url the remote file to evaluate on bootstrap
* @param {WorkerOptions} [options] optional arguments to define the runtime to use
* @returns {Worker}
*/
function XWorker(url, options) {
const worker = xworker();
const { postMessage } = worker;
if (args.length) {
const [type, version] = args;
options = assign({}, options || { type, version });
if (!options.type) options.type = type;
}
if (options?.config) options.config = absoluteURL(options.config);
const bootstrap = fetch(url)
.then(getText)
.then((code) => postMessage.call(worker, { options, code }));
return defineProperty(worker, "postMessage", {
value: (data, ...rest) =>
bootstrap.then(() => postMessage.call(worker, data, ...rest)),
});
};

37
pyscript.core/index.html Normal file
View File

@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>python</title>
</head>
<body>
<ul>
<li><a href="./test/pyscript.html">pyscript</a></li>
<li><a href="./test/index.html">pyodide</a></li>
<li>
<a href="./test/matplot.worker.html">matplot worker (toml)</a>
</li>
<li><a href="./test/matplot.json.html">matplot json</a></li>
<li><a href="./test/matplot.html">matplot toml</a></li>
<li><a href="./test/many.html">all together</a></li>
<li><a href="./test/micropython.html">micropython</a></li>
<li><a href="./test/remote.html">remote micropython</a></li>
<li><a href="./test/shadow-dom.html">shadow-dom</a></li>
<li><a href="./test/table.html">table</a></li>
<li><a href="./test/env.html">env</a></li>
<li><a href="./test/isolated.html">isolated</a></li>
<li><a href="./test/fetch.html">config fetch</a></li>
<li><a href="./test/py-events.html">py-* events</a></li>
<li><a href="./test/ruby.html">ruby</a></li>
<li><a href="./test/wasmoon.html">lua</a></li>
<li><a href="./test/async.html">async</a></li>
<li><a href="./test/worker/">worker</a></li>
<li><a href="./test/plugins/">plugins</a></li>
<li>
<a href="./test/plugins/py-script.html">plugins - PyScript</a>
</li>
</ul>
</body>
</html>

View File

@@ -0,0 +1,3 @@
# MicroPython
This folder contains a preview of MicroPython and it needs a `localhost` server to be discovered by integration tests.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,8 @@
{
"imports": {
"http://pyodide": "./test/mocked/pyodide.mjs",
"https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.mjs": "./test/mocked/pyodide.mjs",
"http://localhost:8080/micropython/micropython.mjs": "./test/mocked/micropython.mjs",
"https://unpkg.com/basic-toml@0.3.1/es.js": "./test/mocked/toml.mjs"
}
}

3519
pyscript.core/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
{
"name": "@pyscript/core",
"version": "0.0.0",
"description": "",
"main": "./cjs/index.js",
"types": "./types/index.d.ts",
"scripts": {
"server": "npx static-handler --cors .",
"build": "npm run rollup:xworker && npm run rollup:min && 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:xworker": "rollup --config rollup/xworker.config.js",
"test": "c8 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:worker": "echo worker is $(cat esm/worker/xworker.js | brotli | wc -c) bytes once compressed",
"ts": "tsc -p ."
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@node-loader/import-maps": "^1.1.0",
"@rollup/plugin-node-resolve": "^15.1.0",
"@rollup/plugin-terser": "^0.4.3",
"ascjs": "^5.0.1",
"c8": "^7.14.0",
"eslint": "^8.41.0",
"linkedom": "^0.14.26",
"rollup": "^3.23.0",
"static-handler": "^0.3.2",
"typescript": "^5.0.4"
},
"module": "./esm/index.js",
"type": "module",
"exports": {
".": {
"types": "./types/esm/index.d.ts",
"import": "./esm/index.js",
"default": "./cjs/index.js"
},
"./package.json": "./package.json"
},
"unpkg": "min.js",
"dependencies": {
"basic-devtools": "^0.1.6"
},
"worker": {
"blob": "sha256-gI4kK2AXlU8ZeNni68iQv7KcVZlHjloKCe2H3Ah7ePE="
}
}

View File

@@ -0,0 +1,3 @@
# PyScript
This folder contains an older version of PyScript and it needs a `localhost` server to be discovered by integration tests.

View File

@@ -0,0 +1,334 @@
/* py-config - not a component */
py-config {
display: none;
}
/* py-{el} - components not defined */
py-script:not(:defined) {
display: none;
}
py-repl:not(:defined) {
display: none;
}
py-title:not(:defined) {
display: none;
}
py-inputbox:not(:defined) {
display: none;
}
py-button:not(:defined) {
display: none;
}
py-box:not(:defined) {
display: none;
}
html {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
line-height: 1.5;
}
.spinner::after {
content: "";
box-sizing: border-box;
width: 40px;
height: 40px;
position: absolute;
top: calc(40% - 20px);
left: calc(50% - 20px);
border-radius: 50%;
}
.spinner.smooth::after {
border-top: 4px solid rgba(255, 255, 255, 1);
border-left: 4px solid rgba(255, 255, 255, 1);
border-right: 4px solid rgba(255, 255, 255, 0);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.label {
text-align: center;
width: 100%;
display: block;
color: rgba(255, 255, 255, 0.8);
font-size: 0.8rem;
margin-top: 6rem;
}
/* Pop-up second layer begin */
.py-overlay {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
color: white;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
transition: opacity 500ms;
visibility: hidden;
color: visible;
opacity: 1;
}
.py-overlay {
visibility: visible;
opacity: 1;
}
.py-pop-up {
text-align: center;
width: 600px;
}
.py-pop-up p {
margin: 5px;
}
.py-pop-up a {
position: absolute;
color: white;
text-decoration: none;
font-size: 200%;
top: 3.5%;
right: 5%;
}
/* Pop-up second layer end */
.alert-banner {
position: relative;
padding: 0.5rem 1.5rem 0.5rem 0.5rem;
margin: 5px 0;
}
.alert-banner p {
margin: 0;
}
.py-error {
background-color: #ffe9e8;
border: solid;
border-color: #f0625f;
color: #9d041c;
}
.py-warning {
background-color: rgb(255, 244, 229);
border: solid;
border-color: #ffa016;
color: #794700;
}
.alert-banner.py-error > #alert-close-button {
color: #9d041c;
}
.alert-banner.py-warning > #alert-close-button {
color: #794700;
}
#alert-close-button {
position: absolute;
right: 0.5rem;
top: 0.5rem;
cursor: pointer;
background: transparent;
border: none;
}
.py-box {
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.py-box div.py-box-child * {
max-width: 100%;
}
.py-repl-box {
flex-direction: column;
}
.py-repl-editor {
--tw-border-opacity: 1;
border-color: rgba(209, 213, 219, var(--tw-border-opacity));
border-width: 1px;
position: relative;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
position: relative;
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: rgb(209, 213, 219);
}
.editor-box:hover button {
opacity: 1;
}
.py-repl-run-button {
opacity: 0;
bottom: 0.25rem;
right: 0.25rem;
position: absolute;
padding: 0;
line-height: inherit;
color: inherit;
cursor: pointer;
background-color: transparent;
background-image: none;
-webkit-appearance: button;
text-transform: none;
font-family: inherit;
font-size: 100%;
margin: 0;
text-rendering: auto;
letter-spacing: normal;
word-spacing: normal;
line-height: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
display: inline-block;
text-align: center;
align-items: flex-start;
cursor: default;
box-sizing: border-box;
background-color: -internal-light-dark(rgb(239, 239, 239), rgb(59, 59, 59));
margin: 0em;
padding: 1px 6px;
border: 0;
}
.py-repl-run-button:hover {
opacity: 1;
}
.py-title {
text-transform: uppercase;
text-align: center;
}
.py-title h1 {
font-weight: 700;
font-size: 1.875rem;
}
.py-input {
padding: 0.5rem;
--tw-border-opacity: 1;
border-color: rgba(209, 213, 219, var(--tw-border-opacity));
border-width: 1px;
border-radius: 0.25rem;
margin-right: 0.75rem;
border-style: solid;
width: auto;
}
.py-box input.py-input {
width: -webkit-fill-available;
}
.central-content {
max-width: 20rem;
margin-left: auto;
margin-right: auto;
}
input {
text-rendering: auto;
color: -internal-light-dark(black, white);
letter-spacing: normal;
word-spacing: normal;
line-height: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
display: inline-block;
text-align: start;
appearance: auto;
-webkit-rtl-ordering: logical;
background-color: -internal-light-dark(rgb(255, 255, 255), rgb(59, 59, 59));
margin: 0em;
padding: 1px 2px;
border-width: 2px;
border-style: inset;
border-color: -internal-light-dark(rgb(118, 118, 118), rgb(133, 133, 133));
border-image: initial;
}
.py-button {
--tw-text-opacity: 1;
color: rgba(255, 255, 255, var(--tw-text-opacity));
padding: 0.5rem;
--tw-bg-opacity: 1;
background-color: rgba(37, 99, 235, var(--tw-bg-opacity));
--tw-border-opacity: 1;
border-color: rgba(37, 99, 235, var(--tw-border-opacity));
border-width: 1px;
border-radius: 0.25rem;
cursor: pointer;
}
.py-li-element p {
margin: 5px;
}
.py-li-element p {
display: inline;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
.line-through {
text-decoration: line-through;
}
/* ===== py-terminal plugin ===== */
/* XXX: it would be nice if these rules were stored in e.g. pyterminal.css and
bundled together at build time (by rollup?) */
.py-terminal {
min-height: 10em;
background-color: black;
color: white;
padding: 0.5rem;
overflow: auto;
}
.py-terminal-hidden {
display: none;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
// ⚠️ This files modifies at build time esm/runtimes.js so that
// it's impossible to forget to export a runtime from esm/runtime folder.
const { join, resolve } = require("node:path");
const { readdirSync, readFileSync, writeFileSync } = require("node:fs");
const RUNTIMES_DIR = resolve(join(__dirname, "..", "esm", "runtime"));
const RUNTIMES_JS = resolve(join(__dirname, "..", "esm", "runtimes.js"));
const createRuntimes = () => {
const runtimes = [];
for (const file of readdirSync(RUNTIMES_DIR)) {
// ignore files starting with underscore
if (/^[a-z].+?\.js/.test(file)) runtimes.push(file.slice(0, -3));
}
// generate the output to append at the end of the file
const output = [];
for (const runtime of runtimes)
output.push(`import ${runtime} from './runtime/${runtime}.js';`);
output.push(
`for (const runtime of [${runtimes.join(", ")}]) register(runtime);`,
);
return output.join("\n");
};
writeFileSync(
RUNTIMES_JS,
// find //:RUNTIMES comment and replace anything after that
readFileSync(RUNTIMES_JS)
.toString()
.replace(/(\/\/:RUNTIMES)([\S\s]*)$/, `$1\n${createRuntimes()}\n`),
);

View File

@@ -0,0 +1,30 @@
// ⚠️ This files creates esm/worker/xworker.js in a way that it can be loaded
// through a Blob and as a string, allowing Workers to run within any page.
// This still needs special CSP care when CSP rules are applied to the page
// and this file is also creating a unique sha256 version of that very same
// text content to allow CSP rules to play nicely with it.
const { join, resolve } = require("node:path");
const { readdirSync, readFileSync, rmSync, writeFileSync } = require("node:fs");
const { createHash } = require("node:crypto");
const WORKERS_DIR = resolve(join(__dirname, "..", "esm", "worker"));
const PACKAGE_JSON = resolve(join(__dirname, "..", "package.json"));
for (const file of readdirSync(WORKERS_DIR)) {
if (file.startsWith("__")) {
const js = JSON.stringify(
readFileSync(join(WORKERS_DIR, file)).toString(),
);
const hash = createHash("sha256");
hash.update(js);
const json = require(PACKAGE_JSON);
json.worker = { blob: "sha256-" + hash.digest("base64") };
writeFileSync(PACKAGE_JSON, JSON.stringify(json, null, " "));
writeFileSync(
join(WORKERS_DIR, "xworker.js"),
`/* c8 ignore next */\nexport default () => new Worker(URL.createObjectURL(new Blob([${js}],{type:'application/javascript'})),{type:'module'});`,
);
rmSync(join(WORKERS_DIR, file));
}
}

View File

@@ -0,0 +1,18 @@
// This file generates /min.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/index.js",
plugins: [nodeResolve(), terser()],
output: {
esModule: true,
file: "./min.js",
},
};

View File

@@ -0,0 +1,24 @@
// This file generates /min.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";
import { fileURLToPath } from "node:url";
import { dirname, join, resolve } from "node:path";
createRequire(import.meta.url)("./build_runtimes.cjs");
const WORKERS_DIR = resolve(
join(dirname(fileURLToPath(import.meta.url)), "..", "esm", "worker"),
);
export default {
input: join(WORKERS_DIR, "_template.js"),
plugins: [nodeResolve(), terser()],
output: {
esModule: true,
file: join(WORKERS_DIR, "__template.js"),
},
};

12
pyscript.core/sw.js Normal file
View File

@@ -0,0 +1,12 @@
addEventListener("fetch", (event) => {
event.respondWith(
(async () => {
const cache = await caches.open("python-script");
const cachedResponse = await cache.match(event.request);
if (cachedResponse) return cachedResponse;
const networkResponse = await fetch(event.request);
event.waitUntil(cache.put(event.request, networkResponse.clone()));
return networkResponse;
})(),
);
});

View File

@@ -0,0 +1,27 @@
const { writeFileShim } = require("../cjs/runtime/_utils.js");
const assert = require("./assert.js");
const FS = {
mkdir(...args) {
this.mkdir_args = args;
},
cwd: () => __dirname,
writeFile(...args) {
this.writeFile_args = args;
},
};
writeFileShim(FS, "./test/abc.js", []);
assert(JSON.stringify(FS.mkdir_args), `["${__dirname}/test"]`);
assert(
JSON.stringify(FS.writeFile_args),
`["${__dirname}/test/abc.js",{},{"canOwn":true}]`,
);
writeFileShim(FS, "/./../abc.js", []);
assert(JSON.stringify(FS.mkdir_args), `["${__dirname}"]`);
assert(
JSON.stringify(FS.writeFile_args),
`["${__dirname}/abc.js",{},{"canOwn":true}]`,
);

1
pyscript.core/test/a.py Normal file
View File

@@ -0,0 +1 @@
x = "hello from A"

View File

@@ -0,0 +1,10 @@
const assert = (current, expected, message = "Unexpected Error") => {
if (!Object.is(current, expected)) {
console.error(`\x1b[1m${message}\x1b[0m`);
console.error(" expected", expected);
console.error(" got", current);
process.exit(1);
}
};
module.exports = assert;

View File

@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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>
</head>
<body>
<script type="py" async>
from js import document, fetch
response = await fetch("async.html")
document.body.appendChild(
document.createElement('pre')
).textContent = await response.text()
</script>
</body>
</html>

1
pyscript.core/test/b.py Normal file
View File

@@ -0,0 +1 @@
x = "hello from B"

View File

@@ -0,0 +1,3 @@
{
"packages": ["matplotlib"]
}

View File

@@ -0,0 +1,3 @@
packages = [
"matplotlib"
]

View File

@@ -0,0 +1,8 @@
const div = document.body.appendChild(document.createElement("div"));
div.style.cssText = "position:fixed;top:0;left:0";
let i = 0;
(function counter() {
div.textContent = ++i;
requestAnimationFrame(counter);
})();

View File

@@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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>
</head>
<body>
<script type="mpy">
import sys
import js
version = sys.version
js.document.currentScript.target.textContent = version
</script>
<hr />
<script type="mpy">
import js
js.document.currentScript.target.textContent = version
</script>
<hr />
<script type="py">
import sys
import js
version = sys.version
js.document.currentScript.target.textContent = version
</script>
<hr />
<script type="py" target="last-script-target">
import js
js.document.currentScript.target.textContent = version
</script>
<div id="last-script-target"></div>
</body>
</html>

View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<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>
</head>
<body>
<script type="mpy" config="./fetch.toml">
import js
import a, b
js.console.log(a.x)
js.console.log(b.x)
</script>
</body>
</html>

View File

@@ -0,0 +1,2 @@
[[fetch]]
files = ["./a.py", "./b.py"]

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M31.885 16c-8.124 0-7.617 3.523-7.617 3.523l.01 3.65h7.752v1.095H21.197S16 23.678 16 31.876c0 8.196 4.537 7.906 4.537 7.906h2.708v-3.804s-.146-4.537 4.465-4.537h7.688s4.32.07 4.32-4.175v-7.019S40.374 16 31.885 16zm-4.275 2.454c.771 0 1.395.624 1.395 1.395s-.624 1.395-1.395 1.395a1.393 1.393 0 0 1-1.395-1.395c0-.771.624-1.395 1.395-1.395z" fill="url(#a)"/><path d="M32.115 47.833c8.124 0 7.617-3.523 7.617-3.523l-.01-3.65H31.97v-1.095h10.832S48 40.155 48 31.958c0-8.197-4.537-7.906-4.537-7.906h-2.708v3.803s.146 4.537-4.465 4.537h-7.688s-4.32-.07-4.32 4.175v7.019s-.656 4.247 7.833 4.247zm4.275-2.454a1.393 1.393 0 0 1-1.395-1.395c0-.77.624-1.394 1.395-1.394s1.395.623 1.395 1.394c0 .772-.624 1.395-1.395 1.395z" fill="url(#b)"/><defs><linearGradient id="a" x1="19.075" y1="18.782" x2="34.898" y2="34.658" gradientUnits="userSpaceOnUse"><stop stop-color="#387EB8"/><stop offset="1" stop-color="#366994"/></linearGradient><linearGradient id="b" x1="28.809" y1="28.882" x2="45.803" y2="45.163" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE052"/><stop offset="1" stop-color="#FFC331"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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>
</head>
<body>
<script type="py">
import sys
import js
js.document.currentScript.target.textContent = sys.version
</script>
</body>
</html>

209
pyscript.core/test/index.js Normal file
View File

@@ -0,0 +1,209 @@
const assert = require("./assert.js");
require("./_utils.js");
const { fetch } = globalThis;
const tick = (ms = 10) => new Promise(($) => setTimeout($, ms));
const clear = (python) => {
for (const [key, value] of Object.entries(python)) {
if (typeof value === "object") python[key] = null;
else python[key] = "";
}
};
const patchFetch = (callback) => {
Object.defineProperty(globalThis, "fetch", {
configurable: true,
get() {
try {
return callback;
} finally {
globalThis.fetch = fetch;
}
},
});
};
const { parseHTML } = require("linkedom");
const { document, window } = parseHTML("...");
globalThis.document = document;
globalThis.Element = window.Element;
globalThis.MutationObserver = window.MutationObserver;
globalThis.XPathResult = {};
globalThis.XPathEvaluator =
window.XPathEvaluator ||
class XPathEvaluator {
createExpression() {
return { evaluate: () => [] };
}
};
const { registerPlugin } = require("../cjs");
(async () => {
// shared 3rd party mocks
const {
python: pyodide,
setTarget,
loadPyodide,
} = await import("./mocked/pyodide.mjs");
const { python: micropython } = await import("./mocked/micropython.mjs");
// shared helpers
const div = document.createElement("div");
const shadowRoot = div.attachShadow({ mode: "open" });
const content = `
import sys
import js
js.document.currentScript.target.textContent = sys.version
`;
const { URL } = globalThis;
globalThis.URL = function (href) {
return { href };
};
globalThis.location = { href: "" };
// all tests
for (const test of [
async function versionedRuntime() {
document.head.innerHTML = `<script type="py" version="0.22.1">${content}</script>`;
await tick();
assert(pyodide.content, content);
assert(pyodide.target.tagName, "PY-SCRIPT");
},
async function basicExpectations() {
document.head.innerHTML = `<script type="py">${content}</script>`;
await tick();
assert(pyodide.content, content);
assert(pyodide.target.tagName, "PY-SCRIPT");
},
async function foreignRuntime() {
document.head.innerHTML = `<script type="py" version="http://pyodide">${content}</script>`;
await tick();
assert(pyodide.content, content);
assert(pyodide.target.tagName, "PY-SCRIPT");
},
async function basicMicroPython() {
document.head.innerHTML = `<script type="mpy">${content}</script>`;
await tick();
assert(micropython.content, content);
assert(micropython.target.tagName, "MPY-SCRIPT");
const script = document.head.firstElementChild;
document.body.appendChild(script);
await tick();
assert(script.nextSibling, micropython.target);
micropython.target = null;
},
async function exernalResourceInShadowRoot() {
patchFetch(() =>
Promise.resolve({ text: () => Promise.resolve("OK") }),
);
shadowRoot.innerHTML = `
<my-plugin></my-plugin>
<script src="./whatever" env="unique" type="py" target="my-plugin"></script>
`.trim();
await tick();
assert(pyodide.content, "OK");
assert(pyodide.target.tagName, "MY-PLUGIN");
},
async function explicitTargetNode() {
setTarget(div);
shadowRoot.innerHTML = `
<my-plugin></my-plugin>
<script type="py"></script>
`.trim();
await tick();
assert(pyodide.target, div);
},
async function explicitTargetAsString() {
setTarget("my-plugin");
shadowRoot.innerHTML = `
<my-plugin></my-plugin>
<script type="py"></script>
`.trim();
await tick();
assert(pyodide.target.tagName, "MY-PLUGIN");
},
async function jsonConfig() {
const packages = {};
patchFetch(() => Promise.resolve({ json: () => ({ packages }) }));
shadowRoot.innerHTML = `<script config="./whatever.json" type="py"></script>`;
await tick();
assert(pyodide.packages, packages);
},
async function tomlConfig() {
const jsonPackages = JSON.stringify({
packages: { a: Math.random() },
});
patchFetch(() =>
Promise.resolve({ text: () => Promise.resolve(jsonPackages) }),
);
shadowRoot.innerHTML = `<script config="./whatever.toml" type="py"></script>`;
// there are more promises in here let's increase the tick delay to avoid flaky tests
await tick(20);
assert(
JSON.stringify({ packages: pyodide.packages }),
jsonPackages,
);
},
async function fetchConfig() {
const jsonPackages = JSON.stringify({
fetch: [
{ files: ["./a.py", "./b.py"] },
{ from: "utils" },
{ from: "/utils", files: ["c.py"] },
],
});
patchFetch(() =>
Promise.resolve({
arrayBuffer: () => Promise.resolve([]),
text: () => Promise.resolve(jsonPackages),
}),
);
shadowRoot.innerHTML = `
<script type="py" config="./fetch.toml">
import js
import a, b
js.console.log(a.x)
js.console.log(b.x)
</script>
`;
await tick(10);
},
async function testDefaultRuntime() {
const py = await pyscript.env.py;
const keys = Object.keys(loadPyodide()).join(",");
assert(Object.keys(py).join(","), keys);
const unique = await pyscript.env.unique;
assert(Object.keys(unique).join(","), keys);
},
async function pyEvents() {
shadowRoot.innerHTML = `
<button py-click="test()">test</button>
<button py-env="unique" py-click="test()">test</button>
`;
await tick(20);
},
]) {
await test();
clear(pyodide);
clear(micropython);
}
globalThis.URL = URL;
})();

View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<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>
</head>
<body>
<script type="py">
# define a "global" variable and print it
shared_env = 1
print(shared_env)
</script>
<script type="py">
# just print the "global" variable from same env
print(shared_env)
</script>
<script type="py" env="another-one">
# see the error
print(shared_env)
</script>
</body>
</html>

View File

@@ -0,0 +1,17 @@
{
"background_color": "#ffffff",
"description": "PythonScript",
"display": "standalone",
"name": "PythonScript",
"orientation": "any",
"short_name": "PythonScript",
"start_url": "./",
"theme_color": "#ffffff",
"icons": [
{
"src": "icon.svg",
"sizes": "144x144",
"type": "image/svg+xml"
}
]
}

View File

@@ -0,0 +1,65 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
</head>
<body>
<script type="py" version="0.23.2">
import sys
import js
js.document.currentScript.target.textContent = sys.version
</script>
<script type="mpy" src="three.py"></script>
<script type="py" config="./config.json">
import matplotlib.pyplot as plt
import matplotlib.tri as tri
import numpy as np
import base64
import io
import js
# First create the x and y coordinates of the points.
n_angles = 36
n_radii = 8
min_radius = 0.25
radii = np.linspace(min_radius, 0.95, n_radii)
angles = np.linspace(0, 2 * np.pi, n_angles, endpoint=False)
angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)
angles[:, 1::2] += np.pi / n_angles
x = (radii * np.cos(angles)).flatten()
y = (radii * np.sin(angles)).flatten()
z = (np.cos(radii) * np.cos(3 * angles)).flatten()
# Create the Triangulation; no triangles so Delaunay triangulation created.
triang = tri.Triangulation(x, y)
# Mask off unwanted triangles.
triang.set_mask(np.hypot(x[triang.triangles].mean(axis=1),
y[triang.triangles].mean(axis=1))
< min_radius)
fig1, ax1 = plt.subplots()
ax1.set_aspect('equal')
tpc = ax1.tripcolor(triang, z, shading='flat')
fig1.colorbar(tpc)
ax1.set_title('tripcolor of Delaunay triangulation, flat shading')
buf = io.BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
img = js.document.createElement("img")
img.style.transform = "scale(.5)"
img.src = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')
js.document.currentScript.target.appendChild(img)
</script>
</body>
</html>

View File

@@ -0,0 +1,59 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
</head>
<body>
<script type="py" config="./config.toml">
import matplotlib.pyplot as plt
import matplotlib.tri as tri
import numpy as np
import base64
import io
import js
# First create the x and y coordinates of the points.
n_angles = 36
n_radii = 8
min_radius = 0.25
radii = np.linspace(min_radius, 0.95, n_radii)
angles = np.linspace(0, 2 * np.pi, n_angles, endpoint=False)
angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)
angles[:, 1::2] += np.pi / n_angles
x = (radii * np.cos(angles)).flatten()
y = (radii * np.sin(angles)).flatten()
z = (np.cos(radii) * np.cos(3 * angles)).flatten()
# Create the Triangulation; no triangles so Delaunay triangulation created.
triang = tri.Triangulation(x, y)
# Mask off unwanted triangles.
triang.set_mask(np.hypot(x[triang.triangles].mean(axis=1),
y[triang.triangles].mean(axis=1))
< min_radius)
fig1, ax1 = plt.subplots()
ax1.set_aspect('equal')
tpc = ax1.tripcolor(triang, z, shading='flat')
fig1.colorbar(tpc)
ax1.set_title('tripcolor of Delaunay triangulation, flat shading')
buf = io.BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
img = js.document.createElement("img")
img.style.transform = "scale(.5)"
img.src = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')
js.document.currentScript.target.appendChild(img)
</script>
</body>
</html>

View File

@@ -0,0 +1,59 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
</head>
<body>
<script type="py" config="./config.json">
import matplotlib.pyplot as plt
import matplotlib.tri as tri
import numpy as np
import base64
import io
import js
# First create the x and y coordinates of the points.
n_angles = 36
n_radii = 8
min_radius = 0.25
radii = np.linspace(min_radius, 0.95, n_radii)
angles = np.linspace(0, 2 * np.pi, n_angles, endpoint=False)
angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)
angles[:, 1::2] += np.pi / n_angles
x = (radii * np.cos(angles)).flatten()
y = (radii * np.sin(angles)).flatten()
z = (np.cos(radii) * np.cos(3 * angles)).flatten()
# Create the Triangulation; no triangles so Delaunay triangulation created.
triang = tri.Triangulation(x, y)
# Mask off unwanted triangles.
triang.set_mask(np.hypot(x[triang.triangles].mean(axis=1),
y[triang.triangles].mean(axis=1))
< min_radius)
fig1, ax1 = plt.subplots()
ax1.set_aspect('equal')
tpc = ax1.tripcolor(triang, z, shading='flat')
fig1.colorbar(tpc)
ax1.set_title('tripcolor of Delaunay triangulation, flat shading')
buf = io.BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
img = js.document.createElement("img")
img.style.transform = "scale(.5)"
img.src = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')
js.document.currentScript.target.appendChild(img)
</script>
</body>
</html>

View File

@@ -0,0 +1,51 @@
import js
import matplotlib
try:
js.document
except AttributeError:
matplotlib.use("agg")
import base64
import io
import matplotlib.pyplot as plt
import matplotlib.tri as tri
import numpy as np
# First create the x and y coordinates of the points.
n_angles = 36
n_radii = 8
min_radius = 0.25
radii = np.linspace(min_radius, 0.95, n_radii)
angles = np.linspace(0, 2 * np.pi, n_angles, endpoint=False)
angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)
angles[:, 1::2] += np.pi / n_angles
x = (radii * np.cos(angles)).flatten()
y = (radii * np.sin(angles)).flatten()
z = (np.cos(radii) * np.cos(3 * angles)).flatten()
# Create the Triangulation; no triangles so Delaunay triangulation created.
triang = tri.Triangulation(x, y)
# Mask off unwanted triangles.
triang.set_mask(
np.hypot(x[triang.triangles].mean(axis=1), y[triang.triangles].mean(axis=1))
< min_radius
)
fig1, ax1 = plt.subplots()
ax1.set_aspect("equal")
tpc = ax1.tripcolor(triang, z, shading="flat")
fig1.colorbar(tpc)
ax1.set_title("tripcolor of Delaunay triangulation, flat shading")
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")
)

View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
</head>
<body>
<script type="mpy">
from js import XWorker
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': 'py', 'config': './config.toml'})
w.onmessage = show_image
</script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
</head>
<body>
<script type="mpy">
import sys
import js
js.document.currentScript.target.textContent = sys.version
</script>
</body>
</html>

View File

@@ -0,0 +1,7 @@
export const python = { content: "", target: null };
export const loadMicroPython = () => ({
runPython(content) {
python.content = content;
python.target = document.currentScript.target;
},
});

View File

@@ -0,0 +1,40 @@
import { basename, dirname } from "node:path";
let target;
export const setTarget = (value) => {
target = value;
};
export const python = { content: "", target: null, packages: null };
export const loadPyodide = () => ({
loadPackage() {},
pyimport() {
return {
install(packages) {
python.packages = packages;
},
destroy() {},
};
},
runPython(content) {
python.content = content;
if (target) {
document.currentScript.target = target;
target = void 0;
}
python.target = document.currentScript.target;
},
FS: {
mkdirTree() {},
writeFile() {},
analyzePath: (path) => ({
parentPath: dirname(path),
name: basename(path),
}),
},
_module: {
PATH: { dirname },
PATH_FS: {
resolve: (path) => path,
},
},
});

View File

@@ -0,0 +1 @@
export const parse = (text) => JSON.parse(text);

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
</head>
<body>
<script type="py">
import sys
import js
js.document.currentScript.target.textContent = sys.version
</script>
</body>
</html>

View File

@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script type="importmap">
{
"imports": {
"basic-devtools": "../node_modules/basic-devtools/esm/index.js"
}
}
</script>
<script type="module" src="../esm/index.js"></script>
</head>
<body>
<script type="mpy" src="http://localhost:7357/print-a.py"></script>
<script type="mpy">
print('B')
</script>
<script type="mpy" src="http://localhost:7357/print-a.py"></script>
<script type="mpy">
print('C')
</script>
<script type="mpy">
print('D')
</script>
<pre>
A
B
A
C
D</pre
>
</body>
</html>

View File

@@ -0,0 +1,11 @@
const { readFileSync } = require("fs");
require("http")
.createServer((req, res) => {
const content = readFileSync(__dirname + req.url);
res.setHeader("Access-Control-Allow-Origin", "*");
setTimeout(() => {
res.end(content);
}, 1000);
})
.listen(7357);

View File

@@ -0,0 +1,3 @@
{
"type": "commonjs"
}

View File

@@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Plugins</title>
<style>
py-script {
display: none;
}
</style>
<script type="importmap">
{ "imports": { "@pyscript/core": "../../min.js" } }
</script>
<script type="module">
import { registerPlugin } from "@pyscript/core";
registerPlugin("mpy-script", {
type: "micropython", // or just 'mpy'
async onRuntimeReady(element, micropython) {
console.log(micropython);
// Somehow this doesn't work in MicroPython
micropython.io.stdout = (message) => {
console.log("🐍", micropython.type, message);
};
micropython.run(element.textContent);
element.replaceChildren("See console ->");
element.style.display = "block";
},
});
</script>
</head>
<body>
<mpy-script> print('Hello Console!') </mpy-script>
</body>
</html>

View File

@@ -0,0 +1,34 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Plugins</title>
<style>
py-script {
display: none;
}
</style>
<script type="importmap">
{ "imports": { "@pyscript/core": "../../min.js" } }
</script>
<script type="module">
import { registerPlugin } from "@pyscript/core";
registerPlugin("lua-script", {
type: "wasmoon", // or just 'lua'
async onRuntimeReady(element, wasmoon) {
// Somehow this doesn't work in Wasmoon
wasmoon.io.stdout = (message) => {
console.log("🌑", wasmoon.type, message);
};
wasmoon.run(element.textContent);
element.replaceChildren("See console ->");
element.style.display = "block";
},
});
</script>
</head>
<body>
<lua-script> print('Hello Console!') </lua-script>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<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" } }
</script>
<script type="module" src="py-script.js"></script>
</head>
<body>
<py-config>
[[fetch]]
from = "../"
to_folder = "./"
files = ["a.py", "b.py"]
</py-config>
<py-script>
import js
import a, b
print('Hello Console!')
js.console.log(a.x, b.x)
'Hello Web!'
</py-script>
</body>
</html>

View File

@@ -0,0 +1,59 @@
import { registerPlugin } 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;
const sharedPyodide = new Promise((resolve) => {
const pyConfig = document.querySelector("py-config");
const config = pyConfig?.getAttribute("src") || pyConfig?.textContent;
registerPlugin("py-script", {
config,
type: "pyodide", // or just 'py'
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;
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

@@ -0,0 +1 @@
print("A")

View File

@@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python events</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
</head>
<body>
<script type="py">
def print_version(event):
import sys
print(event.type)
print(sys.version)
</script>
<button
py-pointerdown="print_version(event)"
py-click="print_version(event)"
>
python version
</button>
<script type="mpy">
def print_version(event):
import sys
print(event.type)
print(sys.version)
</script>
<button
mpy-pointerdown="print_version(event)"
mpy-click="print_version(event)"
>
micropython version
</button>
</body>
</html>

View File

@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>py-script</title>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="../pyscript/pyscript.css" />
<script defer src="../pyscript/pyscript.js"></script>
<title>py-script</title>
</head>
<body>
<py-script>
import sys
display(sys.version, target="target")
</py-script>
<div id="target"></div>
</body>
</html>

View File

@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script defer src="./counter.js"></script>
<script type="module" src="../min.js"></script>
</head>
<body>
<script
type="mpy"
version="http://localhost:8080/micropython/micropython.mjs"
>
import sys
import js
js.document.currentScript.target.textContent = sys.version
</script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Ruby</title>
<link rel="stylesheet" href="style.css" />
<script type="importmap">
{
"imports": {
"basic-devtools": "../node_modules/basic-devtools/esm/index.js"
}
}
</script>
<script type="module" src="../esm/index.js"></script>
</head>
<body>
<script type="rb">
def print_version(event)
puts event[:type]
print "ruby #{ RUBY_VERSION }p#{ RUBY_PATCHLEVEL }"
end
</script>
<button rb-click="print_version(event)">ruby version</button>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<script>
navigator.serviceWorker.register("../sw.js");
</script>
<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">
customElements.define(
"shadow-dom",
class extends HTMLElement {
constructor() {
const sd = super().attachShadow({ mode: "closed" });
sd.appendChild(
Object.assign(document.createElement("script"), {
type: "mpy",
textContent: `
import sys
import js
js.document.currentScript.target.textContent = sys.version
`,
}),
);
}
},
);
</script>
</head>
<body>
<shadow-dom></shadow-dom>
</body>
</html>

View File

@@ -0,0 +1,9 @@
html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
sans-serif;
}
:root python-script {
display: block;
}

View File

@@ -0,0 +1,34 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<style>
table {
background-color: silver;
}
tr {
background-color: white;
color: black;
}
</style>
<link rel="stylesheet" href="style.css" />
<script type="module" src="../min.js"></script>
</head>
<body>
<table cellspacing="2" cellpadding="2">
<tr>
<td>check edge cases</td>
<script type="mpy">
import sys
import js
td = js.document.createElement('td')
td.textContent = 'OK'
js.document.currentScript.target = td
</script>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python</title>
<link rel="stylesheet" href="style.css" />
<script type="importmap">
{
"imports": {
"basic-devtools": "../node_modules/basic-devtools/esm/index.js"
}
}
</script>
<script type="module" src="../esm/index.js"></script>
</head>
<body>
<script type="py">
def print_version():
import sys
print(sys.version)
print("bad.", file=sys.stderr)
</script>
<button py-click="print_version()">python version</button>
<script type="mpy">
def print_version():
import sys
print(sys.version)
print("bad.", file=sys.stderr)
</script>
<button mpy-click="print_version()">micropython version</button>
<script type="lua">
function print_version()
print(_VERSION)
printErr(123)
end
</script>
<button lua-click="print_version()">lua version</button>
<script type="rb">
def print_version()
print "ruby #{ RUBY_VERSION }p#{ RUBY_PATCHLEVEL }"
end
</script>
<button rb-click="print_version()">ruby version</button>
</body>
</html>

View File

@@ -0,0 +1,3 @@
import js
js.document.currentScript.target.textContent = 1 + 2

View File

@@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>lua</title>
<link rel="stylesheet" href="style.css" />
<script type="importmap">
{
"imports": {
"basic-devtools": "../node_modules/basic-devtools/esm/index.js"
}
}
</script>
<script type="module" src="../esm/index.js"></script>
</head>
<body>
<script type="lua" config="./fetch.toml">
local function read_file(path)
local file = io.open(path, "rb")
if not file then return nil end
local content = file:read "*a"
file:close()
return content
end
function print_version(event)
print(event.type)
print(_VERSION)
print(read_file('/a.py'))
end
</script>
<button lua-click="print_version(event)">lua version</button>
</body>
</html>

View File

@@ -0,0 +1,77 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>python workers</title>
<script type="importmap">
{
"imports": {
"basic-devtools": "../../node_modules/basic-devtools/esm/index.js",
"@pyscript/core": "../../esm/index.js"
}
}
</script>
<script defer src="../counter.js"></script>
</head>
<body>
<!-- XWorker - JavaScript to MicroPython -->
<script type="module">
import { XWorker } from "@pyscript/core";
const w = new XWorker("./worker.py", { type: "mpy" });
w.postMessage("JavaScript: Hello MicroPython 👋");
w.onmessage = (event) => {
console.log(event.data);
};
</script>
<!-- XWorker - MicroPython to MicroPython -->
<script type="mpy">
from js import XWorker
def handle_message(event):
print(event.data)
w = XWorker('./worker.py')
w.postMessage('MicroPython: Hello MicroPython 👋')
w.onmessage = handle_message
</script>
<!-- XWorker - MicroPython to Pyodide -->
<script type="mpy">
from js import XWorker
def handle_message(event):
print(event.data)
w = XWorker('./worker.py', **{'type': 'py', 'async': True, 'config': '../fetch.toml'})
w.postMessage('MicroPython: Hello Pyodide 👋')
w.onmessage = handle_message
</script>
<!-- XWorker - MicroPython to Lua -->
<script type="mpy">
from js import XWorker
def handle_message(event):
print(event.data)
w = XWorker('./worker.lua', type='lua')
w.postMessage('MicroPython: Hello Lua 👋')
w.onmessage = handle_message
</script>
<!-- XWorker - MicroPython to Ruby
<script type="mpy">
from js import XWorker
def handle_message(event):
print(event.data)
w = XWorker('./worker.rb', type="rb")
w.postMessage('MicroPython: Hello Ruby 👋')
w.onmessage = handle_message
</script><!---->
</body>
</html>

View File

@@ -0,0 +1,6 @@
function on_message(event)
print(event.data)
xworker.postMessage('Lua: Hello MicroPython 👋')
end
xworker.onmessage = on_message

View File

@@ -0,0 +1,9 @@
from js import xworker
def on_message(event):
print(event.data)
xworker.postMessage("Pyodide: Hello MicroPython 👋")
xworker.onmessage = on_message

View File

@@ -0,0 +1,10 @@
require "js"
xworker = JS::eval("return xworker")
def on_message(event)
puts event[:data]
xworker.postMessage('Ruby: Hello MicroPython 👋')
end
xworker.onmessage = on_message

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "ES2022",
"target": "ES2022",
"moduleResolution": "Classic",
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "types"
},
"include": ["esm/index.js"]
}

3
pyscript.core/types/fetch-utils.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
export function getBuffer(response: Response): Promise<ArrayBuffer>;
export function getJSON(response: Response): Promise<any>;
export function getText(response: Response): Promise<string>;

5
pyscript.core/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
export { registerPlugin } from "./plugins.js";
export const XWorker: (
url: string,
options?: import("./worker/class.js").WorkerOptions,
) => Worker;

2
pyscript.core/types/loader.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export function getRuntime(id: string, config?: string): Promise<any>;
export function getRuntimeID(type: string, version?: string): string;

61
pyscript.core/types/plugins.d.ts vendored Normal file
View File

@@ -0,0 +1,61 @@
export const PLUGINS_SELECTORS: any[];
export function handlePlugin(node: Element): void;
export function registerPlugin(name: string, options: PluginOptions): void;
/**
* plugin configuration
*/
export type Runtime = {
/**
* the runtime type
*/
type: string;
/**
* the bootstrapped runtime
*/
runtime: object;
/**
* an XWorker constructor that defaults to same runtime on the Worker.
*/
XWorker: (url: string, options?: object) => Worker;
/**
* a cloned config used to bootstrap the runtime
*/
config: object;
/**
* an utility to run code within the runtime
*/
run: (code: string) => any;
/**
* an utility to run code asynchronously within the runtime
*/
runAsync: (code: string) => Promise<any>;
/**
* an utility to write a file in the virtual FS, if available
*/
writeFile: (path: string, data: ArrayBuffer) => void;
};
/**
* plugin configuration
*/
export type PluginOptions = {
/**
* the runtime/interpreter type to receive
*/
type: string;
/**
* the optional runtime version to use
*/
version?: string;
/**
* the optional config to use within such runtime
*/
config?: string;
/**
* the optional environment to use
*/
env?: string;
/**
* the callback that will be invoked once
*/
onRuntimeReady: (node: Element, runtime: Runtime) => void;
};

14
pyscript.core/types/runtime/_utils.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
export const io: WeakMap<object, any>;
export function stdio(init: any): {
stderr: (...args: any[]) => any;
stdout: (...args: any[]) => any;
get(engine: any): Promise<any>;
};
export function writeFile(FS: any, path: any, buffer: any): any;
export function writeFileShim(FS: any, path: any, buffer: any): any;
export const base: WeakMap<object, any>;
export function fetchPaths(
module: any,
runtime: any,
config_fetch: any,
): Promise<any[]>;

View File

@@ -0,0 +1,28 @@
declare namespace _default {
const type: string[];
function module(): string;
function engine(
{
loadMicroPython,
}: {
loadMicroPython: any;
},
config: any,
url: any,
): Promise<any>;
function run(runtime: any, code: any): any;
function runAsync(runtime: any, code: any): any;
function runEvent(runtime: any, code: any, key: any): any;
function runWorker(runtime: any, code: any, xworker: any): any;
function runWorkerAsync(runtime: any, code: any, xworker: any): any;
function writeFile(
{
FS,
}: {
FS: any;
},
path: any,
buffer: any,
): any;
}
export default _default;

View File

@@ -0,0 +1,27 @@
declare namespace _default {
const type: string[];
function module(version?: string): string;
function engine(
{
loadPyodide,
}: {
loadPyodide: any;
},
config: any,
): Promise<any>;
function run(runtime: any, code: any): any;
function runAsync(runtime: any, code: any): any;
function runEvent(runtime: any, code: any, key: any): any;
function runWorker(runtime: any, code: any, xworker: any): any;
function runWorkerAsync(runtime: any, code: any, xworker: any): any;
function writeFile(
{
FS,
}: {
FS: any;
},
path: any,
buffer: any,
): any;
}
export default _default;

21
pyscript.core/types/runtime/ruby.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
declare namespace _default {
const experimental: boolean;
const type: string[];
function module(version?: string): string;
function engine(
{
DefaultRubyVM,
}: {
DefaultRubyVM: any;
},
config: any,
url: any,
): Promise<any>;
function run(runtime: any, code: any): any;
function runAsync(runtime: any, code: any): any;
function runEvent(runtime: any, code: any, key: any): any;
function runWorker(runtime: any, code: any, xworker: any): any;
function runWorkerAsync(runtime: any, code: any, xworker: any): any;
function writeFile(): never;
}
export default _default;

View File

@@ -0,0 +1,35 @@
declare namespace _default {
const type: string[];
function module(version?: string): string;
function engine(
{
LuaFactory,
LuaLibraries,
}: {
LuaFactory: any;
LuaLibraries: any;
},
config: any,
): Promise<any>;
function run(runtime: any, code: any): any;
function runAsync(runtime: any, code: any): any;
function runEvent(runtime: any, code: any, key: any): any;
function runWorker(runtime: any, code: any, xworker: any): any;
function runWorkerAsync(runtime: any, code: any, xworker: any): any;
function writeFile(
{
cmodule: {
module: { FS },
},
}: {
cmodule: {
module: {
FS: any;
};
};
},
path: any,
buffer: any,
): any;
}
export default _default;

9
pyscript.core/types/runtimes.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/** @type {Map<string, object>} */
export const registry: Map<string, object>;
/** @type {Map<string, object>} */
export const configs: Map<string, object>;
/** @type {string[]} */
export const selectors: string[];
/** @type {string[]} */
export const prefixes: string[];
export const runtime: Map<any, any>;

View File

@@ -0,0 +1,9 @@
export const runtimes: Map<any, any>;
export function getDetails(
type: any,
id: any,
name: any,
version: any,
config: any,
): any;
export function handle(script: HTMLScriptElement): Promise<void>;

1
pyscript.core/types/toml.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export function parse(text: string): object;

37
pyscript.core/types/utils.d.ts vendored Normal file
View File

@@ -0,0 +1,37 @@
export const isArray: (arg: any) => arg is any[];
export const assign: {
<T extends {}, U>(target: T, source: U): T & U;
<T_1 extends {}, U_1, V>(
target: T_1,
source1: U_1,
source2: V,
): T_1 & U_1 & V;
<T_2 extends {}, U_2, V_1, W>(
target: T_2,
source1: U_2,
source2: V_1,
source3: W,
): T_2 & U_2 & V_1 & W;
(target: object, ...sources: any[]): any;
};
export const create: {
(o: object): any;
(o: object, properties: PropertyDescriptorMap & ThisType<any>): any;
};
export const defineProperty: <T>(
o: T,
p: PropertyKey,
attributes: PropertyDescriptor & ThisType<any>,
) => T;
export const all: {
<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]>;
<T_1 extends [] | readonly unknown[]>(
values: T_1,
): Promise<{ -readonly [P in keyof T_1]: Awaited<T_1[P]> }>;
};
export const resolve: {
(): Promise<void>;
<T>(value: T): Promise<Awaited<T>>;
<T_1>(value: T_1 | PromiseLike<T_1>): Promise<Awaited<T_1>>;
};
export function absoluteURL(path: any, base?: string): string;

21
pyscript.core/types/worker/class.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
declare function _default(
...args: any[]
): (url: string, options?: WorkerOptions) => Worker;
export default _default;
/**
* plugin configuration
*/
export type WorkerOptions = {
/**
* the runtime/interpreter type to use
*/
type: string;
/**
* the optional runtime version to use
*/
version?: string;
/**
* the optional config to use within such runtime
*/
config?: string;
};

Some files were not shown because too many files have changed in this diff Show More