[next] Improve config parsing on bootstrap (#1704)

This commit is contained in:
Andrea Giammarchi
2023-09-15 12:50:52 +02:00
committed by GitHub
parent 00fdc73015
commit 840bc803b7
11 changed files with 186 additions and 92 deletions

View File

@@ -16,6 +16,7 @@ repos:
- id: check-json - id: check-json
exclude: tsconfig\.json exclude: tsconfig\.json
- id: check-toml - id: check-toml
exclude: bad\.toml
- id: check-xml - id: check-xml
- id: check-yaml - id: check-yaml
- id: detect-private-key - id: detect-private-key

View File

@@ -1,12 +1,12 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.1.18", "version": "0.1.19",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.1.18", "version": "0.1.19",
"license": "APACHE-2.0", "license": "APACHE-2.0",
"dependencies": { "dependencies": {
"@ungap/with-resolvers": "^0.1.0", "@ungap/with-resolvers": "^0.1.0",
@@ -597,9 +597,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.519", "version": "1.4.520",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.519.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.520.tgz",
"integrity": "sha512-kqs9oGYL4UFVkLKhqCTgBCYZv+wZ374yABDMqlDda9HvlkQxvSr7kgf4hfWVjMieDbX+1MwPHFBsOGCMIBaFKg==", "integrity": "sha512-Frfus2VpYADsrh1lB3v/ft/WVFlVzOIm+Q0p7U7VqHI6qr7NWHYKe+Wif3W50n7JAFoBsWVsoU0+qDks6WQ60g==",
"dev": true "dev": true
}, },
"node_modules/entities": { "node_modules/entities": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.1.18", "version": "0.1.19",
"type": "module", "type": "module",
"description": "PyScript", "description": "PyScript",
"module": "./index.js", "module": "./index.js",
@@ -21,6 +21,7 @@
"scripts": { "scripts": {
"server": "npx static-handler --cors --coep --coop --corp .", "server": "npx static-handler --cors --coep --coop --corp .",
"build": "node rollup/stdlib.cjs && node rollup/plugins.cjs && rm -rf dist && rollup --config rollup/core.config.js && npm run ts", "build": "node rollup/stdlib.cjs && node rollup/plugins.cjs && rm -rf dist && rollup --config rollup/core.config.js && npm run ts",
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do echo -e \"\\033[2m$js:\\033[0m $(cat $js | brotli | wc -c) bytes\"; done",
"ts": "tsc -p ." "ts": "tsc -p ."
}, },
"keywords": [ "keywords": [

View File

@@ -7,38 +7,96 @@ import { $ } from "basic-devtools";
import allPlugins from "./plugins.js"; import allPlugins from "./plugins.js";
import { robustFetch as fetch, getText } from "./fetch.js"; import { robustFetch as fetch, getText } from "./fetch.js";
import { ErrorCode } from "./exceptions.js";
// TODO: this is not strictly polyscript related but handy ... not sure const badURL = (url, expected = "") => {
// we should factor this utility out a part but this works anyway. let message = `(${ErrorCode.BAD_CONFIG}): Invalid URL: ${url}`;
import { parse } from "../node_modules/polyscript/esm/toml.js"; if (expected) message += `\nexpected ${expected} content`;
throw new Error(message);
};
/**
* Given a string, returns its trimmed content as text,
* fetching it from a file if the content is a URL.
* @param {string} config either JSON, TOML, or a file to fetch
* @returns {{json: boolean, toml: boolean, text: string}}
*/
const configDetails = async (config) => {
let text = config?.trim();
// we only support an object as root config
let url = "",
toml = false,
json = /^{/.test(text) && /}$/.test(text);
// handle files by extension (relaxing urls parts after)
if (!json && /\.(\w+)(?:\?\S*)?$/.test(text)) {
const ext = RegExp.$1;
if (ext === "json" && type !== "toml") json = true;
else if (ext === "toml" && type !== "json") toml = true;
else badURL(text, type);
url = text;
text = (await fetch(url).then(getText)).trim();
}
return { json, toml: toml || (!json && !!text), text, url };
};
const syntaxError = (type, url, { message }) => {
let str = `(${ErrorCode.BAD_CONFIG}): Invalid ${type}`;
if (url) str += ` @ ${url}`;
return new SyntaxError(`${str}\n${message}`);
};
// find the shared config for all py-script elements // find the shared config for all py-script elements
let config, plugins, parsed; let config, plugins, parsed, error, type;
let pyConfig = $("py-config"); let pyConfig = $("py-config");
if (pyConfig) config = pyConfig.getAttribute("src") || pyConfig.textContent; if (pyConfig) config = pyConfig.getAttribute("src") || pyConfig.textContent;
else { else {
pyConfig = $('script[type="py"][config]'); pyConfig = $('script[type="py"][config]');
if (pyConfig) config = pyConfig.getAttribute("config"); if (pyConfig) config = pyConfig.getAttribute("config");
} }
if (pyConfig) type = pyConfig.getAttribute("type");
// load its content if remote // catch possible fetch errors
if (/^https?:\/\//.test(config)) config = await fetch(config).then(getText); try {
const { json, toml, text, url } = await configDetails(config);
// parse config only if not empty config = text;
if (config?.trim()) { if (json || type === "json") {
try { try {
parsed = JSON.parse(config); parsed = JSON.parse(text);
} catch (_) { } catch (e) {
parsed = await parse(config); error = syntaxError("JSON", url, e);
}
} else if (toml || type === "toml") {
try {
const { parse } = await import(
/* webpackIgnore: true */
"https://cdn.jsdelivr.net/npm/@webreflection/toml-j0.4/toml.js"
);
parsed = parse(text);
} catch (e) {
error = syntaxError("TOML", url, e);
}
} }
} catch (e) {
error = e;
} }
// parse all plugins and optionally ignore only // parse all plugins and optionally ignore only
// those flagged as "undesired" via `!` prefix // those flagged as "undesired" via `!` prefix
const toBeAwaited = []; const toBeAwaited = [];
for (const [key, value] of Object.entries(allPlugins)) { for (const [key, value] of Object.entries(allPlugins)) {
if (!parsed?.plugins?.includes(`!${key}`)) toBeAwaited.push(value()); if (error) {
if (key === "error") {
// show on page the config is broken, meaning that
// it was not possible to disable error plugin neither
// as that part wasn't correctly parsed anyway
value().then(({ notify }) => notify(error.message));
}
} else if (!parsed?.plugins?.includes(`!${key}`)) {
toBeAwaited.push(value());
}
} }
// assign plugins as Promise.all only if needed
if (toBeAwaited.length) plugins = Promise.all(toBeAwaited); if (toBeAwaited.length) plugins = Promise.all(toBeAwaited);
export { config, plugins }; export { config, plugins, error };

View File

@@ -11,7 +11,7 @@ import { Hook } from "../node_modules/polyscript/esm/worker/hooks.js";
import sync from "./sync.js"; import sync from "./sync.js";
import stdlib from "./stdlib.js"; import stdlib from "./stdlib.js";
import { config, plugins } from "./config.js"; import { config, plugins, error } from "./config.js";
import { robustFetch as fetch, getText } from "./fetch.js"; import { robustFetch as fetch, getText } from "./fetch.js";
const { assign, defineProperty, entries } = Object; const { assign, defineProperty, entries } = Object;
@@ -128,74 +128,81 @@ const workerHooks = {
}; };
// define the module as both `<script type="py">` and `<py-script>` // define the module as both `<script type="py">` and `<py-script>`
define(TYPE, { // but only if the config didn't throw an error
config, error ||
env: `${TYPE}-script`, define(TYPE, {
interpreter: "pyodide", config,
...workerHooks, env: `${TYPE}-script`,
onWorkerReady(_, xworker) { interpreter: "pyodide",
assign(xworker.sync, sync); ...workerHooks,
}, onWorkerReady(_, xworker) {
onBeforeRun(pyodide, element) { assign(xworker.sync, sync);
currentElement = element; },
bootstrapNodeAndPlugins(pyodide, element, before, "onBeforeRun"); onBeforeRun(pyodide, element) {
}, currentElement = element;
onBeforeRunAsync(pyodide, element) { bootstrapNodeAndPlugins(pyodide, element, before, "onBeforeRun");
currentElement = element; },
bootstrapNodeAndPlugins(pyodide, element, before, "onBeforeRunAsync"); onBeforeRunAsync(pyodide, element) {
}, currentElement = element;
onAfterRun(pyodide, element) { bootstrapNodeAndPlugins(
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRun"); pyodide,
}, element,
onAfterRunAsync(pyodide, element) { before,
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync"); "onBeforeRunAsync",
},
async onInterpreterReady(pyodide, element) {
if (shouldRegister) {
shouldRegister = false;
registerModule(pyodide);
}
// ensure plugins are bootstrapped already
if (plugins) await plugins;
// 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) {
const { head, body } = document;
if (head.contains(element)) body.append(show);
else 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 });
// notify before the code runs
dispatch(element, TYPE);
pyodide[`run${isAsync ? "Async" : ""}`](
await fetchSource(element, pyodide.io, true),
); );
} else { },
// resolve PyScriptElement to allow connectedCallback onAfterRun(pyodide, element) {
element._pyodide.resolve(pyodide); bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRun");
} },
console.debug("[pyscript/main] PyScript Ready"); onAfterRunAsync(pyodide, element) {
}, bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync");
}); },
async onInterpreterReady(pyodide, element) {
if (shouldRegister) {
shouldRegister = false;
registerModule(pyodide);
}
// ensure plugins are bootstrapped already
if (plugins) await plugins;
// 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) {
const { head, body } = document;
if (head.contains(element)) body.append(show);
else 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 });
// notify before the code runs
dispatch(element, TYPE);
pyodide[`run${isAsync ? "Async" : ""}`](
await fetchSource(element, pyodide.io, true),
);
} else {
// resolve PyScriptElement to allow connectedCallback
element._pyodide.resolve(pyodide);
}
console.debug("[pyscript/main] PyScript Ready");
},
});
class PyScriptElement extends HTMLElement { class PyScriptElement extends HTMLElement {
constructor() { constructor() {
@@ -226,7 +233,8 @@ class PyScriptElement extends HTMLElement {
} }
} }
customElements.define("py-script", PyScriptElement); // define py-script only if the config didn't throw an error
error || customElements.define("py-script", PyScriptElement);
/** /**
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module. * A `Worker` facade able to bootstrap on the worker thread only a PyScript module.

View File

@@ -24,7 +24,7 @@ hooks.onBeforeRun.add(function override(pyScript) {
// Error hook utilities // Error hook utilities
// Custom function to show notifications // Custom function to show notifications
function notify(message) { export function notify(message) {
const div = document.createElement("div"); const div = document.createElement("div");
div.className = "py-error"; div.className = "py-error";
div.textContent = message; div.textContent = message;

View File

@@ -0,0 +1 @@
files = [

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Next Plugin</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
<py-config src="bad.toml" type="toml"></py-config>
</head>
</html>

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Next Plugin</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
<py-config>
files = [
</py-config>
</head>
</html>

View File

@@ -1,2 +1,3 @@
export let config: any; export let config: any;
export let plugins: any; export let plugins: any;
export let error: any;

View File

@@ -1 +1 @@
export {}; export function notify(message: any): void;