diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f6eafe2..5f2dcae7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,7 @@ repos: - id: check-json exclude: tsconfig\.json - id: check-toml + exclude: bad\.toml - id: check-xml - id: check-yaml - id: detect-private-key diff --git a/pyscript.core/package-lock.json b/pyscript.core/package-lock.json index fe2777b1..74698014 100644 --- a/pyscript.core/package-lock.json +++ b/pyscript.core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pyscript/core", - "version": "0.1.18", + "version": "0.1.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pyscript/core", - "version": "0.1.18", + "version": "0.1.19", "license": "APACHE-2.0", "dependencies": { "@ungap/with-resolvers": "^0.1.0", @@ -597,9 +597,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.519", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.519.tgz", - "integrity": "sha512-kqs9oGYL4UFVkLKhqCTgBCYZv+wZ374yABDMqlDda9HvlkQxvSr7kgf4hfWVjMieDbX+1MwPHFBsOGCMIBaFKg==", + "version": "1.4.520", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.520.tgz", + "integrity": "sha512-Frfus2VpYADsrh1lB3v/ft/WVFlVzOIm+Q0p7U7VqHI6qr7NWHYKe+Wif3W50n7JAFoBsWVsoU0+qDks6WQ60g==", "dev": true }, "node_modules/entities": { diff --git a/pyscript.core/package.json b/pyscript.core/package.json index 91a378aa..14d68636 100644 --- a/pyscript.core/package.json +++ b/pyscript.core/package.json @@ -1,6 +1,6 @@ { "name": "@pyscript/core", - "version": "0.1.18", + "version": "0.1.19", "type": "module", "description": "PyScript", "module": "./index.js", @@ -21,6 +21,7 @@ "scripts": { "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", + "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 ." }, "keywords": [ diff --git a/pyscript.core/src/config.js b/pyscript.core/src/config.js index 23a52c63..79625ee9 100644 --- a/pyscript.core/src/config.js +++ b/pyscript.core/src/config.js @@ -7,38 +7,96 @@ import { $ } from "basic-devtools"; import allPlugins from "./plugins.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 -// we should factor this utility out a part but this works anyway. -import { parse } from "../node_modules/polyscript/esm/toml.js"; +const badURL = (url, expected = "") => { + let message = `(${ErrorCode.BAD_CONFIG}): Invalid URL: ${url}`; + 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 -let config, plugins, parsed; +let config, plugins, parsed, error, type; let pyConfig = $("py-config"); if (pyConfig) config = pyConfig.getAttribute("src") || pyConfig.textContent; else { pyConfig = $('script[type="py"][config]'); if (pyConfig) config = pyConfig.getAttribute("config"); } +if (pyConfig) type = pyConfig.getAttribute("type"); -// load its content if remote -if (/^https?:\/\//.test(config)) config = await fetch(config).then(getText); - -// parse config only if not empty -if (config?.trim()) { - try { - parsed = JSON.parse(config); - } catch (_) { - parsed = await parse(config); +// catch possible fetch errors +try { + const { json, toml, text, url } = await configDetails(config); + config = text; + if (json || type === "json") { + try { + parsed = JSON.parse(text); + } catch (e) { + 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 // those flagged as "undesired" via `!` prefix const toBeAwaited = []; 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); -export { config, plugins }; +export { config, plugins, error }; diff --git a/pyscript.core/src/core.js b/pyscript.core/src/core.js index 56c81c14..d1403394 100644 --- a/pyscript.core/src/core.js +++ b/pyscript.core/src/core.js @@ -11,7 +11,7 @@ import { Hook } from "../node_modules/polyscript/esm/worker/hooks.js"; import sync from "./sync.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"; const { assign, defineProperty, entries } = Object; @@ -128,74 +128,81 @@ const workerHooks = { }; // define the module as both ` + + + diff --git a/pyscript.core/test/config.html b/pyscript.core/test/config.html new file mode 100644 index 00000000..e9f0af02 --- /dev/null +++ b/pyscript.core/test/config.html @@ -0,0 +1,13 @@ + + + + + + PyScript Next Plugin + + + + files = [ + + + diff --git a/pyscript.core/types/config.d.ts b/pyscript.core/types/config.d.ts index 6507150f..2d5eceee 100644 --- a/pyscript.core/types/config.d.ts +++ b/pyscript.core/types/config.d.ts @@ -1,2 +1,3 @@ export let config: any; export let plugins: any; +export let error: any; diff --git a/pyscript.core/types/plugins/error.d.ts b/pyscript.core/types/plugins/error.d.ts index cb0ff5c3..5c76300b 100644 --- a/pyscript.core/types/plugins/error.d.ts +++ b/pyscript.core/types/plugins/error.d.ts @@ -1 +1 @@ -export {}; +export function notify(message: any): void;