@pyscript/core 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/cjs/custom/pyscript/exceptions.js +86 -0
- package/cjs/custom/pyscript/fetch.js +65 -0
- package/cjs/custom/pyscript.js +197 -0
- package/cjs/custom.js +201 -0
- package/cjs/index.js +2 -2
- package/cjs/interpreter/_python.js +8 -0
- package/cjs/interpreter/micropython.js +10 -12
- package/cjs/interpreter/pyodide.js +9 -7
- package/cjs/plugins/pyscript/exceptions.js +86 -0
- package/cjs/plugins/pyscript/fetch.js +65 -0
- package/cjs/plugins/pyscript.js +153 -83
- package/cjs/worker/class.js +4 -4
- package/cjs/worker/xworker.js +1 -1
- package/core.js +2 -0
- package/esm/custom/pyscript/exceptions.js +80 -0
- package/esm/custom/pyscript/fetch.js +63 -0
- package/esm/custom/pyscript.js +195 -0
- package/esm/{custom-types.js → custom.js} +70 -81
- package/esm/index.js +2 -2
- package/esm/interpreter/_python.js +6 -0
- package/esm/interpreter/micropython.js +10 -12
- package/esm/interpreter/pyodide.js +9 -7
- package/esm/script-handler.js +1 -1
- package/esm/worker/class.js +4 -4
- package/esm/worker/xworker.js +1 -1
- package/package.json +13 -11
- package/pyscript.js +2 -0
- package/types/coincident/window.d.ts +10 -0
- package/types/pyscript/pyscript.core/esm/custom.d.ts +54 -0
- package/types/pyscript/pyscript.core/esm/index.d.ts +1 -1
- package/types/pyscript/pyscript.core/esm/interpreter/_python.d.ts +2 -0
- package/types/pyscript/pyscript.core/esm/interpreter/micropython.d.ts +4 -2
- package/types/pyscript/pyscript.core/esm/interpreter/pyodide.d.ts +4 -2
- package/esm/worker/hooks.js +0 -1
- package/min.js +0 -2
package/README.md
CHANGED
@@ -19,7 +19,7 @@ This project requires some automatic artifact creation to:
|
|
19
19
|
|
20
20
|
* create a _Worker_ as a _Blob_ based on the same code used by this repo
|
21
21
|
* create automatically the list of runtimes available via the module
|
22
|
-
* create the `
|
22
|
+
* create the `core.js` file used by most integration tests
|
23
23
|
* create a sha256 version of the Blob content for CSP cases
|
24
24
|
|
25
25
|
Accordingly, to build latest project:
|
@@ -0,0 +1,86 @@
|
|
1
|
+
'use strict';
|
2
|
+
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>`;
|
3
|
+
|
4
|
+
/**
|
5
|
+
* These error codes are used to identify the type of error that occurred.
|
6
|
+
* @see https://docs.pyscript.net/latest/reference/exceptions.html?highlight=errors
|
7
|
+
*/
|
8
|
+
const ErrorCode = {
|
9
|
+
GENERIC: "PY0000", // Use this only for development then change to a more specific error code
|
10
|
+
FETCH_ERROR: "PY0001",
|
11
|
+
FETCH_NAME_ERROR: "PY0002",
|
12
|
+
// Currently these are created depending on error code received from fetching
|
13
|
+
FETCH_UNAUTHORIZED_ERROR: "PY0401",
|
14
|
+
FETCH_FORBIDDEN_ERROR: "PY0403",
|
15
|
+
FETCH_NOT_FOUND_ERROR: "PY0404",
|
16
|
+
FETCH_SERVER_ERROR: "PY0500",
|
17
|
+
FETCH_UNAVAILABLE_ERROR: "PY0503",
|
18
|
+
BAD_CONFIG: "PY1000",
|
19
|
+
MICROPIP_INSTALL_ERROR: "PY1001",
|
20
|
+
BAD_PLUGIN_FILE_EXTENSION: "PY2000",
|
21
|
+
NO_DEFAULT_EXPORT: "PY2001",
|
22
|
+
TOP_LEVEL_AWAIT: "PY9000",
|
23
|
+
};
|
24
|
+
exports.ErrorCode = ErrorCode;
|
25
|
+
|
26
|
+
class UserError extends Error {
|
27
|
+
constructor(errorCode, message = "", messageType = "text") {
|
28
|
+
super(`(${errorCode}): ${message}`);
|
29
|
+
this.errorCode = errorCode;
|
30
|
+
this.messageType = messageType;
|
31
|
+
this.name = "UserError";
|
32
|
+
}
|
33
|
+
}
|
34
|
+
exports.UserError = UserError
|
35
|
+
|
36
|
+
class FetchError extends UserError {
|
37
|
+
constructor(errorCode, message) {
|
38
|
+
super(errorCode, message);
|
39
|
+
this.name = "FetchError";
|
40
|
+
}
|
41
|
+
}
|
42
|
+
exports.FetchError = FetchError
|
43
|
+
|
44
|
+
class InstallError extends UserError {
|
45
|
+
constructor(errorCode, message) {
|
46
|
+
super(errorCode, message);
|
47
|
+
this.name = "InstallError";
|
48
|
+
}
|
49
|
+
}
|
50
|
+
exports.InstallError = InstallError
|
51
|
+
|
52
|
+
function _createAlertBanner(
|
53
|
+
message,
|
54
|
+
level,
|
55
|
+
messageType = "text",
|
56
|
+
logMessage = true,
|
57
|
+
) {
|
58
|
+
switch (`log-${level}-${logMessage}`) {
|
59
|
+
case "log-error-true":
|
60
|
+
console.error(message);
|
61
|
+
break;
|
62
|
+
case "log-warning-true":
|
63
|
+
console.warn(message);
|
64
|
+
break;
|
65
|
+
}
|
66
|
+
|
67
|
+
const content = messageType === "html" ? "innerHTML" : "textContent";
|
68
|
+
const banner = Object.assign(document.createElement("div"), {
|
69
|
+
className: `alert-banner py-${level}`,
|
70
|
+
[content]: message,
|
71
|
+
});
|
72
|
+
|
73
|
+
if (level === "warning") {
|
74
|
+
const closeButton = Object.assign(document.createElement("button"), {
|
75
|
+
id: "alert-close-button",
|
76
|
+
innerHTML: CLOSEBUTTON,
|
77
|
+
});
|
78
|
+
|
79
|
+
banner.appendChild(closeButton).addEventListener("click", () => {
|
80
|
+
banner.remove();
|
81
|
+
});
|
82
|
+
}
|
83
|
+
|
84
|
+
document.body.prepend(banner);
|
85
|
+
}
|
86
|
+
exports._createAlertBanner = _createAlertBanner
|
@@ -0,0 +1,65 @@
|
|
1
|
+
'use strict';
|
2
|
+
const { FetchError, ErrorCode } = require("./exceptions");
|
3
|
+
|
4
|
+
/**
|
5
|
+
* This is a fetch wrapper that handles any non 200 responses and throws a
|
6
|
+
* FetchError with the right ErrorCode. This is useful because our FetchError
|
7
|
+
* will automatically create an alert banner.
|
8
|
+
*
|
9
|
+
* @param {string} url - URL to fetch
|
10
|
+
* @param {Request} [options] - options to pass to fetch
|
11
|
+
* @returns {Promise<Response>}
|
12
|
+
*/
|
13
|
+
async function robustFetch(url, options) {
|
14
|
+
let response;
|
15
|
+
|
16
|
+
// Note: We need to wrap fetch into a try/catch block because fetch
|
17
|
+
// throws a TypeError if the URL is invalid such as http://blah.blah
|
18
|
+
try {
|
19
|
+
response = await fetch(url, options);
|
20
|
+
} catch (err) {
|
21
|
+
const error = err;
|
22
|
+
let errMsg;
|
23
|
+
if (url.startsWith("http")) {
|
24
|
+
errMsg =
|
25
|
+
`Fetching from URL ${url} failed with error ` +
|
26
|
+
`'${error.message}'. Are your filename and path correct?`;
|
27
|
+
} else {
|
28
|
+
errMsg = `PyScript: Access to local files
|
29
|
+
(using [[fetch]] configurations in <py-config>)
|
30
|
+
is not available when directly opening a HTML file;
|
31
|
+
you must use a webserver to serve the additional files.
|
32
|
+
See <a style="text-decoration: underline;" href="https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062">this reference</a>
|
33
|
+
on starting a simple webserver with Python.
|
34
|
+
`;
|
35
|
+
}
|
36
|
+
throw new FetchError(ErrorCode.FETCH_ERROR, errMsg);
|
37
|
+
}
|
38
|
+
|
39
|
+
// Note that response.ok is true for 200-299 responses
|
40
|
+
if (!response.ok) {
|
41
|
+
const errorMsg = `Fetching from URL ${url} failed with error ${response.status} (${response.statusText}). Are your filename and path correct?`;
|
42
|
+
switch (response.status) {
|
43
|
+
case 404:
|
44
|
+
throw new FetchError(ErrorCode.FETCH_NOT_FOUND_ERROR, errorMsg);
|
45
|
+
case 401:
|
46
|
+
throw new FetchError(
|
47
|
+
ErrorCode.FETCH_UNAUTHORIZED_ERROR,
|
48
|
+
errorMsg,
|
49
|
+
);
|
50
|
+
case 403:
|
51
|
+
throw new FetchError(ErrorCode.FETCH_FORBIDDEN_ERROR, errorMsg);
|
52
|
+
case 500:
|
53
|
+
throw new FetchError(ErrorCode.FETCH_SERVER_ERROR, errorMsg);
|
54
|
+
case 503:
|
55
|
+
throw new FetchError(
|
56
|
+
ErrorCode.FETCH_UNAVAILABLE_ERROR,
|
57
|
+
errorMsg,
|
58
|
+
);
|
59
|
+
default:
|
60
|
+
throw new FetchError(ErrorCode.FETCH_ERROR, errorMsg);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
return response;
|
64
|
+
}
|
65
|
+
exports.robustFetch = robustFetch
|
@@ -0,0 +1,197 @@
|
|
1
|
+
'use strict';
|
2
|
+
require("@ungap/with-resolvers");
|
3
|
+
const { $ } = require("basic-devtools");
|
4
|
+
|
5
|
+
const { define } = require("../index.js");
|
6
|
+
const { queryTarget } = require("../script-handler.js");
|
7
|
+
const { defineProperty } = require("../utils.js");
|
8
|
+
const { getText } = require("../fetch-utils.js");
|
9
|
+
|
10
|
+
// TODO: should this utility be in core instead?
|
11
|
+
const { robustFetch: fetch } = require("./pyscript/fetch.js");
|
12
|
+
|
13
|
+
// append ASAP CSS to avoid showing content
|
14
|
+
document.head.appendChild(document.createElement("style")).textContent = `
|
15
|
+
py-script, py-config {
|
16
|
+
display: none;
|
17
|
+
}
|
18
|
+
`;
|
19
|
+
|
20
|
+
(async () => {
|
21
|
+
// create a unique identifier when/if needed
|
22
|
+
let id = 0;
|
23
|
+
const getID = (prefix = "py") => `${prefix}-${id++}`;
|
24
|
+
|
25
|
+
// find the shared config for all py-script elements
|
26
|
+
let config;
|
27
|
+
let pyConfig = $("py-config");
|
28
|
+
if (pyConfig) config = pyConfig.getAttribute("src") || pyConfig.textContent;
|
29
|
+
else {
|
30
|
+
pyConfig = $('script[type="py"]');
|
31
|
+
config = pyConfig?.getAttribute("config");
|
32
|
+
}
|
33
|
+
|
34
|
+
if (/^https?:\/\//.test(config)) config = await fetch(config).then(getText);
|
35
|
+
|
36
|
+
// generic helper to disambiguate between custom element and script
|
37
|
+
const isScript = (element) => element.tagName === "SCRIPT";
|
38
|
+
|
39
|
+
// helper for all script[type="py"] out there
|
40
|
+
const before = (script) => {
|
41
|
+
defineProperty(document, "currentScript", {
|
42
|
+
configurable: true,
|
43
|
+
get: () => script,
|
44
|
+
});
|
45
|
+
};
|
46
|
+
|
47
|
+
const after = () => {
|
48
|
+
delete document.currentScript;
|
49
|
+
};
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
|
53
|
+
* It either throws an error if the 'src' can't be fetched or it returns a fallback
|
54
|
+
* content as source.
|
55
|
+
*/
|
56
|
+
const fetchSource = async (tag) => {
|
57
|
+
if (tag.hasAttribute("src")) {
|
58
|
+
try {
|
59
|
+
const response = await fetch(tag.getAttribute("src"));
|
60
|
+
return response.then(getText);
|
61
|
+
} catch (error) {
|
62
|
+
// TODO _createAlertBanner(err) instead ?
|
63
|
+
alert(error.message);
|
64
|
+
throw error;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
return tag.textContent;
|
68
|
+
};
|
69
|
+
|
70
|
+
// common life-cycle handlers for any node
|
71
|
+
const bootstrapNodeAndPlugins = (pyodide, element, callback, hook) => {
|
72
|
+
if (isScript(element)) callback(element);
|
73
|
+
for (const fn of hooks[hook]) fn(pyodide, element);
|
74
|
+
};
|
75
|
+
|
76
|
+
const addDisplay = (element) => {
|
77
|
+
const id = isScript(element) ? element.target.id : element.id;
|
78
|
+
return `
|
79
|
+
# this code is just for demo purpose but the basics work
|
80
|
+
def _display(what, target="${id}", append=True):
|
81
|
+
from js import document
|
82
|
+
element = document.getElementById(target)
|
83
|
+
element.textContent = what
|
84
|
+
display = _display
|
85
|
+
`;
|
86
|
+
};
|
87
|
+
|
88
|
+
// define the module as both `<script type="py">` and `<py-script>`
|
89
|
+
define("py", {
|
90
|
+
config,
|
91
|
+
env: "py-script",
|
92
|
+
interpreter: "pyodide",
|
93
|
+
codeBeforeRunWorker() {
|
94
|
+
const { codeBeforeRunWorker: set } = hooks;
|
95
|
+
const prefix = 'print("codeBeforeRunWorker")';
|
96
|
+
return [prefix].concat(...set).join("\n");
|
97
|
+
},
|
98
|
+
codeAfterRunWorker() {
|
99
|
+
const { codeAfterRunWorker: set } = hooks;
|
100
|
+
const prefix = 'print("codeAfterRunWorker")';
|
101
|
+
return [prefix].concat(...set).join("\n");
|
102
|
+
},
|
103
|
+
onBeforeRun(pyodide, element) {
|
104
|
+
bootstrapNodeAndPlugins(pyodide, element, before, "onBeforeRun");
|
105
|
+
pyodide.interpreter.runPython(addDisplay(element));
|
106
|
+
},
|
107
|
+
onBeforeRunAync(pyodide, element) {
|
108
|
+
pyodide.interpreter.runPython(addDisplay(element));
|
109
|
+
bootstrapNodeAndPlugins(
|
110
|
+
pyodide,
|
111
|
+
element,
|
112
|
+
before,
|
113
|
+
"onBeforeRunAync",
|
114
|
+
);
|
115
|
+
},
|
116
|
+
onAfterRun(pyodide, element) {
|
117
|
+
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRun");
|
118
|
+
},
|
119
|
+
onAfterRunAsync(pyodide, element) {
|
120
|
+
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync");
|
121
|
+
},
|
122
|
+
async onRuntimeReady(pyodide, element) {
|
123
|
+
// allows plugins to do whatever they want with the element
|
124
|
+
// before regular stuff happens in here
|
125
|
+
for (const callback of hooks.onRuntimeReady)
|
126
|
+
callback(pyodide, element);
|
127
|
+
if (isScript(element)) {
|
128
|
+
const {
|
129
|
+
attributes: { async: isAsync, target },
|
130
|
+
} = element;
|
131
|
+
const hasTarget = !!target?.value;
|
132
|
+
const show = hasTarget
|
133
|
+
? queryTarget(target.value)
|
134
|
+
: document.createElement("script-py");
|
135
|
+
|
136
|
+
if (!hasTarget) element.after(show);
|
137
|
+
if (!show.id) show.id = getID();
|
138
|
+
|
139
|
+
// allows the code to retrieve the target element via
|
140
|
+
// document.currentScript.target if needed
|
141
|
+
defineProperty(element, "target", { value: show });
|
142
|
+
|
143
|
+
pyodide[`run${isAsync ? "Async" : ""}`](
|
144
|
+
await fetchSource(element),
|
145
|
+
);
|
146
|
+
} else {
|
147
|
+
// resolve PyScriptElement to allow connectedCallback
|
148
|
+
element._pyodide.resolve(pyodide);
|
149
|
+
}
|
150
|
+
},
|
151
|
+
});
|
152
|
+
|
153
|
+
class PyScriptElement extends HTMLElement {
|
154
|
+
constructor() {
|
155
|
+
if (!super().id) this.id = getID();
|
156
|
+
this._pyodide = Promise.withResolvers();
|
157
|
+
this.srcCode = "";
|
158
|
+
this.executed = false;
|
159
|
+
}
|
160
|
+
async connectedCallback() {
|
161
|
+
if (!this.executed) {
|
162
|
+
this.executed = true;
|
163
|
+
const { run } = await this._pyodide.promise;
|
164
|
+
this.srcCode = await fetchSource(this);
|
165
|
+
this.textContent = "";
|
166
|
+
const result = run(this.srcCode);
|
167
|
+
if (!this.textContent && result) this.textContent = result;
|
168
|
+
this.style.display = "block";
|
169
|
+
}
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
customElements.define("py-script", PyScriptElement);
|
174
|
+
})();
|
175
|
+
|
176
|
+
const hooks = {
|
177
|
+
/** @type {Set<function>} */
|
178
|
+
onBeforeRun: new Set(),
|
179
|
+
/** @type {Set<function>} */
|
180
|
+
onBeforeRunAync: new Set(),
|
181
|
+
/** @type {Set<function>} */
|
182
|
+
onAfterRun: new Set(),
|
183
|
+
/** @type {Set<function>} */
|
184
|
+
onAfterRunAsync: new Set(),
|
185
|
+
/** @type {Set<function>} */
|
186
|
+
onRuntimeReady: new Set(),
|
187
|
+
|
188
|
+
/** @type {Set<string>} */
|
189
|
+
codeBeforeRunWorker: new Set(),
|
190
|
+
/** @type {Set<string>} */
|
191
|
+
codeBeforeRunWorkerAsync: new Set(),
|
192
|
+
/** @type {Set<string>} */
|
193
|
+
codeAfterRunWorker: new Set(),
|
194
|
+
/** @type {Set<string>} */
|
195
|
+
codeAfterRunWorkerAsync: new Set(),
|
196
|
+
};
|
197
|
+
exports.hooks = hooks;
|
package/cjs/custom.js
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
'use strict';
|
2
|
+
require("@ungap/with-resolvers");
|
3
|
+
const { $$ } = require("basic-devtools");
|
4
|
+
|
5
|
+
const { assign, create } = require("./utils.js");
|
6
|
+
const { getDetails } = require("./script-handler.js");
|
7
|
+
const {
|
8
|
+
registry: defaultRegistry,
|
9
|
+
prefixes,
|
10
|
+
configs
|
11
|
+
} = require("./interpreters.js");
|
12
|
+
const { getRuntimeID } = require("./loader.js");
|
13
|
+
const { io } = require("./interpreter/_utils.js");
|
14
|
+
const { addAllListeners } = require("./listeners.js");
|
15
|
+
|
16
|
+
const CUSTOM_SELECTORS = [];
|
17
|
+
exports.CUSTOM_SELECTORS = CUSTOM_SELECTORS;
|
18
|
+
|
19
|
+
/**
|
20
|
+
* @typedef {Object} Runtime custom configuration
|
21
|
+
* @prop {object} interpreter the bootstrapped interpreter
|
22
|
+
* @prop {(url:string, options?: object) => Worker} XWorker an XWorker constructor that defaults to same interpreter on the Worker.
|
23
|
+
* @prop {object} config a cloned config used to bootstrap the interpreter
|
24
|
+
* @prop {(code:string) => any} run an utility to run code within the interpreter
|
25
|
+
* @prop {(code:string) => Promise<any>} runAsync an utility to run code asynchronously within the interpreter
|
26
|
+
* @prop {(path:string, data:ArrayBuffer) => void} writeFile an utility to write a file in the virtual FS, if available
|
27
|
+
*/
|
28
|
+
|
29
|
+
const types = new Map();
|
30
|
+
const waitList = new Map();
|
31
|
+
|
32
|
+
// REQUIRES INTEGRATION TEST
|
33
|
+
/* c8 ignore start */
|
34
|
+
/**
|
35
|
+
* @param {Element} node any DOM element registered via define.
|
36
|
+
*/
|
37
|
+
const handleCustomType = (node) => {
|
38
|
+
for (const selector of CUSTOM_SELECTORS) {
|
39
|
+
if (node.matches(selector)) {
|
40
|
+
const type = types.get(selector);
|
41
|
+
const { resolve } = waitList.get(type);
|
42
|
+
const { options, known } = registry.get(type);
|
43
|
+
if (!known.has(node)) {
|
44
|
+
known.add(node);
|
45
|
+
const {
|
46
|
+
interpreter: runtime,
|
47
|
+
version,
|
48
|
+
config,
|
49
|
+
env,
|
50
|
+
onRuntimeReady,
|
51
|
+
} = options;
|
52
|
+
const name = getRuntimeID(runtime, version);
|
53
|
+
const id = env || `${name}${config ? `|${config}` : ""}`;
|
54
|
+
const { interpreter: engine, XWorker: Worker } = getDetails(
|
55
|
+
runtime,
|
56
|
+
id,
|
57
|
+
name,
|
58
|
+
version,
|
59
|
+
config,
|
60
|
+
);
|
61
|
+
engine.then((interpreter) => {
|
62
|
+
const module = create(defaultRegistry.get(runtime));
|
63
|
+
|
64
|
+
const {
|
65
|
+
onBeforeRun,
|
66
|
+
onBeforeRunAsync,
|
67
|
+
onAfterRun,
|
68
|
+
onAfterRunAsync,
|
69
|
+
codeBeforeRunWorker,
|
70
|
+
codeBeforeRunWorkerAsync,
|
71
|
+
codeAfterRunWorker,
|
72
|
+
codeAfterRunWorkerAsync,
|
73
|
+
} = options;
|
74
|
+
|
75
|
+
const hooks = {
|
76
|
+
beforeRun: codeBeforeRunWorker?.(),
|
77
|
+
beforeRunAsync: codeBeforeRunWorkerAsync?.(),
|
78
|
+
afterRun: codeAfterRunWorker?.(),
|
79
|
+
afterRunAsync: codeAfterRunWorkerAsync?.(),
|
80
|
+
};
|
81
|
+
|
82
|
+
const XWorker = function XWorker(...args) {
|
83
|
+
return Worker.apply(hooks, args);
|
84
|
+
};
|
85
|
+
|
86
|
+
// These two loops mimic a `new Map(arrayContent)` without needing
|
87
|
+
// the new Map overhead so that [name, [before, after]] can be easily destructured
|
88
|
+
// and new sync or async patches become easy to add (when the logic is the same).
|
89
|
+
|
90
|
+
// patch sync
|
91
|
+
for (const [name, [before, after]] of [
|
92
|
+
["run", [onBeforeRun, onAfterRun]],
|
93
|
+
]) {
|
94
|
+
const method = module[name];
|
95
|
+
module[name] = function (interpreter, code) {
|
96
|
+
if (before) before.call(this, resolved, node);
|
97
|
+
const result = method.call(this, interpreter, code);
|
98
|
+
if (after) after.call(this, resolved, node);
|
99
|
+
return result;
|
100
|
+
};
|
101
|
+
}
|
102
|
+
|
103
|
+
// patch async
|
104
|
+
for (const [name, [before, after]] of [
|
105
|
+
["runAsync", [onBeforeRunAsync, onAfterRunAsync]],
|
106
|
+
]) {
|
107
|
+
const method = module[name];
|
108
|
+
module[name] = async function (interpreter, code) {
|
109
|
+
if (before) await before.call(this, resolved, node);
|
110
|
+
const result = await method.call(
|
111
|
+
this,
|
112
|
+
interpreter,
|
113
|
+
code,
|
114
|
+
);
|
115
|
+
if (after) await after.call(this, resolved, node);
|
116
|
+
return result;
|
117
|
+
};
|
118
|
+
}
|
119
|
+
|
120
|
+
module.setGlobal(interpreter, "XWorker", XWorker);
|
121
|
+
|
122
|
+
const resolved = {
|
123
|
+
type,
|
124
|
+
interpreter,
|
125
|
+
XWorker,
|
126
|
+
io: io.get(interpreter),
|
127
|
+
config: structuredClone(configs.get(name)),
|
128
|
+
run: module.run.bind(module, interpreter),
|
129
|
+
runAsync: module.runAsync.bind(module, interpreter),
|
130
|
+
};
|
131
|
+
|
132
|
+
resolve(resolved);
|
133
|
+
|
134
|
+
onRuntimeReady?.(resolved, node);
|
135
|
+
});
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
};
|
140
|
+
exports.handleCustomType = handleCustomType;
|
141
|
+
|
142
|
+
/**
|
143
|
+
* @type {Map<string, {options:object, known:WeakSet<Element>}>}
|
144
|
+
*/
|
145
|
+
const registry = new Map();
|
146
|
+
|
147
|
+
/**
|
148
|
+
* @typedef {Object} PluginOptions custom configuration
|
149
|
+
* @prop {'pyodide' | 'micropython' | 'wasmoon' | 'ruby-wasm-wasi'} interpreter the interpreter to use
|
150
|
+
* @prop {string} [version] the optional interpreter version to use
|
151
|
+
* @prop {string} [config] the optional config to use within such interpreter
|
152
|
+
* @prop {(environment: object, node: Element) => void} [onRuntimeReady] the callback that will be invoked once
|
153
|
+
*/
|
154
|
+
|
155
|
+
/**
|
156
|
+
* Allows custom types and components on the page to receive interpreters to execute any code
|
157
|
+
* @param {string} type the unique `<script type="...">` identifier
|
158
|
+
* @param {PluginOptions} options the custom type configuration
|
159
|
+
*/
|
160
|
+
const define = (type, options) => {
|
161
|
+
if (defaultRegistry.has(type) || registry.has(type))
|
162
|
+
throw new Error(`<script type="${type}"> already registered`);
|
163
|
+
|
164
|
+
if (!defaultRegistry.has(options?.interpreter))
|
165
|
+
throw new Error(`Unspecified interpreter`);
|
166
|
+
|
167
|
+
// allows reaching out the interpreter helpers on events
|
168
|
+
defaultRegistry.set(type, defaultRegistry.get(options?.interpreter));
|
169
|
+
|
170
|
+
// ensure a Promise can resolve once a custom type has been bootstrapped
|
171
|
+
whenDefined(type);
|
172
|
+
|
173
|
+
// allows selector -> registry by type
|
174
|
+
const selectors = [`script[type="${type}"]`, `${type}-script`];
|
175
|
+
for (const selector of selectors) types.set(selector, type);
|
176
|
+
|
177
|
+
CUSTOM_SELECTORS.push(...selectors);
|
178
|
+
prefixes.push(`${type}-`);
|
179
|
+
|
180
|
+
// ensure always same env for this custom type
|
181
|
+
registry.set(type, {
|
182
|
+
options: assign({ env: type }, options),
|
183
|
+
known: new WeakSet(),
|
184
|
+
});
|
185
|
+
|
186
|
+
addAllListeners(document);
|
187
|
+
$$(selectors.join(",")).forEach(handleCustomType);
|
188
|
+
};
|
189
|
+
exports.define = define;
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Resolves whenever a defined custom type is bootstrapped on the page
|
193
|
+
* @param {string} type the unique `<script type="...">` identifier
|
194
|
+
* @returns {Promise<object>}
|
195
|
+
*/
|
196
|
+
const whenDefined = (type) => {
|
197
|
+
if (!waitList.has(type)) waitList.set(type, Promise.withResolvers());
|
198
|
+
return waitList.get(type).promise;
|
199
|
+
};
|
200
|
+
exports.whenDefined = whenDefined;
|
201
|
+
/* c8 ignore stop */
|
package/cjs/index.js
CHANGED
@@ -5,13 +5,13 @@ const xworker = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 i
|
|
5
5
|
const { handle } = require("./script-handler.js");
|
6
6
|
const { assign } = require("./utils.js");
|
7
7
|
const { selectors, prefixes } = require("./interpreters.js");
|
8
|
-
const { CUSTOM_SELECTORS, handleCustomType } = require("./custom
|
8
|
+
const { CUSTOM_SELECTORS, handleCustomType } = require("./custom.js");
|
9
9
|
const { listener, addAllListeners } = require("./listeners.js");
|
10
10
|
|
11
11
|
(m => {
|
12
12
|
exports.define = m.define;
|
13
13
|
exports.whenDefined = m.whenDefined;
|
14
|
-
})(require("./custom
|
14
|
+
})(require("./custom.js"));
|
15
15
|
const XWorker = xworker();
|
16
16
|
exports.XWorker = XWorker;
|
17
17
|
|
@@ -10,6 +10,14 @@ const runAsync = (interpreter, code) =>
|
|
10
10
|
interpreter.runPythonAsync(clean(code));
|
11
11
|
exports.runAsync = runAsync;
|
12
12
|
|
13
|
+
const setGlobal = (interpreter, name, value) =>
|
14
|
+
interpreter.globals.set(name, value);
|
15
|
+
exports.setGlobal = setGlobal;
|
16
|
+
|
17
|
+
const deleteGlobal = (interpreter, name) =>
|
18
|
+
interpreter.globals.delete(name);
|
19
|
+
exports.deleteGlobal = deleteGlobal;
|
20
|
+
|
13
21
|
const writeFile = ({ FS }, path, buffer) =>
|
14
22
|
writeFileUtil(FS, path, buffer);
|
15
23
|
exports.writeFile = writeFile;
|
@@ -1,6 +1,12 @@
|
|
1
1
|
'use strict';
|
2
2
|
const { fetchPaths, stdio } = require("./_utils.js");
|
3
|
-
const {
|
3
|
+
const {
|
4
|
+
run,
|
5
|
+
runAsync,
|
6
|
+
setGlobal,
|
7
|
+
deleteGlobal,
|
8
|
+
writeFile
|
9
|
+
} = require("./_python.js");
|
4
10
|
|
5
11
|
const type = "micropython";
|
6
12
|
|
@@ -8,7 +14,7 @@ const type = "micropython";
|
|
8
14
|
/* c8 ignore start */
|
9
15
|
module.exports = {
|
10
16
|
type,
|
11
|
-
module: (version = "1.20.0-
|
17
|
+
module: (version = "1.20.0-253") =>
|
12
18
|
`https://cdn.jsdelivr.net/npm/@micropython/micropython-webassembly-pyscript@${version}/micropython.mjs`,
|
13
19
|
async engine({ loadMicroPython }, config, url) {
|
14
20
|
const { stderr, stdout, get } = stdio();
|
@@ -17,16 +23,8 @@ module.exports = {
|
|
17
23
|
if (config.fetch) await fetchPaths(this, runtime, config.fetch);
|
18
24
|
return runtime;
|
19
25
|
},
|
20
|
-
setGlobal
|
21
|
-
|
22
|
-
globalThis[id] = value;
|
23
|
-
this.run(interpreter, `from js import ${id};${name}=${id};`);
|
24
|
-
},
|
25
|
-
deleteGlobal(interpreter, name) {
|
26
|
-
const id = `__pyscript_${this.type}_${name}`;
|
27
|
-
this.run(interpreter, `del ${id};del ${name}`);
|
28
|
-
delete globalThis[id];
|
29
|
-
},
|
26
|
+
setGlobal,
|
27
|
+
deleteGlobal,
|
30
28
|
run,
|
31
29
|
runAsync,
|
32
30
|
writeFile,
|
@@ -1,6 +1,12 @@
|
|
1
1
|
'use strict';
|
2
2
|
const { fetchPaths, stdio } = require("./_utils.js");
|
3
|
-
const {
|
3
|
+
const {
|
4
|
+
run,
|
5
|
+
runAsync,
|
6
|
+
setGlobal,
|
7
|
+
deleteGlobal,
|
8
|
+
writeFile
|
9
|
+
} = require("./_python.js");
|
4
10
|
|
5
11
|
const type = "pyodide";
|
6
12
|
|
@@ -25,12 +31,8 @@ module.exports = {
|
|
25
31
|
}
|
26
32
|
return interpreter;
|
27
33
|
},
|
28
|
-
setGlobal
|
29
|
-
|
30
|
-
},
|
31
|
-
deleteGlobal(interpreter, name) {
|
32
|
-
interpreter.globals.delete(name);
|
33
|
-
},
|
34
|
+
setGlobal,
|
35
|
+
deleteGlobal,
|
34
36
|
run,
|
35
37
|
runAsync,
|
36
38
|
writeFile,
|