@pyscript/core deprecation (#1607)

This commit is contained in:
Andrea Giammarchi
2023-07-21 11:07:42 +02:00
committed by GitHub
parent f0e69cbc36
commit 26e7a54f1f
142 changed files with 13 additions and 52811 deletions

View File

@@ -44,17 +44,18 @@ jobs:
${{ runner.os }}-build- ${{ runner.os }}-build-
${{ runner.os }}- ${{ runner.os }}-
- name: install next deps # TODO: this will likely change soon to pyscript.next
working-directory: pyscript.core # - name: install next deps
run: npm i; npx playwright install # working-directory: pyscript.core
# run: npm i; npx playwright install
- name: build next # - name: build next
working-directory: pyscript.core # working-directory: pyscript.core
run: npm run build # run: npm run build
- name: Run next tests # - name: Run next tests
working-directory: pyscript.core # working-directory: pyscript.core
run: npm run test # run: npm run test
# TODO: DO we want to upload next yet? # TODO: DO we want to upload next yet?
# - uses: actions/upload-artifact@v3 # - uses: actions/upload-artifact@v3

9
.gitignore vendored
View File

@@ -141,12 +141,3 @@ coverage/
# junit xml for test results # junit xml for test results
test_results test_results
# pyscript.core
pyscript.core/coverage/
pyscript.core/node_modules/
pyscript.core/cjs/
!pyscript.core/cjs/package.json
pyscript.core/min.js
pyscript.core/esm/worker/xworker.js
pyscript.core/types/

View File

@@ -3,10 +3,3 @@ ISSUE_TEMPLATE
package-lock.json package-lock.json
docs docs
examples/panel.html examples/panel.html
pyscript.core/micropython/
pyscript.core/pyscript/
pyscript.core/types/
pyscript.core/esm/worker/xworker.js
pyscript.core/cjs/package.json
pyscript.core/min.js
pyscript.core/pyscript.js

View File

@@ -1,13 +0,0 @@
{
"env": {
"browser": true,
"es2022": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"ignorePatterns": ["__template.js"],
"rules": {}
}

View File

@@ -1,30 +0,0 @@
# 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 }}

View File

@@ -1,10 +0,0 @@
.DS_Store
coverage/
node_modules/
cjs/
!cjs/package.json
core.js
pyscript.js
esm/worker/xworker.js
esm/worker/__template.js
types/

View File

@@ -1,21 +0,0 @@
.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
cjs/worker/__template.js
esm/worker/_template.js
esm/worker/__template.js

View File

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

View File

@@ -1,92 +1,5 @@
# @pyscript/core # @pyscript/core - deprecated
[![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) After various discussions around this topic, we decided to avoid any confusion around this folder which never really belonged in here, as _core_ as we meant it was meant to be a _PyScript Next_ dependency, not the _PyScript Next_ itself.
--- We have hence moved and renamed _core_ as [polyscript](https://github.com/pyscript/polyscript/#readme) which is the base module to build up a better _PyScript Next_ without confusing our users or ourselves while talking about these two distinct projects.
## Documentation
Please read [the documentation page](./docs/README.md) to know all the user-facing details around this module.
## Development
The working folder (source code of truth) is the `./esm` one, while the `./cjs` is populated as a dual module and to test (but it's 1:1 code, no transpilation except for imports/exports).
```sh
# install all dependencies needed by core
npm i
```
### Build / Artifacts
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 `core.js` or the `pyscript.js` file used by most integration tests
* create a sha256 version of the Blob content for CSP cases
Accordingly, to build latest project:
```sh
# create all artifacts needed to test core
npm run build
# optionally spin a server with CORS, COOP, and COEP enabled
npm run server
```
If **no minification** is desired or helpful while debugging potential issues, please use `NO_MIN=1` in front of the _build_ step:
```sh
NO_MIN=1 npm run build
npm run server
```
### Dev Build
Besides spinning the _localhost_ server via `npm run server`, the `npm run dev` will watch changes in the `./esm` folder and it will build automatically non optimized artifacts out of the box.
## Integration Tests
To keep it simple, and due to technical differences between what was in PyScript before and what we actually need for core (special headers, multiple interpreters, different bootstrap logic), core integration tests can be performed simply by running:
```sh
npm run test:integration
```
The package's entry takes care of eventually bootstrapping localhost, starting in parallel all tests, and shutting down the server after, if any was bootstrapped.
The tool to test integration is still _playwright_ but moves things a bit faster (from my side) tests are written in JS.
#### Integration Tests Structure
```
integration
├ interpreter
│ ├ micropython
│ ├ pyodide
│ ├ ruby-wasm-wasi
│ ├ wasmoon
│ ├ xxx.yy
│ ├ xxx.toml
│ └ utils.js
├ _shared.js
├ micropython.js
├ pyodide.js
├ ruby-wasm-wasi.js
└ wasmoon.js
```
- **interpreter** this folder contains, per each interpreter, a dedicated folder with the interpreter's name. Each of these sub-folders will contain all `.html` and other files to test every specific behavior. In this folder, it's possible to share files, config, or anything else that makes sense for one or more interpreters.
- **\_shared.js** contains some utility used across all tests. Any file prefixed with `_` (underscore) will be ignored for tests purposes but it can be used by the code itself.
- **micropython.js** and all others contain the actual test per each interpreter. If a test is the same across multiple interpreters it can be exported via the `_shared.js` file as it is for most _Pyodide_ and _MicroPython_ cases.
The [test/integration.spec.js](./test/integration.spec.js) file simply loops over folders that match interpreters _by name_ and execute in parallel all tests.
#### Manual Test
To **test manually** an integration test, simply `npm run server` and reach the _html_ file created for that particular test.
As example, reaching http://localhost:8080/test/integration/interpreter/micropython/fetch.html would log in the console and show expectations on the page and this can be easily tested via multiple browsers by simply reaching the very same integration test.

View File

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

View File

@@ -1,33 +0,0 @@
let queue = Promise.resolve();
const { exec } = require("node:child_process");
const build = (fileName) => {
if (fileName) console.log(fileName, "changed");
else console.log("building without optimizations");
queue = queue.then(
() =>
new Promise((resolve, reject) => {
exec(
"npm run rollup:xworker && npm run rollup:core && npm run rollup:pyscript",
{ cwd: __dirname, env: { ...process.env, NO_MIN: true } },
(error) => {
if (error) reject(error);
else
resolve(
console.log(fileName || "", "build completed"),
);
},
);
}),
);
};
const options = {
ignored: /\/(?:__template|interpreters|xworker)\.[mc]?js$/,
persistent: true,
};
require("chokidar").watch("./esm", options).on("change", build);
build();

View File

@@ -1,475 +0,0 @@
# PyScript Core Documentation
* [Terminology](#terminology) - what we mean by "_term_" in this document
* [Bootstrapping core](#bootstrapping-core) - how to enable PyScript Next in your page
* [How Scripts Work](#how-scripts-work) - how `<script type="...">` works
* [How Events Work](#how-events-work) - how `<button py-click="...">` works
* [XWorker](#xworker) - how `XWorker` class and its `xworker` reference work
* [Custom Scripts](#custom-scripts) - how *custom types* can be defined and used to enrich any core feature
## Terminology
This section goal is to avoid confusion around topics discussed in this document, describing each *term* as exhaustively as possible.
<details>
<summary><strong>Interpreter</strong></summary>
<div>
Also commonly referred as *runtime* or *engine*, we consider an **interpreter** any "_piece of software_" able to parse, understand, and ultimately execute, a *Programming Language* through this project.
We also explicitly use that "_piece of software_" as the interpreter name it refers to. We currently bundle references to four interpreters:
* [pyodide](https://pyodide.org/en/stable/index.html) is the name of the interpreter that runs likely the most complete version of latest *Python*, enabling dozen official modules at run time, also offering a great *JS* integration in its core
* [micropython](https://micropython.org/) is the name of the interpreter that runs a small subset of the *Python* standard library and is optimized to run in constrained environments such as *Mobile* phones, or even *Desktop*, thanks to its tiny size and an extremely fast bootstrap
* [wasmoon](https://github.com/ceifa/wasmoon) is the name of the interpreter that runs *Lua* on the browser and that, among the previous two interpreters, is fully compatible with all core features
* [ruby-wasm-wasi](https://github.com/ruby/ruby.wasm) is the name of the (currently *experimental*) interpreter that adds *Ruby* to the list of programming languages currently supported
`<script>` tags specify which *interpreter* to use via the `type` attribute. This is typically the full name of the interpreter:
```html
<script type="pyodide">
import sys
print(sys.version)
</script>
<script type="micropython">
import sys
print(sys.version)
</script>
<script type="wasmoon">
print(_VERSION)
</script>
<script type="ruby-wasm-wasi">
print "ruby #{ RUBY_VERSION }"
</script>
```
- Please note we decided on purpose to not use the generic programming language name instead of its interpreter project name to avoid being too exclusive for alternative projects that would like to target that very same Programming Language (i.e. note *pyodide* & *micropython* not using *python* indeed as interpreter name).
Custom values for the `type` attribute can also be created which alias (and potential build on top of) existing interpreter types. We include `<script type="py">` (and its `<py-script>` custom element counter-part) which use the Pyodide interpreter while extending its behavior in specific ways familiar to existing PyScript users (*the `<py-config>` tag, `<py-repl>`, etc*).
</div>
</details>
<details>
<summary><strong>Target</strong></summary>
<div>
When it comes to *strings* or *attributes*, we consider the **target** any valid element's *id* on the page or, in most cases, any valid *CSS* selector.
```html
<!-- - requires py-script custom type -->
<script type="py">
# target here is a string
display('Hello PyScript', target='output')
</script>
<div id="output">
<!-- will show "Hello PyScript" once the script executes -->
</div>
```
When it comes to the `property` or `field` attached to a `<script>` element though, that *id* or *selector* would already be resolved, so that such field would always point at the very same related element.
```html
<script type="micropython" target="output">
from js import document
document.currentScript.target.textContent = "Hello";
</script>
<div id="output">
<!-- will show "Hello" once the script executes -->
</div>
```
- Please note that if no `target` attribute is specified, the *script* will automatically create a "_companion element_" when the `target` property/field is accessed for the very first time:
```html
<script type="micropython">
from js import document
# will create a <script-micropython> element appended
# right after the currently executing script
document.currentScript.target.textContent = "Hello";
</script>
<!--
created during previous code execution
<script-micropython>Hello</script-micropython>
-->
```
</div>
</details>
<details>
<summary><strong>Env</strong></summary>
<div>
- This is an **advanced feature** that is worth describing but usually it is not needed for most common use cases.
Mostly due its terseness that plays nicely as attribute's suffix, among its commonly understood meaning, we consider an *env* an identifier that guarantee the used *interpreter* would always be the same and no other interpreters, even if they point at very same project, could interfere with globals, behavior, or what's not.
In few words, every single *env* would spawn a new interpreter dedicated to such env, and global variables defined elsewhere will not affect this "_environment_" and vice-versa, an *env* cannot dictate what will happen to other interpreters.
```html
<!-- default env per each interpreter -->
<script type="micropython">
shared = True
</script>
<script type="micropython">
# prints True - shared is global
print(shared)
</script>
<!-- dedicated interpreter -->
<script type="micropython" env="my-project-env">
# throws an error - shared doesn't exist
print(shared)
</script>
```
- Please note if the interpreter takes 1 second to bootstrap, multiple *environments* will take *that* second multiplied by the number of different environments, which is why this feature is considered for **advanced** use cases only and it should be discouraged as generic practice.
</div>
</details>
## Bootstrapping core
In order to have anything working at all in our pages, we need to at least bootstrap *@pyscript/core* functionalities, otherwise all examples and scripts mentioned in this document would just sit there ... sadly ignored by every browser:
```html
<!doctype html>
<html>
<head>
<!-- this is a way to automatically bootstrap @pyscript/core -->
<script type="module" src="https://esm.run/@pyscript/core"></script>
</head>
<body>
<script type="micropython">
from js import document
document.body.textContent = '@pyscript/core'
</script>
</body>
</html>
```
As *core* exposes some utility/API, using the following method would also work:
```html
<script type="module">
import {
define, // define a custom type="..."
whenDefined, // wait for a custom type to be defined
XWorker // allows JS <-> Interpreter communication
} from 'https://esm.run/@pyscript/core';
</script>
```
Please keep reading this document to understand how to use those utilities or how to have other *Pogramming Languages* enabled in your page via `<script>` elements.
## How Scripts Work
The [&lt;script&gt; element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script) has at least these extremely important peculiarities compared to any other element defined by the [HTML Standard](https://html.spec.whatwg.org/multipage/):
* its only purpose is to contain *data blocks*, meaning that browsers will never try to parse its content as generic *HTML* (and browsers will completely ignore either its content or its attributes, including the `src`, when its [type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type) is not known)
* its completely unobtrusive when it comes to both *aria* and *layout*, indeed it's one of the few nodes that can be declared almost anywhere without breaking its parent tree (other notable exception would be a comment node)
* for our specific use case, it already offers attributes that are historically well understood and known, also simplifying somehow the creation of this document
The long story short is that any `<script type="non-standard-type">` has zero issues with any browser of choice, but it's true that using some specific *custom type* might lead to future issues in case that `type` could have some special meaning for the future of the Web.
We encourage everyone to be careful when using this *core* API as we definitively don't want to clash or conflict, by any mean, with what the Web might need or offer in the near to far future, but we're also confident so far our current *types* are more than safe.
### Script Attributes
| name | example | behavior |
| :-------- | :-------------------------------------------- | :--------|
| async | `<script type="pyodide" async>` | The code is evaluated via `runAsync` utility where, if the *interpreter* allows it, top level *await* would be possible, among other *PL* specific asynchronous features. |
| config | `<script type="pyodide" config="./cfg.toml">` | The interpreter will load and parse the *JSON* or *TOML* file to configure itself. Please see [currently supported config values](https://docs.pyscript.net/latest/reference/elements/py-config.html#supported-configuration-values) as this is currently based on `<py-config>` features. |
| env | `<script type="pyodide" env="brand">` | Create, if not known yet, a dedicated *environment* for the specified `type`. Please read the [Terminology](#terminology) **env** dedicated details to know more. |
| src | `<script type="pyodide" src="./app.py">` | Fetch code from the specified `src` file, overriding or ignoring the content of the `<script>` itself, if any. |
| target | `<script type="pyodide" target="outcome">` | Describe as *id* or *CSS* selector the default *target* to use as `document.currentScript.target` field. Please read the [Terminology](#terminology) **target** dedicated details to know more. |
| type | `<script type="micropython">` | Define the *interpreter* to use with this script. Please read the [Terminology](#terminology) **interpreter** dedicated details to know more. |
| version | `<script type="pyodide" version="0.23.2">` | Allow the usage of a specific version where, if numeric, must be available through the project *CDN* used by *core* but if specified as fully qualified *URL*, allows usage of any interpreter's version: `<script type="pyodide" version="http://localhost:8080/pyodide.local.mjs">` |
### Script Features
These are all special, *script* related features, offered by *@pyscript/core* out of the box.
<details>
<summary><strong>document.currentScript</strong></summary>
<div>
No matter the interpreter of choice, if there is any way to reach the `document` from such interpreter, its `currentScript` will point at the exact/very-same script that is currently executing the code, even if its `async` attribute is used, mimicking what the standard [document.currentScript](https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript) offers already, and in an unobtrusive way for the rest of the page, as this property only exists for *synchronous* and blocking scripts that are running, hence never interfering with this *core* logic or vice-versa.
```html
<script type="micropython" id="my-target">
from js import document
# explicitly grab the current script as target
my_target = document.getElementById('my-target')
# verify it is the exact same node with same id
print(document.currentScript.id == my_target.id)
</script>
```
Not only this is helpful to crawl the surrounding *DOM* or *HTML*, every script will also have a `target` property that will point either to the element reachable through the `target` attribute, or it lazily creates once a companion element that will be appended right after the currently executing *script*.
Please read the [Terminology](#terminology) **target** dedicated details to know more.
</div>
</details>
<details>
<summary><strong>XWorker</strong></summary>
<div>
With or without access to the `document`, every (*non experimental*) interpreter will have defined, either at the global level or after an import (i.e.`from xworker import XWorker` in *Python* case), a reference to the `XWorker` "_class_" (it's just a *function*!), which goal is to enable off-loading heavy operations on a worker, without blocking the main / UI thread (the current page) and allowing such worker to even reach the `document` or anything else available on the very same main / UI thread.
```html
<script type="micropython">
from xworker import XWorker
print(XWorker != None)
</script>
```
Please read the [XWorker](#xworker) dedicated section to know more.
</div>
</details>
## How Events Work
The event should contain the *interpreter* or *custom type* prefix, followed by the *event* type it'd like to handle.
```html
<script type="micropython">
def print_type(event):
print(event.type)
</script>
<button micropython-click="print_type">
print type
</button>
```
Differently from *Web* inline events, there's no code evaluation at all within the attribute: it's just a globally available name that will receive the current event and nothing else.
#### The type-env attribute
Just as the `env` attribute on a `<script>` tag specifies a specific instance of an interpreter to use to run code, it is possible to use the `[type]-env` attribute to specify which instance of an interpreter or custom type should be used to run event code:
```html
<script type="micropython">
def log():
print(1)
</script>
<!-- note the env value -->
<script type="micropython" env="two">
# the button will log 2
def log():
print(2)
</script>
<!-- note the micropython-env value -->
<button
micropython-env="two"
micropython-click="log"
>
log
</button>
```
As mentioned before, this will work with `py-env` too, or any custom type defined out there.
## XWorker
Whenever computing relatively expensive stuff, such as a *matplot* image, or literally anything else that would take more than let's say 100ms to answer, running your *interpreter* of choice within a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is likely desirable, so that the main / UI thread won't block users' actions, listeners, or any other computation going on in these days highly dynamic pages.
`@pyscript/core` adds a functionality called `XWorker` to all of the interpreters it offers, which works in each language the way `Worker` does in JavaScript.
In each Interpreter, `XWorker` is either global reference or an import (i.e.`from xworker import XWorker` in *Python* case) module's utility, with a counter `xworker` (lower case) global reference, or an import (i.e.`from xworker import xworker` in *Python* case) module's utility, within the worker code.
In short, the `XWorker` utility is to help, without much thinking, to run any desired interpreter out of a *Worker*, enabling extra features on the *worker*'s code side.
### Enabling XWorker
We use the latest Web technologies to allow fast, non-blocking, yet synchronous like, operations from any non-experimental interpreter's worker, and the standard requires some special header to enable such technologies and, most importantly, the [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer).
There is an exhaustive [section](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements) around this topic but the *TL;DR* version is:
* to protect your page from undesired attacks, the `Cross-Origin-Opener-Policy` header should be present with the `same-origin` value
* to protect other sites from your pages' code, the `Cross-Origin-Embedder-Policy` header should be present with either the `credentialless` value (Chrome and Firefox browsers) or the `require-corp` one (Safari + other browsers)
* when the `Cross-Origin-Embedder-Policy` header is set with the `require-corp` value, the `Cross-Origin-Resource-Policy` header should also be available with [one of these options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy): `same-site`, `same-origin` or `cross-origin`
There are **alternative ways** to enable these headers for your site or local host, and [this script](https://github.com/gzuidhof/coi-serviceworker#readme) is just one of these, one that works with most free-hosting websites too.
### XWorker options
Before showing any example, it's important to understand how the offered API differs from Web standard *workers*:
| name | example | behavior |
| :-------- | :------------------------------------------------------- | :--------|
| async | `XWorker('./file.py', async=True)` | The worker code is evaluated via `runAsync` utility where, if the *interpreter* allows it, top level *await* would be possible, among other *PL* specific asynchronous features. |
| config | `XWorker('./file.py', config='./cfg.toml')` | The worker will load and parse the *JSON* or *TOML* file to configure itself. Please see [currently supported config values](https://docs.pyscript.net/latest/reference/elements/py-config.html#supported-configuration-values) as this is currently based on `<py-config>` features. |
| type | `XWorker('./file.py', type='pyodide')` | Define the *interpreter* to use with this worker which is, by default, the same one used within the running code. Please read the [Terminology](#terminology) **interpreter** dedicated details to know more. |
| version | `XWorker('./file.py', type='pyodide', version='0.23.2')` | Allow the usage of a specific version where, if numeric, must be available through the project *CDN* used by *core* but if specified as fully qualified *URL*, allows usage of any interpreter's version: `<script type="pyodide" version="http://localhost:8080/pyodide.local.mjs">` |
The returning *JS* reference to any `XWorker(...)` call is literally a `Worker` instance that, among its default API, have the extra following feature:
| name | example | behavior |
| :-------- | :--------------------------------- | :--------|
| sync | `sync = XWorker('./file.py').sync` | Allows exposure of callbacks that can be run synchronously from the worker file, even if the defined callback is *asynchronous*. This property is also available in the `xworker` reference. |
```python
sync = XWorker('./file.py').sync
def from_main(some, value):
# return something interesting from main
# or do anything else
print(some)
print(value)
sync.from_main = from_main
```
In the `xworker` counter part:
```python
# will log 1 and "two" in default stdout console
xworker.sync.from_main(1, "two")
```
### The xworker reference
The content of the file used to initialize any `XWorker` on the main thread can always reach the `xworker` counter part as globally available or as import (i.e.`from xworker import xworker` in *Python* case) module's utility.
Within a *Worker* execution context, the `xworker` exposes the following features:
| name | example | behavior |
| :------------ | :------------------------------------------| :--------|
| sync | `xworker.sync.from_main(1, "two")` | Executes the exposed `from_main` function in the main thread. Returns synchronously its result, if any. |
| window | `xworker.window.document.title = 'Worker'` | Differently from *pyodide* or *micropython* `import js`, this field allows every single possible operation directly in the main thread. It does not refer to the local `js` environment the interpreter might have decided to expose, it is a proxy to handle otherwise impossible operations in the main thread, such as manipulating the *DOM*, reading `localStorage` otherwise not available in workers, change location or anything else usually possible to do in the main thread. |
| isWindowProxy | `xworker.isWindowProxy(ref)` | **Advanced** - Allows introspection of *JS* references, helping differentiating between local worker references, and main thread global JS references. This is valid both for non primitive objects (array, dictionaries) as well as functions, as functions are also enabled via `xworker.window` in both ways: we can add a listener from the worker or invoke a function in the main. Please note that functions passed to the main thread will always be invoked asynchronously.
```python
print(xworker.window.document.title)
xworker.window.document.body.append("Hello Main Thread")
xworker.window.setTimeout(print, 100, "timers too")
```
- Please note that even if non blocking, if too many operations are orchestrated from a *worker*, instead of the *main* thread, the overall performance might still be slower due the communication channel and all the primitives involved in the synchronization process. Feel free to use the `window` feature as a great enabler for unthinkable or quick solutions but keep in mind it is still an indirection.
#### The `sync` utility
This helper does not interfere with the global context but it still ensure a function can be exposed form *main* and be used from *thread* and/or vice-versa.
```python
# main
def alert_user(message):
import js
js.alert(message)
w = XWorker('./file.py')
# expose the function to the thread
w.sync.alert_user = alert_user
# thread
if condition == None:
xworker.sync.alert_user('something wrong!')
```
## Custom Scripts
With `@pyscript/core` it is possible to extend any *interpreter*, allowing users or contributors to define their own `type` for the `<script>` they would like to augment with goodness or extra simplicity.
The *core* module itself exposes two methods to do so:
| name | example | behavior |
| :------------ | :------------------------ | :--------|
| define | `define('mpy', options)` | Register once a `<script type="mpy">` and a counter `<mpy-script>` selector that will bootstrap and handle all nodes in the page that match such selectors. The available `options` are described after this table. |
| whenDefined | `whenDefined('mpy')` | Return a promise that will be resolved once the custom `mpy` script will be available, returning an *interpreter* wrapper once it will be fully ready. |
```js
import { define, whenDefined } from '@pyscript/core';
define('mpy', {
interpreter: 'micropython',
// the rest of the custom type options
});
// an "mpy" dependent plugin for the "mpy" custom type
whenDefined("mpy").then(interpreterWrapper => {
// define or perform any task via the wrapper
})
```
### Custom Scripts Options
**Advanced** - Even if we strive to provide the easiest way for anyone to use core interpreters and features, the life cycle of a custom script might require any hook we also use internally to make `<script type="py">` possible, which is why this list is quite long, but hopefully exhaustive, and it covers pretty much everything we do internally as well.
The list of options' fields is described as such and all of these are *optional* while defining a custom type:
| name | example | behavior |
| :------------------------ | :-------------------------------------------- | :--------|
| version | `{verstion: '0.23.2'}` | Allow the usage of a specific version of an interpreter, same way `version` attribute works with `<script>` elements. |
| config | `{config: 'type.toml'}` | Ensure such config is already parsed and available for every custom `type` that execute code. |
| env | `{env: 'my-project'}` | Guarantee same environment for every custom `type`, avoiding conflicts with any other possible default or custom environment. |
| onInterpreterReady | `{onInterpreterReady(wrap, element) {}}` | This is the main entry point to define anything extra to the context of the always same interpreter. This callback is *awaited* and executed, after the desired *interpreter* is fully available and bootstrapped *once* though other optional fields, per each element that matches the defined `type`. The `wrap` reference contains many fields and utilities helpful to run most common operations, and it is passed along most other options too, when defined. |
| onBeforeRun | `{onBeforeRun(wrap, element) {}}` | This is a **hook** into the logic that runs right before any *interpreter* `run(...)` is performed. It receives the same `wrap` already sent when *onInterpreterReady* executes, and it passes along the current `element` that is going to execute such code. |
| onAfterRun | `{onAfterRun(wrap, element) {}}` | This is a **hook** into the logic that runs right after any *interpreter* `run(...)` is performed. It receives the same `wrap` already sent when *onInterpreterReady* executes, and it passes along the current `element` that already executed the code. |
| onBeforeRunAsync | `{onBeforeRunAsync(wrap, element) {}}` | This is a **hook** into the logic that runs right before any *interpreter* `runAsync(...)` is performed. It receives the same `wrap` already sent when *onInterpreterReady* executes, and it passes along the current `element` that is going to execute such code asynchronously. |
| onAfterRunAsync | `{onAfterRunAsync(wrap, element) {}}` | This is a **hook** into the logic that runs right after any *interpreter* `runAsync(...)` is performed. It receives the same `wrap` already sent when *onInterpreterReady* executes, and it passes along the current `element` that already executed the code asynchronously. |
| onWorkerReady | `{onWorkerReady(interpreter, xworker) {}}` | This is a **hook** into the logic that runs right before a new `XWorker` instance has been created in the **main** thread. It makes it possible to pre-define exposed `sync` methods to the `xworker` counter-part, enabling cross thread features out of the custom type without needing any extra effort. |
| codeBeforeRunWorker | `{codeBeforeRunWorker(){}}` | This is a **hook** into the logic that runs right before any *interpreter* `run(...)` is performed *within a worker*. Because all worker code is executed as `code`, this callback is expected to **return a string** that can be prepended for any worker synchronous operation. |
| codeAfterRunWorker | `{codeAfterRunWorker(){}}` | This is a **hook** into the logic that runs right after any *interpreter* `run(...)` is performed *within a worker*. Because all worker code is executed as `code`, this callback is expected to **return a string** that can be appended for any worker synchronous operation. |
| codeBeforeRunWorkerAsync | `{codeBeforeRunWorkerAsync(){}}` | This is a **hook** into the logic that runs right before any *interpreter* `runAsync(...)` is performed *within a worker*. Because all worker code is executed as `code`, this callback is expected to **return a string** that can be prepended for any worker asynchronous operation. |
| codeAfterRunWorkerAsync | `{codeAfterRunWorkerAsync(){}}` | This is a **hook** into the logic that runs right after any *interpreter* `runAsync(...)` is performed *within a worker*. Because all worker code is executed as `code`, this callback is expected to **return a string** that can be appended for any worker asynchronous operation. |
### Custom Scripts Wrappers
Almost every interpreter has its own way of doing the same thing needed for most common use cases, and with this in mind we abstracted most operations to allow a terser *core* for anyone to consume, granting that its functionalities are the same, no matter which interpreter one prefers.
There are also cases that are not tackled directly in *core*, but necessary to anyone trying to extend *core* as it is, so that some helper felt necessary to enable users and contributors as much as they want.
In few words, while every *interpreter* is literally passed along to unlock its potentials 100%, the most common details or operations we need in core are:
| name | example | behavior |
| :------------------------ | :-------------------------------------------- | :--------|
| type | `wrap.type` | Return the current `type` (interpreter or custom type) used in the current code execution. |
| interpreter | `wrap.interpreter` | Return the *interpreter* _AS-IS_ after being bootstrapped by the desired `config`. |
| XWorker | `wrap.XWorker` | Refer to the `XWorker` class available to the main thread code while executing. |
| io | `wrap.io` | Allow to lazily define different `stdout` or `stderr` via the running *interpreter*. This `io` field can be lazily defined and restored back for any element currently running the code. |
| config | `wrap.config` | It is the resolved *JSON* config and it is an own clone per each element running the code, usable also as "_state_" reference for the specific element, as changing it at run time will never affect any other element. |
| run | `wrap.run(code)` | It abstracts away the need to know the exact method name used to run code synchronously, whenever the *interpreter* allows such operation, facilitating future migrations from an interpreter to another. |
| runAsync | `wrap.runAsync(code)` | It abstracts away the need to know the exact method name used to run code asynchronously, whenever the *interpreter* allows such operation, facilitating future migrations from an interpreter to another. |
| runEvent | `wrap.runEvent(code, event)` | It abstracts away the need to know how an *interpreter* retrieves paths to execute an event handler. |
This is the `wrap` mentioned with most hooks and initializers previously described, and we're more than happy to learn if we are not passing along some extra helper.
### The io helper
```js
// change the default stdout while running code
wrap.io.stdout = (message) => {
console.log("🌑", wrap.type, message);
};
// change the default stderr while running code
wrap.io.stderr = (message) => {
console.error("🌑", wrap.type, message);
};
```

View File

@@ -1,191 +0,0 @@
import "@ungap/with-resolvers";
import { $$ } from "basic-devtools";
import { assign, create } from "./utils.js";
import { getDetails } from "./script-handler.js";
import {
registry as defaultRegistry,
prefixes,
configs,
} from "./interpreters.js";
import { getRuntimeID } from "./loader.js";
import { io } from "./interpreter/_utils.js";
import { addAllListeners } from "./listeners.js";
import { Hook } from "./worker/hooks.js";
export const CUSTOM_SELECTORS = [];
/**
* @typedef {Object} Runtime custom configuration
* @prop {object} interpreter the bootstrapped interpreter
* @prop {(url:string, options?: object) => Worker} XWorker an XWorker constructor that defaults to same interpreter on the Worker.
* @prop {object} config a cloned config used to bootstrap the interpreter
* @prop {(code:string) => any} run an utility to run code within the interpreter
* @prop {(code:string) => Promise<any>} runAsync an utility to run code asynchronously within the interpreter
* @prop {(path:string, data:ArrayBuffer) => void} writeFile an utility to write a file in the virtual FS, if available
*/
const types = new Map();
const waitList = new Map();
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
/**
* @param {Element} node any DOM element registered via define.
*/
export const handleCustomType = (node) => {
for (const selector of CUSTOM_SELECTORS) {
if (node.matches(selector)) {
const type = types.get(selector);
const { resolve } = waitList.get(type);
const { options, known } = registry.get(type);
if (!known.has(node)) {
known.add(node);
const {
interpreter: runtime,
version,
config,
env,
onInterpreterReady,
} = options;
const name = getRuntimeID(runtime, version);
const id = env || `${name}${config ? `|${config}` : ""}`;
const { interpreter: engine, XWorker: Worker } = getDetails(
runtime,
id,
name,
version,
config,
);
engine.then((interpreter) => {
const module = create(defaultRegistry.get(runtime));
const {
onBeforeRun,
onBeforeRunAsync,
onAfterRun,
onAfterRunAsync,
} = options;
const hooks = new Hook(interpreter, options);
const XWorker = function XWorker(...args) {
return Worker.apply(hooks, args);
};
// 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).
// 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;
};
}
// 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.registerJSModule(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),
runEvent: module.runEvent.bind(module, interpreter),
};
resolve(resolved);
onInterpreterReady?.(resolved, node);
});
}
}
}
};
/**
* @type {Map<string, {options:object, known:WeakSet<Element>}>}
*/
const registry = new Map();
/**
* @typedef {Object} CustomOptions custom configuration
* @prop {'pyodide' | 'micropython' | 'wasmoon' | 'ruby-wasm-wasi'} interpreter the interpreter to use
* @prop {string} [version] the optional interpreter version to use
* @prop {string} [config] the optional config to use within such interpreter
* @prop {(environment: object, node: Element) => void} [onInterpreterReady] the callback that will be invoked once
*/
/**
* Allows custom types and components on the page to receive interpreters to execute any code
* @param {string} type the unique `<script type="...">` identifier
* @param {CustomOptions} options the custom type configuration
*/
export const define = (type, options) => {
if (defaultRegistry.has(type) || registry.has(type))
throw new Error(`<script type="${type}"> already registered`);
if (!defaultRegistry.has(options?.interpreter))
throw new Error(`Unspecified interpreter`);
// allows reaching out the interpreter helpers on events
defaultRegistry.set(type, defaultRegistry.get(options?.interpreter));
// ensure a Promise can resolve once a custom type has been bootstrapped
whenDefined(type);
// allows selector -> registry by type
const selectors = [`script[type="${type}"]`, `${type}-script`];
for (const selector of selectors) types.set(selector, type);
CUSTOM_SELECTORS.push(...selectors);
prefixes.push(`${type}-`);
// ensure always same env for this custom type
registry.set(type, {
options: assign({ env: type }, options),
known: new WeakSet(),
});
addAllListeners(document);
$$(selectors.join(",")).forEach(handleCustomType);
};
/**
* Resolves whenever a defined custom type is bootstrapped on the page
* @param {string} type the unique `<script type="...">` identifier
* @returns {Promise<object>}
*/
export const whenDefined = (type) => {
if (!waitList.has(type)) waitList.set(type, Promise.withResolvers());
return waitList.get(type).promise;
};
/* c8 ignore stop */

View File

@@ -1,191 +0,0 @@
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() {
return [...hooks.codeBeforeRunWorker].join("\n");
},
codeAfterRunWorker() {
return [...hooks.codeAfterRunWorker].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 onInterpreterReady(pyodide, element) {
// allows plugins to do whatever they want with the element
// before regular stuff happens in here
for (const callback of hooks.onInterpreterReady)
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>} */
onInterpreterReady: 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

@@ -1,80 +0,0 @@
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

@@ -1,63 +0,0 @@
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

@@ -1,8 +0,0 @@
/** @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();

View File

@@ -1,68 +0,0 @@
import { $$ } from "basic-devtools";
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.js";
import { listener, addAllListeners } from "./listeners.js";
export { define, whenDefined } from "./custom.js";
export const XWorker = xworker();
const INTERPRETER_SELECTORS = selectors.join(",");
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.lastIndexOf("-") + 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) {
addAllListeners(node);
if (node.matches(INTERPRETER_SELECTORS)) handle(node);
else {
$$(INTERPRETER_SELECTORS, node).forEach(handle);
if (!CUSTOM_SELECTORS.length) continue;
handleCustomType(node);
$$(CUSTOM_SELECTORS.join(","), node).forEach(
handleCustomType,
);
}
}
}
}
});
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));
},
});
addAllListeners(observe(document));
$$(INTERPRETER_SELECTORS, document).forEach(handle);

View File

@@ -1,23 +0,0 @@
import { clean } from "./_utils.js";
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
export const registerJSModule = (interpreter, name, value) => {
interpreter.registerJsModule(name, value);
};
export const run = (interpreter, code) => interpreter.runPython(clean(code));
export const runAsync = (interpreter, code) =>
interpreter.runPythonAsync(clean(code));
export const runEvent = async (interpreter, code, event) => {
// allows method(event) as well as namespace.method(event)
// it does not allow fancy brackets names for now
const [name, ...keys] = code.split(".");
let target = interpreter.globals.get(name);
let context;
for (const key of keys) [context, target] = [target, target[key]];
await target.call(context, event);
};
/* c8 ignore stop */

View File

@@ -1,135 +0,0 @@
import "@ungap/with-resolvers";
import { getBuffer } from "../fetch-utils.js";
import { absoluteURL } from "../utils.js";
/**
* Trim code only if it's a single line that prettier or other tools might have modified.
* @param {string} code code that might be a single line
* @returns {string}
*/
export const clean = (code) =>
code.replace(/^[^\r\n]+$/, (line) => line.trim());
// 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 interpreter = await engine;
io.set(interpreter, localIO);
return interpreter;
},
};
};
// This should be the only helper needed for all Emscripten based FS exports
export const writeFile = ({ FS, PATH, PATH_FS }, path, buffer) => {
const absPath = PATH_FS.resolve(path);
FS.mkdirTree(PATH.dirname(absPath));
return FS.writeFile(absPath, new Uint8Array(buffer), {
canOwn: true,
});
};
/* c8 ignore stop */
// This is instead a fallback for Lua or others
export const writeFileShim = (FS, path, buffer) => {
mkdirTree(FS, dirname(path));
path = resolve(FS, 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("/")) {
if (branch === ".") continue;
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, interpreter, config_fetch) =>
all(
calculateFetchPaths(config_fetch).map(({ url, path }) =>
fetchResolved(config_fetch, url)
.then(getBuffer)
.then((buffer) => module.writeFile(interpreter, path, buffer)),
),
);
/* c8 ignore stop */

View File

@@ -1,26 +0,0 @@
import { fetchPaths, stdio, writeFile } from "./_utils.js";
import { registerJSModule, run, runAsync, runEvent } from "./_python.js";
const type = "micropython";
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
export default {
type,
module: (version = "1.20.0-297") =>
`https://cdn.jsdelivr.net/npm/@micropython/micropython-webassembly-pyscript@${version}/micropython.mjs`,
async engine({ loadMicroPython }, config, url) {
const { stderr, stdout, get } = stdio();
url = url.replace(/\.m?js$/, ".wasm");
const interpreter = await get(loadMicroPython({ stderr, stdout, url }));
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
return interpreter;
},
registerJSModule,
run,
runAsync,
runEvent,
writeFile: ({ FS, _module: { PATH, PATH_FS } }, path, buffer) =>
writeFile({ FS, PATH, PATH_FS }, path, buffer),
};
/* c8 ignore stop */

View File

@@ -1,34 +0,0 @@
import { fetchPaths, stdio, writeFile } from "./_utils.js";
import { registerJSModule, run, runAsync, runEvent } from "./_python.js";
const type = "pyodide";
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
export default {
type,
module: (version = "0.23.2") =>
`https://cdn.jsdelivr.net/pyodide/v${version}/full/pyodide.mjs`,
async engine({ loadPyodide }, config, url) {
const { stderr, stdout, get } = stdio();
const indexURL = url.slice(0, url.lastIndexOf("/"));
const interpreter = await get(
loadPyodide({ stderr, stdout, indexURL }),
);
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
if (config.packages) {
await interpreter.loadPackage("micropip");
const micropip = await interpreter.pyimport("micropip");
await micropip.install(config.packages);
micropip.destroy();
}
return interpreter;
},
registerJSModule,
run,
runAsync,
runEvent,
writeFile: ({ FS, PATH, _module: { PATH_FS } }, path, buffer) =>
writeFile({ FS, PATH, PATH_FS }, path, buffer),
};
/* c8 ignore stop */

View File

@@ -1,61 +0,0 @@
import { clean, fetchPaths } from "./_utils.js";
import { entries } from "../utils.js";
const type = "ruby-wasm-wasi";
const jsType = type.replace(/\W+/g, "_");
// 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 */
export default {
type,
experimental: true,
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: interpreter } = await DefaultRubyVM(module);
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
return interpreter;
},
// Fallback to globally defined module fields (i.e. $xworker)
registerJSModule(interpreter, _, value) {
const code = ['require "js"'];
for (const [k, v] of entries(value)) {
const id = `__module_${jsType}_${k}`;
globalThis[id] = v;
code.push(`$${k}=JS.global[:${id}]`);
}
this.run(interpreter, code.join(";"));
},
run: (interpreter, code) => interpreter.eval(clean(code)),
runAsync: (interpreter, code) => interpreter.evalAsync(clean(code)),
async runEvent(interpreter, code, event) {
// patch common xworker.onmessage/onerror cases
if (/^xworker\.(on\w+)$/.test(code)) {
const { $1: name } = RegExp;
const id = `__module_${jsType}_event`;
globalThis[id] = event;
this.run(
interpreter,
`require "js";$xworker.call("${name}",JS.global[:${id}])`,
);
delete globalThis[id];
} else {
// Experimental: allows only events by fully qualified method name
const method = this.run(interpreter, `method(:${code})`);
await method.call(code, interpreter.wrap(event));
}
},
writeFile: () => {
throw new Error(`writeFile is not supported in ${type}`);
},
};
/* c8 ignore stop */

View File

@@ -1,51 +0,0 @@
import { clean, fetchPaths, stdio, writeFileShim } from "./_utils.js";
import { entries } from "../utils.js";
const type = "wasmoon";
// MISSING:
// * I've no idea how to import packages
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
export default {
type,
module: (version = "1.15.0") =>
`https://cdn.jsdelivr.net/npm/wasmoon@${version}/+esm`,
async engine({ LuaFactory, LuaLibraries }, config) {
const { stderr, stdout, get } = stdio();
const interpreter = await get(new LuaFactory().createEngine());
interpreter.global.getTable(LuaLibraries.Base, (index) => {
interpreter.global.setField(index, "print", stdout);
interpreter.global.setField(index, "printErr", stderr);
});
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
return interpreter;
},
// Fallback to globally defined module fields
registerJSModule: (interpreter, _, value) => {
for (const [k, v] of entries(value)) interpreter.global.set(k, v);
},
run: (interpreter, code) => interpreter.doStringSync(clean(code)),
runAsync: (interpreter, code) => interpreter.doString(clean(code)),
runEvent: async (interpreter, code, event) => {
// allows method(event) as well as namespace.method(event)
// it does not allow fancy brackets names for now
const [name, ...keys] = code.split(".");
let target = interpreter.global.get(name);
let context;
for (const key of keys) [context, target] = [target, target[key]];
await target.call(context, event);
},
writeFile: (
{
cmodule: {
module: { FS },
},
},
path,
buffer,
) => writeFileShim(FS, path, buffer),
};
/* c8 ignore stop */

View File

@@ -1,58 +0,0 @@
// ⚠️ Part of this file is automatically generated
// The :RUNTIMES comment is a delimiter and no code should be written/changed after
// See rollup/build_interpreters.cjs to know more
import { base } from "./interpreter/_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 interpreter = new Proxy(new Map(), {
get(map, id) {
if (!map.has(id)) {
const [type, ...rest] = id.split("@");
const interpreter = registry.get(type);
const url = /^https?:\/\//i.test(rest)
? rest.join("@")
: interpreter.module(...rest);
map.set(id, {
url,
module: import(url),
engine: interpreter.engine.bind(interpreter),
});
}
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 = (interpreter) => {
for (const type of [].concat(interpreter.type)) {
registry.set(type, interpreter);
selectors.push(`script[type="${type}"]`);
prefixes.push(`${type}-`);
}
};
//:RUNTIMES
import micropython from "./interpreter/micropython.js";
import pyodide from "./interpreter/pyodide.js";
import ruby_wasm_wasi from "./interpreter/ruby-wasm-wasi.js";
import wasmoon from "./interpreter/wasmoon.js";
for (const interpreter of [micropython, pyodide, ruby_wasm_wasi, wasmoon])
register(interpreter);

View File

@@ -1,63 +0,0 @@
import { $x } from "basic-devtools";
import { interpreters } from "./script-handler.js";
import { all, create, defineProperty } from "./utils.js";
import { registry, prefixes } from "./interpreters.js";
// TODO: this is ugly; need to find a better way
defineProperty(globalThis, "pyscript", {
value: {
env: new Proxy(create(null), {
get: (_, name) => awaitInterpreter(name),
}),
},
});
/* c8 ignore start */ // attributes are tested via integration / e2e
// ensure both interpreter and its queue are awaited then returns the interpreter
const awaitInterpreter = async (key) => {
if (interpreters.has(key)) {
const { interpreter, queue } = interpreters.get(key);
return (await all([interpreter, queue]))[0];
}
const available = interpreters.size
? `Available interpreters are: ${[...interpreters.keys()]
.map((r) => `"${r}"`)
.join(", ")}.`
: `There are no interpreters in this page.`;
throw new Error(`The interpreter "${key}" was not found. ${available}`);
};
export 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 interpreter = await awaitInterpreter(
el.getAttribute(`${name}-env`) || name,
);
const handler = registry.get(name);
handler.runEvent(interpreter, value, event);
}
};
/**
* Look for known prefixes and add related listeners.
* @param {Document | Element} root
*/
export const addAllListeners = (root) => {
for (let { name, ownerElement: el } of $x(
`.//@*[${prefixes
.map((p) => `starts-with(name(),"${p}")`)
.join(" or ")}]`,
root,
)) {
name = name.slice(name.lastIndexOf("-") + 1);
if (name !== "env") el.addEventListener(name, listener);
}
};
/* c8 ignore stop */

View File

@@ -1,42 +0,0 @@
import { interpreter } from "./interpreters.js";
import { absoluteURL, resolve } from "./utils.js";
import { parse } from "./toml.js";
import { getJSON, getText } from "./fetch-utils.js";
/**
* @param {string} id the interpreter 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);
config = absoluteURL(config);
} else if (config.endsWith(".toml")) {
options = fetch(config).then(getText).then(parse);
config = absoluteURL(config);
} 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) => interpreter[id](options, config));
};
/**
* @param {string} type the interpreter type
* @param {string} [version] the optional interpreter version
* @returns
*/
export const getRuntimeID = (type, version = "") =>
`${type}@${version}`.replace(/@$/, "");

View File

@@ -1,135 +0,0 @@
import { $ } from "basic-devtools";
import xworker from "./worker/class.js";
import { getRuntime, getRuntimeID } from "./loader.js";
import { registry } from "./interpreters.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;
};
export 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 interpreters = 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} interpreter is experimental`);
const [interpreter, content] = await all([
handled.get(script).interpreter,
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(document, "currentScript", {
configurable: true,
get: () => script,
});
module.registerJSModule(interpreter, "xworker", { XWorker });
return module[isAsync ? "runAsync" : "run"](interpreter, content);
} finally {
delete document.currentScript;
}
};
const getValue = (ref, prefix) => {
const value = ref?.value;
return value ? prefix + value : "";
};
export const getDetails = (type, id, name, version, config) => {
if (!interpreters.has(id)) {
const details = {
interpreter: getRuntime(name, config),
queue: resolve(),
XWorker: xworker(type, version),
};
interpreters.set(id, details);
// enable sane defaults when single interpreter *of kind* is used in the page
// this allows `xxx-*` attributes to refer to such interpreter without `env` around
if (!interpreters.has(type)) interpreters.set(type, details);
}
return interpreters.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 interpreter,
// and/or source code with different config or interpreter
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

@@ -1,8 +0,0 @@
// lazy TOML parser (fast-toml might be a better alternative)
const TOML_LIB = `https://cdn.jsdelivr.net/npm/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

@@ -1,21 +0,0 @@
const { isArray } = Array;
const { assign, create, defineProperties, defineProperty, entries } = 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,
defineProperties,
defineProperty,
entries,
all,
resolve,
absoluteURL,
};

View File

@@ -1,107 +0,0 @@
// ⚠️ 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 * as JSON from "@ungap/structured-clone/json";
import coincident from "coincident/window";
import { create } from "../utils.js";
import { registry } from "../interpreters.js";
import { getRuntime, getRuntimeID } from "../loader.js";
// bails out out of the box with a native/meaningful error
// in case the SharedArrayBuffer is not available
try {
new SharedArrayBuffer(4);
} catch (_) {
throw new Error(
[
"Unable to use SharedArrayBuffer due insecure environment.",
"Please read requirements in MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements",
].join("\n"),
);
}
let interpreter, runEvent;
const add = (type, fn) => {
addEventListener(
type,
fn ||
(async (event) => {
try {
await interpreter;
runEvent(`xworker.on${type}`, event);
} catch (error) {
postMessage(error);
}
}),
!!fn && { once: true },
);
};
const { proxy: sync, window, isWindowProxy } = coincident(self, JSON);
const xworker = {
// allows synchronous utilities between this worker and the main thread
sync,
// allow access to the main thread world
window,
// allow introspection for foreign (main thread) refrences
isWindowProxy,
// standard worker related events / features
onerror() {},
onmessage() {},
onmessageerror() {},
postMessage: postMessage.bind(self),
};
add("message", ({ data: { options, code, hooks } }) => {
interpreter = (async () => {
try {
const { type, version, config, async: isAsync } = options;
const interpreter = await getRuntime(
getRuntimeID(type, version),
config,
);
const details = create(registry.get(type));
const name = `run${isAsync ? "Async" : ""}`;
if (hooks) {
// patch code if needed
const { beforeRun, beforeRunAsync, afterRun, afterRunAsync } =
hooks;
const after = afterRun || afterRunAsync;
const before = beforeRun || beforeRunAsync;
// append code that should be executed *after* first
if (after) {
const method = details[name].bind(details);
details[name] = (interpreter, code) =>
method(interpreter, `${code}\n${after}`);
}
// prepend code that should be executed *before* (so that after is post-patched)
if (before) {
const method = details[name].bind(details);
details[name] = (interpreter, code) =>
method(interpreter, `${before}\n${code}`);
}
}
// set the `xworker` global reference once
details.registerJSModule(interpreter, "xworker", { xworker });
// simplify runEvent calls
runEvent = details.runEvent.bind(details, interpreter);
// run either sync or async code in the worker
await details[name](interpreter, code);
return interpreter;
} catch (error) {
postMessage(error);
}
})();
add("error");
add("message");
add("messageerror");
});

View File

@@ -1,64 +0,0 @@
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 { Hook } from "./hooks.js";
/**
* @typedef {Object} WorkerOptions custom configuration
* @prop {string} type the interpreter type to use
* @prop {string} [version] the optional interpreter version to use
* @prop {string} [config] the optional config to use within such interpreter
*/
export default (...args) =>
/**
* A XWorker is a Worker facade able to bootstrap a channel with any desired interpreter.
* @param {string} url the remote file to evaluate on bootstrap
* @param {WorkerOptions} [options] optional arguments to define the interpreter to use
* @returns {Worker}
*/
function XWorker(url, options) {
const worker = xworker();
const { postMessage } = worker;
const isHook = this instanceof Hook;
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) => {
const hooks = isHook ? this.stringHooks : void 0;
postMessage.call(worker, { options, code, hooks });
});
defineProperties(worker, {
postMessage: {
value: (data, ...rest) =>
bootstrap.then(() =>
postMessage.call(worker, data, ...rest),
),
},
sync: {
value: coincident(worker, JSON).proxy,
},
});
if (isHook) this.onWorkerReady?.(this.interpreter, worker);
worker.addEventListener("message", (event) => {
if (event.data instanceof Error) {
event.stopImmediatePropagation();
worker.onerror(event);
}
});
return worker;
};

View File

@@ -1,24 +0,0 @@
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
const workerHooks = [
["beforeRun", "codeBeforeRunWorker"],
["beforeRunAsync", "codeBeforeRunWorkerAsync"],
["afterRun", "codeAfterRunWorker"],
["afterRunAsync", "codeAfterRunWorkerAsync"],
];
export class Hook {
constructor(interpreter, options) {
this.interpreter = interpreter;
this.onWorkerReady = options.onWorkerReady;
for (const [key, value] of workerHooks) this[key] = options[value]?.();
}
get stringHooks() {
const hooks = {};
for (const [key] of workerHooks) {
if (this[key]) hooks[key] = this[key];
}
return hooks;
}
}
/* c8 ignore stop */

View File

@@ -1,41 +0,0 @@
<!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/worker/input.html">worker w/ input</a></li>
<li><a href="./test/order.html">execution order</a></li>
<li><a href="./test/plugins/">plugins</a></li>
<li>
<a href="./test/plugins/py-script.html">
custom tags: PyScript
</a>
</li>
</ul>
</body>
</html>

View File

@@ -1,3 +0,0 @@
# 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

View File

@@ -1,8 +0,0 @@
{
"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-297/micropython.mjs": "./test/mocked/micropython.mjs",
"https://cdn.jsdelivr.net/npm/basic-toml@0.3.1/es.js": "./test/mocked/toml.mjs"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,74 +0,0 @@
{
"name": "@pyscript/core",
"version": "0.0.11",
"description": "PyScript Next core",
"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:core && npm run rollup:pyscript && eslint esm/ && npm run ts && npm run cjs",
"cjs": "ascjs --no-default esm cjs",
"dev": "node dev.cjs",
"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 && npm run test:integration",
"test:html": "npm run test && c8 report -r html",
"test:integration//": "Don't bother with spinning servers. Trap the tests EXIT_CODE. Kill the running server, if any. Return the EXIT_CODE to eventually throw an error.",
"test:integration": "static-handler --cors --coep --coop --corp . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/ || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
"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 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 ."
},
"keywords": [
"py-script",
"pyscript",
"next"
],
"author": "Anaconda Inc.",
"license": "MIT",
"devDependencies": {
"@node-loader/import-maps": "^1.1.0",
"@playwright/test": "^1.35.1",
"@rollup/plugin-node-resolve": "^15.1.0",
"@rollup/plugin-terser": "^0.4.3",
"ascjs": "^5.0.1",
"c8": "^8.0.0",
"chokidar": "^3.5.3",
"eslint": "^8.43.0",
"linkedom": "^0.14.26",
"rollup": "^3.25.3",
"static-handler": "^0.4.2",
"typescript": "^5.1.3"
},
"module": "./esm/index.js",
"type": "module",
"exports": {
".": {
"types": "./types/esm/index.d.ts",
"import": "./esm/index.js",
"default": "./cjs/index.js"
},
"./py": {
"import": "./pyscript.js",
"default": "./cjs/custom/pyscript.js"
},
"./py-script": {
"import": "./esm/custom/pyscript.js",
"default": "./cjs/custom/pyscript.js"
},
"./package.json": "./package.json"
},
"unpkg": "core.js",
"dependencies": {
"@ungap/structured-clone": "^1.2.0",
"@ungap/with-resolvers": "^0.1.0",
"basic-devtools": "^0.1.6",
"coincident": "^0.11.1"
},
"worker": {
"blob": "sha256-YJ2S61l39eRltU0ldf3Q7CfCASBa8FrQLLsfXUSzBHc="
}
}

View File

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

View File

@@ -1,334 +0,0 @@
/* 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

@@ -1,42 +0,0 @@
// ⚠️ This files modifies at build time esm/interpreters.js so that
// it's impossible to forget to export a interpreter from esm/interpreter folder.
const { join, resolve } = require("node:path");
const { readdirSync, readFileSync, writeFileSync } = require("node:fs");
const RUNTIMES_DIR = resolve(join(__dirname, "..", "esm", "interpreter"));
const RUNTIMES_JS = resolve(join(__dirname, "..", "esm", "interpreters.js"));
const createRuntimes = () => {
const interpreters = [];
for (const file of readdirSync(RUNTIMES_DIR)) {
// ignore files starting with underscore
if (/^[a-z].+?\.js/.test(file)) interpreters.push(file.slice(0, -3));
}
// generate the output to append at the end of the file
const output = [];
for (let i = 0; i < interpreters.length; i++) {
const interpreter = interpreters[i].replace(/-/g, "_");
output.push(
`import ${interpreter.replace(/-/g, "_")} from "./interpreter/${
interpreters[i]
}.js";`,
);
interpreters[i] = interpreter;
}
output.push(
`
for (const interpreter of [${interpreters.join(", ")}])
register(interpreter);
`.trim(),
);
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

@@ -1,40 +0,0 @@
// ⚠️ 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("__")) {
if (process.env.NO_MIN) {
writeFileSync(
join(WORKERS_DIR, "xworker.js"),
`/* c8 ignore next */\nexport default () => new Worker('/esm/worker/__template.js',{type:'module'});`,
);
} else {
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, " ") + "\n",
);
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

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

View File

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

View File

@@ -1,17 +0,0 @@
const { writeFileShim } = require("../cjs/interpreter/_utils.js");
const assert = require("./assert.js");
const FS = {
mkdir(...args) {
this.mkdir_args = args;
},
cwd: () => __dirname,
writeFile(...args) {
this.writeFile_args = args;
},
};
// REQUIRE INTEGRATION TESTS
writeFileShim(FS, "./test/abc.js", []);
writeFileShim(FS, "/./../abc.js", []);

View File

@@ -1,2 +0,0 @@
x = "hello from A"
print(x)

View File

@@ -1,10 +0,0 @@
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

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Lights Camera Action</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script type="module" src="../../core.js"></script>
</head>
<body>
<script type="pyodide" src="./main.py"></script>
<script type="micropython" src="./main.py"></script>
<p>Open the console.</p>
<button pyodide-click="click_handler">Pyodide Click</button>
<button micropython-click="click_handler">MicroPython Click</button>
</body>
</html>

View File

@@ -1,5 +0,0 @@
import js
async def click_handler(e):
js.console.log(e)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
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

@@ -1,50 +0,0 @@
<!doctype html>
<html>
<head>
<!-- this is a way to automatically bootstrap @pyscript/core -->
<script type="module" src="https://esm.run/@pyscript/core"></script>
</head>
<body>
<script type="micropython" id="my-target">
from js import document
# explicitly grab the current script as target
my_target = document.getElementById('my-target')
# verify it is the exact same node with same id
print(document.currentScript.id == my_target.id)
</script>
<script type="micropython">
from xworker import XWorker
print(XWorker != None)
</script>
<script type="micropython">
def print_type(event, double):
# logs "click 4"
print(f"{event.type} {double(2)}")
</script>
<button micropython-click="print_type(event, lambda x: x * 2)">
print type
</button>
<script type="micropython">
def log():
print(1)
</script>
<!-- note the env value -->
<script type="micropython" env="two">
def log():
print(2)
</script>
<!-- note the micropython-env value -->
<button
micropython-env="two"
micropython-click="log()"
>
log
</button>
<script type="micropython">
from js import document
document.body.append('@pyscript/core')
</script>
</body>
</html>

View File

@@ -1,36 +0,0 @@
<!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="../core.js"></script>
</head>
<body>
<script type="micropython">
import sys
import js
version = sys.version
js.document.currentScript.target.textContent = version
</script>
<hr />
<script type="micropython">
import js
js.document.currentScript.target.textContent = version
</script>
<hr />
<script type="pyodide">
import sys
import js
version = sys.version
js.document.currentScript.target.textContent = version
</script>
<hr />
<script type="pyodide" target="last-script-target">
import js
js.document.currentScript.target.textContent = version
</script>
<div id="last-script-target"></div>
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
</head>
<body>
<div>See console ➡️</div>
<script type="micropython">
from xworker import XWorker
def handle_error(event):
print("Python")
print(event.data.message)
w = XWorker('./error.py')
w.onerror = handle_error
</script>
<script type="module">
import { XWorker } from "../core.js";
const w = new XWorker("./error.py", { type: "micropython" });
w.onerror = ({ data: error }) => {
console.log('JS', error);
};
</script>
</body>
</html>

View File

@@ -1,3 +0,0 @@
# from xworker import xworker
xworker.postMessage("error")

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,17 +0,0 @@
<!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="../core.js"></script>
</head>
<body>
<script type="pyodide">
import sys
import js
js.document.currentScript.target.textContent = sys.version
</script>
</body>
</html>

View File

@@ -1,209 +0,0 @@
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: () => [] };
}
};
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="pyodide" version="0.23.2">${content}</script>`;
await tick();
assert(pyodide.content, content);
assert(pyodide.target.tagName, "PYODIDE-SCRIPT");
},
async function basicExpectations() {
document.head.innerHTML = `<script type="pyodide">${content}</script>`;
await tick();
assert(pyodide.content, content);
assert(pyodide.target.tagName, "PYODIDE-SCRIPT");
},
async function foreignRuntime() {
document.head.innerHTML = `<script type="pyodide" version="http://pyodide">${content}</script>`;
await tick();
assert(pyodide.content, content);
assert(pyodide.target.tagName, "PYODIDE-SCRIPT");
},
async function basicMicroPython() {
document.head.innerHTML = `<script type="micropython">${content}</script>`;
await tick();
assert(micropython.content, content);
assert(micropython.target.tagName, "MICROPYTHON-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="pyodide" 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="pyodide"></script>
`.trim();
await tick();
assert(pyodide.target, div);
},
async function explicitTargetAsString() {
setTarget("my-plugin");
shadowRoot.innerHTML = `
<my-plugin></my-plugin>
<script type="pyodide"></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="pyodide"></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="pyodide"></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="pyodide" 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 pyodide = await pyscript.env.pyodide;
const keys = Object.keys(loadPyodide()).join(",");
assert(Object.keys(pyodide).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

@@ -1,19 +0,0 @@
const { existsSync, readdirSync } = require('node:fs');
const { join } = require('node:path');
const playwright = require('@playwright/test');
// integration tests for interpreters
const TEST_INTERPRETER = join(__dirname, 'integration');
// source of truth for interpreters
const CORE_INTERPRETER = join(__dirname, '..', 'esm', 'interpreter');
for (const file of readdirSync(TEST_INTERPRETER)) {
// filter only JS files that match their counter-part interpreter
if (/\.js$/.test(file) && existsSync(join(CORE_INTERPRETER, file))) {
require(join(TEST_INTERPRETER, file))(
playwright,
`http://localhost:8080/test/integration/interpreter/${file.slice(0, -3)}`
);
}
}

View File

@@ -1,46 +0,0 @@
'use strict';
exports.shared = {
bootstrap: ({ expect }, baseURL) => async ({ page }) => {
await page.goto(`${baseURL}/bootstrap.html`);
await page.waitForSelector('html.ready');
await page.getByRole('button').click();
const result = await page.evaluate(() => document.body.innerText);
await expect(result.trim()).toBe('OK');
},
worker: ({ expect }, url) => async ({ page }) => {
const logs = [];
page.on('console', msg => logs.push(msg.text()));
await page.goto(url);
await page.waitForSelector('html.worker.ready');
await expect(logs.join(',')).toBe('main,thread');
},
workerWindow: ({ expect }, baseURL) => async ({ page }) => {
await page.goto(`${baseURL}/worker-window.html`);
await page.waitForSelector('html.worker.ready');
const result = await page.evaluate(() => document.body.innerText);
await expect(result.trim()).toBe('OK');
},
};
exports.python = {
bootstrap: ({ expect }, baseURL) => async ({ page }) => {
await page.goto(`${baseURL}/bootstrap.html`);
await page.waitForSelector('html.ready');
const result = await page.evaluate(() => document.body.innerText);
await expect(result.trim()).toBe('OK');
},
fetch: ({ expect }, url) => async ({ page }) => {
const logs = [];
page.on('console', msg => logs.push(msg.text()));
await page.goto(url);
await page.waitForSelector('html.ready');
await expect(logs.length).toBe(1);
await expect(logs[0]).toBe('hello from A');
const body = await page.evaluate(() => document.body.innerText);
await expect(body.trim()).toBe('hello from A, hello from B');
},
};

View File

@@ -1,2 +0,0 @@
x = "hello from A"
print(x)

View File

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

View File

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

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('micropython');
</script>
</head>
<body>
<script type="micropython">
import js
js.document.currentScript.target.textContent = "OK"
</script>
</body>
</html>

View File

@@ -1,18 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('micropython');
</script>
</head>
<body>
<script type="micropython" config="../fetch.toml">
import js
import a, b
js.document.currentScript.target.textContent = a.x + ", " + b.x
</script>
</body>
</html>

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('micropython');
</script>
</head>
<body>
<script type="micropython">
from xworker import XWorker
def handle_message(event):
print(event.data)
w = XWorker('../wasmoon/worker.lua', type='wasmoon')
w.postMessage('main')
w.onmessage = handle_message
</script>
</body>
</html>

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('micropython');
</script>
</head>
<body>
<script type="micropython">
from xworker import XWorker
XWorker('./worker-window.py')
</script>
</body>
</html>

View File

@@ -1,9 +0,0 @@
from xworker import xworker
document = xworker.window.document
document.body.textContent = "OK"
# be sure the page knows the worker has done parsing to avoid
# unnecessary random timeouts all over the tests
document.documentElement.className += " worker"

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('micropython');
</script>
</head>
<body>
<script type="micropython">
from xworker import XWorker
def handle_message(event):
print(event.data)
w = XWorker('./worker.py')
w.postMessage('main')
w.onmessage = handle_message
</script>
</body>
</html>

View File

@@ -1,15 +0,0 @@
from xworker import xworker
document = xworker.window.document
def on_message(event):
print(event.data)
xworker.postMessage("thread")
xworker.onmessage = on_message
# be sure the page knows the worker has done parsing to avoid
# unnecessary random timeouts all over the tests
document.documentElement.className += " worker"

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('pyodide');
</script>
</head>
<body>
<script type="pyodide">
import js
js.document.currentScript.target.textContent = "OK"
</script>
</body>
</html>

View File

@@ -1,18 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('pyodide');
</script>
</head>
<body>
<script type="pyodide" config="../fetch.toml">
import js
import a, b
js.document.currentScript.target.textContent = a.x + ", " + b.x
</script>
</body>
</html>

View File

@@ -1,3 +0,0 @@
import '/core.js';
await pyscript.env.pyodide;
document.documentElement.classList.add('ready');

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('pyodide');
</script>
</head>
<body>
<script type="pyodide">
import asyncio
from xworker import XWorker
XWorker('./sync.py').sync.sleep = asyncio.sleep
</script>
</body>
</html>

View File

@@ -1,12 +0,0 @@
import time
from xworker import xworker
time.sleep = xworker.sync.sleep
print("before")
time.sleep(1)
print("after")
# be sure the page knows the worker has done parsing to avoid
# unnecessary random timeouts all over the tests
xworker.window.document.documentElement.classList.add("worker")

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('pyodide');
</script>
</head>
<body>
<script type="pyodide">
from xworker import XWorker
def handle_message(event):
print(event.data)
w = XWorker('./worker.py')
w.postMessage('main')
w.onmessage = handle_message
</script>
</body>
</html>

View File

@@ -1,13 +0,0 @@
from xworker import xworker
def on_message(event):
print(event.data)
xworker.postMessage("thread")
xworker.onmessage = on_message
# be sure the page knows the worker has done parsing to avoid
# unnecessary random timeouts all over the tests
xworker.window.document.documentElement.classList.add("worker")

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('ruby-wasm-wasi');
</script>
</head>
<body>
<script type="ruby-wasm-wasi">
def show_OK(event)
event[:target][:textContent] = 'OK'
end
</script>
<button ruby-wasm-wasi-click="show_OK"></button>
</body>
</html>

View File

@@ -1,3 +0,0 @@
import '/core.js';
await pyscript.env['ruby-wasm-wasi'];
document.documentElement.classList.add('ready');

View File

@@ -1,5 +0,0 @@
import '/core.js';
export const init = name => pyscript.env[name].then(() => {
document.documentElement.classList.add('ready');
});

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('wasmoon');
</script>
</head>
<body>
<script type="wasmoon">
function show_OK(event)
event.target.textContent = 'OK'
end
</script>
<button wasmoon-click="show_OK"></button>
</body>
</html>

View File

@@ -1,3 +0,0 @@
import '/core.js';
await pyscript.env.wasmoon;
document.documentElement.classList.add('ready');

View File

@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('wasmoon');
</script>
</head>
<body>
<script type="wasmoon">
function handle_message(event)
print(event.data)
end
w = XWorker('./worker.lua')
w.postMessage('main')
w.onmessage = handle_message
</script>
</body>
</html>

View File

@@ -1,10 +0,0 @@
function on_message(event)
print(event.data)
xworker.postMessage('thread')
end
xworker.onmessage = on_message
-- be sure the page knows the worker has done parsing to avoid
-- unnecessary random timeouts all over the tests
xworker.window.document.documentElement.classList.add("worker")

View File

@@ -1,17 +0,0 @@
'use strict';
const { shared, python } = require('./_shared.js');
module.exports = (playwright, baseURL) => {
const { test } = playwright;
test('MicroPython bootstrap', python.bootstrap(playwright, baseURL));
test('MicroPython fetch', python.fetch(playwright, `${baseURL}/fetch.html`));
test('MicroPython to MicroPython Worker', shared.worker(playwright, `${baseURL}/worker.html`));
test('MicroPython Worker window', shared.workerWindow(playwright, baseURL));
test('MicroPython to Wasmoon Worker', shared.worker(playwright, `${baseURL}/worker-lua.html`));
};

View File

@@ -1,28 +0,0 @@
'use strict';
const { shared, python } = require('./_shared.js');
module.exports = (playwright, baseURL) => {
const { expect, test } = playwright;
test('Pyodide bootstrap', python.bootstrap(playwright, baseURL));
test('Pyodide fetch', python.fetch(playwright, `${baseURL}/fetch.html`));
test('Pyodide to Pyodide Worker', shared.worker(playwright, `${baseURL}/worker.html`));
test('Pyodide sync (time)', async ({ page }) => {
const logs = [];
page.on('console', msg => logs.push({text: msg.text(), time: new Date}));
await page.goto(`${baseURL}/sync.html`);
await page.waitForSelector('html.worker.ready');
await expect(logs.length).toBe(2);
const [
{text: text1, time: time1},
{text: text2, time: time2}
] = logs;
await expect(text1).toBe('before');
await expect(text2).toBe('after');
await expect((time2 - time1) >= 1000).toBe(true);
});
};

View File

@@ -1,9 +0,0 @@
'use strict';
const { shared } = require('./_shared.js');
module.exports = (playwright, baseURL) => {
const { test } = playwright;
test('Ruby WASM WASI bootstrap', shared.bootstrap(playwright, baseURL));
};

View File

@@ -1,11 +0,0 @@
'use strict';
const { shared } = require('./_shared.js');
module.exports = (playwright, baseURL) => {
const { test } = playwright;
test('Wasmoon bootstrap', shared.bootstrap(playwright, baseURL));
test('Wasmoon to Wasmoon Worker', shared.worker(playwright, `${baseURL}/worker.html`));
};

View File

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

View File

@@ -1,17 +0,0 @@
{
"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"
}
]
}

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