[next] Include most basic error plugin (#1677)

This commit is contained in:
Andrea Giammarchi
2023-09-06 16:49:43 +02:00
committed by GitHub
parent 264675d0c3
commit 1d015c7534
43 changed files with 255 additions and 369 deletions

View File

@@ -1,3 +1,44 @@
# @pyscript/core
We have moved and renamed previous _core_ module as [polyscript](https://github.com/pyscript/polyscript/#readme), which is the base module used in here to build up _PyScript Next_, now hosted in this folder.
## Documentation
Please read [core documentation](./docs/README.md) to know more about this project.
## Development
Clone this repository then run `npm install` within its folder.
Use `npm run build` to create all artifacts and _dist_ files.
Use `npm run server` to test locally, via the `http://localhost:8080/test/` url, smoke tests or to test manually anything you'd like to check.
### Artifacts
There are two main artifacts in this project:
- **stdlib** and its content, where `src/stdlib/pyscript.js` exposes as object literal all the _Python_ content within the folder (recursively)
- **plugins** and its content, where `src/plugins.js` exposes all available _dynamic imports_, able to instrument the bundler to create files a part within the _dist/_ folder, so that by default _core_ remains as small as possible
Accordingly, whenever a file contains this warning at its first line, please do not change such file directly before submitting a merge request, as that file will be overwritten at the next `npm run build` command, either here or in _CI_:
```js
// ⚠️ This file is an artifact: DO NOT MODIFY
```
## Python stdlib
The `pyscript` module available in _Python_ defines its exported utilities via `src/stdlib/pyscript.py`. Any file that would like to provide an export should be placed into `src/stdlib/_pyscript` folder (see the `display.py` as example) and its public functionalities should be explicit in the `src/stdlib/pyscript.py` file.
All _Python_ files will be embedded automatically whenever `npm run build` happens and reflected into the `src/stdlib/pyscript.js` file.
It is _core_ responsibility to ensure those files will be available through the Filesystem in either the _main_ thread, or any _worker_.
## JS plugins
While community or third party plugins don't need to be part of this repository and can be added just importing `@pyscript/core` as module, there are a few plugins that we would like to make available by default and these are considered _core plugins_.
To add a _core plugin_ to this project you can define your plugin entry-point and name in the `src/plugins` folder (see the `error.js` example) and create, if necessary, a folder with the same name where extra files or dependencies can be added.
The _build_ command will bring plugins by name as artifact so that the bundler can create ad-hoc files within the `dist/` folder.

View File

@@ -3,25 +3,21 @@
"version": "0.1.7",
"type": "module",
"description": "PyScript",
"main": "./core.js",
"module": "./core.js",
"unpkg": "./core.js",
"module": "./dist/core.js",
"unpkg": "./dist/core.js",
"exports": {
".": {
"types": "./types/esm/core.d.ts",
"import": "./esm/core.js"
},
"./js": {
"import": "./core.js"
"types": "./types/core.d.ts",
"import": "./src/core.js"
},
"./css": {
"import": "./core.css"
"import": "./dist/core.css"
},
"./package.json": "./package.json"
},
"scripts": {
"server": "npx static-handler --cors --coep --coop --corp .",
"build": "node rollup/stdlib.cjs && rollup --config rollup/core.config.js && rollup --config rollup/core-css.config.js && npm run ts",
"build": "node rollup/stdlib.cjs && node rollup/plugins.cjs && rollup --config rollup/core.config.js && npm run ts",
"ts": "tsc -p ."
},
"keywords": [

View File

@@ -1,20 +0,0 @@
import postcss from "rollup-plugin-postcss";
export default {
input: "./src/core.css",
plugins: [
postcss({
extract: true,
sourceMap: false,
minimize: !process.env.NO_MIN,
plugins: [],
}),
],
output: {
file: "./core.css",
},
onwarn(warning, warn) {
if (warning.code === "FILE_NAME_CONFLICT") return;
warn(warning);
},
};

View File

@@ -3,16 +3,37 @@
import { nodeResolve } from "@rollup/plugin-node-resolve";
import terser from "@rollup/plugin-terser";
import postcss from "rollup-plugin-postcss";
const plugins = [];
export default {
input: "./src/core.js",
plugins: plugins.concat(
process.env.NO_MIN ? [nodeResolve()] : [nodeResolve(), terser()],
),
output: {
esModule: true,
file: "./core.js",
export default [
{
input: "./src/core.js",
plugins: plugins.concat(
process.env.NO_MIN ? [nodeResolve()] : [nodeResolve(), terser()],
),
output: {
esModule: true,
dir: "./dist",
},
},
};
{
input: "./src/core.css",
plugins: [
postcss({
extract: true,
sourceMap: false,
minimize: !process.env.NO_MIN,
plugins: [],
}),
],
output: {
file: "./dist/core.css",
},
onwarn(warning, warn) {
if (warning.code === "FILE_NAME_CONFLICT") return;
warn(warning);
},
},
];

View File

@@ -0,0 +1,24 @@
const { readdirSync, writeFileSync } = require("node:fs");
const { join } = require("node:path");
const plugins = [""];
for (const file of readdirSync(join(__dirname, "..", "src", "plugins"))) {
if (/\.js$/.test(file)) {
const name = file.slice(0, -3);
const key = /^[a-zA-Z0-9$_]+$/.test(name)
? name
: `[${JSON.stringify(name)}]`;
const value = JSON.stringify(`./plugins/${file}`);
plugins.push(` ${key}: () => import(${value}),`);
}
}
plugins.push("");
writeFileSync(
join(__dirname, "..", "src", "plugins.js"),
`// ⚠️ This file is an artifact: DO NOT MODIFY\nexport default {${plugins.join(
"\n",
)}};\n`,
);

View File

@@ -5,6 +5,7 @@ import { htmlDecode } from "./utils.js";
import sync from "./sync.js";
import stdlib from "./stdlib.js";
import plugins from "./plugins.js";
// TODO: this is not strictly polyscript related but handy ... not sure
// we should factor this utility out a part but this works anyway.
@@ -13,7 +14,7 @@ import { Hook } from "../node_modules/polyscript/esm/worker/hooks.js";
import { robustFetch as fetch } from "./fetch.js";
const { assign, defineProperty } = Object;
const { assign, defineProperty, entries } = Object;
const getText = (body) => body.text();
@@ -163,6 +164,16 @@ define("py", {
shouldRegister = false;
registerModule(pyodide);
}
// load plugins unless specified otherwise
const toBeAwaited = [];
for (const [key, value] of entries(plugins)) {
if (!pyodide.config?.plugins?.includes(`!${key}`))
toBeAwaited.push(value());
}
if (toBeAwaited.length) await Promise.all(toBeAwaited);
// allows plugins to do whatever they want with the element
// before regular stuff happens in here
for (const callback of hooks.onInterpreterReady)

View File

@@ -0,0 +1,4 @@
// ⚠️ This file is an artifact: DO NOT MODIFY
export default {
error: () => import("./plugins/error.js"),
};

View File

@@ -0,0 +1,40 @@
// PyScript Error Plugin
import { hooks } from "../core.js";
hooks.onBeforeRun.add(function override(pyScript) {
// be sure this override happens only once
hooks.onBeforeRun.delete(override);
// trap generic `stderr` to propagate to it regardless
const { stderr } = pyScript.io;
// override it with our own logic
pyScript.io.stderr = (...args) => {
// grab the message of the first argument (Error)
const [{ message }] = args;
// show it
notify(message);
// still let other plugins or PyScript itself do the rest
return stderr(...args);
};
});
// Error hook utilities
// Custom function to show notifications
function notify(message) {
const div = document.createElement("div");
div.className = "py-error";
div.textContent = message;
div.style.cssText = `
border: 1px solid red;
background: #ffdddd;
color: black;
font-family: courier, monospace;
white-space: pre;
overflow-x: auto;
padding: 8px;
margin-top: 8px;
`;
document.body.append(div);
}

View File

@@ -1,7 +1,8 @@
<!doctype html>
<html lang="en">
<head>
<script type="module" src="../core.js"></script>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<py-script async>

View File

@@ -1,8 +1,8 @@
<!doctype html>
<html>
<head>
<style>py-script{display:none}</style>
<script type="module" src="../core.js"></script>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
<script type="module">
customElements.whenDefined('py-script').then(PyScript => {
const textContent = `

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Next Plugin</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
<script type="py">
print(1, 2, 3)
first()
</script>
<py-script>
print(4, 5, 6)
second()
</py-script>
</head>
</html>

View File

@@ -0,0 +1,39 @@
// PyScript Error Plugin
import { hooks } from '@pyscript/core';
hooks.onBeforeRun.add(function override(pyScript) {
// be sure this override happens only once
hooks.onBeforeRun.delete(override);
// trap generic `stderr` to propagate to it regardless
const { stderr } = pyScript.io;
// override it with our own logic
pyScript.io.stderr = (...args) => {
// grab the message of the first argument (Error)
const [ { message } ] = args;
// show it
notify(message);
// still let other plugins or PyScript itself do the rest
return stderr(...args);
};
});
// Error hook utilities
// Custom function to show notifications
function notify(message) {
const div = document.createElement('div');
div.textContent = message;
div.style.cssText = `
border: 1px solid red;
background: #ffdddd;
color: black;
font-family: courier, monospace;
white-space: pre;
overflow-x: auto;
padding: 8px;
margin-top: 8px;
`;
document.body.append(div);
}

View File

@@ -1,8 +1,8 @@
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="../core.css" />
<script type="module" src="../core.js"></script>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<body>

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyScript Next</title>
<link rel="stylesheet" href="../core.css" />
<script type="module" src="../core.js"></script>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<script type="py">

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Next No Plugin</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
<py-config>plugins = ['!error']</py-config>
<script type="py">
print(1, 2, 3)
first()
</script>
<py-script>
print(4, 5, 6)
second()
</py-script>
</head>
</html>

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@pyscript/core</title>
<link rel="stylesheet" href="../core.css" />
<script type="module" src="../core.js"></script>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<py-script id="first">
@@ -28,7 +28,7 @@
# Use the 'target' element to specify the ID of an element in the DOM to write the content to
display("!", target="first", append=True)
</py-script>
<py-script worker>\
<py-script worker>
# Appears in a DIV that is a child of this py-script tag, even with the code running in a worker
from pyscript import display
display("worker", append=True)

View File

@@ -4,11 +4,11 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyScript Next</title>
<link rel="stylesheet" href="../core.css" />
<link rel="stylesheet" href="../dist/core.css">
<!-- the PyWorker approach -->
<script type="module">
import { PyWorker } from '../core.js';
import { PyWorker } from '../dist/core.js';
PyWorker('./worker.py', {config: {fetch: [{files: ['./a.py']}]}});
// the type is overwritten as "pyodide" in PyScript as the module
// lives in that env too

View File

@@ -1,10 +0,0 @@
export { ie as default };
declare function ie(e: any, ...r: any[]): any;
declare namespace ie {
import transfer = m.transfer;
export { transfer };
}
declare function m(t: any, { parse: n, stringify: r, transform: u }?: JSON): any;
declare namespace m {
function transfer(...e: any[]): any[];
}

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

@@ -0,0 +1,4 @@
declare namespace _default {
function error(): Promise<typeof import("./plugins/error.js")>;
}
export default _default;

View File

@@ -0,0 +1 @@
export {};

View File

@@ -1,54 +0,0 @@
export const CUSTOM_SELECTORS: any[];
export function handleCustomType(node: Element): void;
export function define(type: string, options: CustomOptions): void;
export function whenDefined(type: string): Promise<object>;
/**
* custom configuration
*/
export type Runtime = {
/**
* the bootstrapped interpreter
*/
interpreter: object;
/**
* an XWorker constructor that defaults to same interpreter on the Worker.
*/
XWorker: (url: string, options?: object) => Worker;
/**
* a cloned config used to bootstrap the interpreter
*/
config: object;
/**
* an utility to run code within the interpreter
*/
run: (code: string) => any;
/**
* an utility to run code asynchronously within the interpreter
*/
runAsync: (code: string) => Promise<any>;
/**
* an utility to write a file in the virtual FS, if available
*/
writeFile: (path: string, data: ArrayBuffer) => void;
};
/**
* custom configuration
*/
export type CustomOptions = {
/**
* the interpreter to use
*/
interpreter: 'pyodide' | 'micropython' | 'wasmoon' | 'ruby-wasm-wasi';
/**
* the optional interpreter version to use
*/
version?: string;
/**
* the optional config to use within such interpreter
*/
config?: string;
/**
* the callback that will be invoked once
*/
onInterpreterReady?: (environment: object, node: Element) => void;
};

View File

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

View File

@@ -1,3 +0,0 @@
export { env } from "./listeners.js";
export const XWorker: (url: string, options?: import("./worker/class.js").WorkerOptions) => Worker;
export { define, whenDefined } from "./custom.js";

View File

@@ -1,4 +0,0 @@
export function registerJSModule(interpreter: any, name: any, value: any): void;
export function run(interpreter: any, code: any): any;
export function runAsync(interpreter: any, code: any): any;
export function runEvent(interpreter: any, code: any, event: any): Promise<void>;

View File

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

View File

@@ -1,25 +0,0 @@
declare namespace _default {
export { type };
export function module(version?: string): string;
export function engine({ loadMicroPython }: {
loadMicroPython: any;
}, config: any, url: any): Promise<any>;
export { registerJSModule };
export { run };
export { runAsync };
export { runEvent };
export function transform(_: any, value: any): any;
export function writeFile({ FS, _module: { PATH, PATH_FS } }: {
FS: any;
_module: {
PATH: any;
PATH_FS: any;
};
}, path: any, buffer: any): any;
}
export default _default;
declare const type: "micropython";
import { registerJSModule } from './_python.js';
import { run } from './_python.js';
import { runAsync } from './_python.js';
import { runEvent } from './_python.js';

View File

@@ -1,25 +0,0 @@
declare namespace _default {
export { type };
export function module(version?: string): string;
export function engine({ loadPyodide }: {
loadPyodide: any;
}, config: any, url: any): Promise<any>;
export { registerJSModule };
export { run };
export { runAsync };
export { runEvent };
export function transform(interpreter: any, value: any): any;
export function writeFile({ FS, PATH, _module: { PATH_FS } }: {
FS: any;
PATH: any;
_module: {
PATH_FS: any;
};
}, path: any, buffer: any): any;
}
export default _default;
declare const type: "pyodide";
import { registerJSModule } from './_python.js';
import { run } from './_python.js';
import { runAsync } from './_python.js';
import { runEvent } from './_python.js';

View File

@@ -1,16 +0,0 @@
declare namespace _default {
export { type };
export let experimental: boolean;
export function module(version?: string): string;
export function engine({ DefaultRubyVM }: {
DefaultRubyVM: any;
}, config: any, url: any): Promise<any>;
export function registerJSModule(interpreter: any, _: any, value: any): void;
export function run(interpreter: any, code: any): any;
export function runAsync(interpreter: any, code: any): any;
export function runEvent(interpreter: any, code: any, event: any): Promise<void>;
export function transform(_: any, value: any): any;
export function writeFile(): never;
}
export default _default;
declare const type: "ruby-wasm-wasi";

View File

@@ -1,22 +0,0 @@
declare namespace _default {
export { type };
export function module(version?: string): string;
export function engine({ LuaFactory, LuaLibraries }: {
LuaFactory: any;
LuaLibraries: any;
}, config: any): Promise<any>;
export function registerJSModule(interpreter: any, _: any, value: any): void;
export function run(interpreter: any, code: any): any;
export function runAsync(interpreter: any, code: any): any;
export function runEvent(interpreter: any, code: any, event: any): Promise<void>;
export function transform(_: any, value: any): any;
export function writeFile({ cmodule: { module: { FS }, }, }: {
cmodule: {
module: {
FS: any;
};
};
}, path: any, buffer: any): any;
}
export default _default;
declare const type: "wasmoon";

View File

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

View File

@@ -1,3 +0,0 @@
export const env: any;
export function listener(event: any): Promise<void>;
export function addAllListeners(root: Document | Element): void;

View File

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

View File

@@ -1,4 +0,0 @@
export function queryTarget(script: any, idOrSelector: any): any;
export const interpreters: Map<any, any>;
export function getDetails(type: any, id: any, name: any, version: any, config: any, runtime?: any): any;
export function handle(script: HTMLScriptElement): Promise<void>;

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
export class Hook {
constructor(interpreter: any, options: any);
interpreter: any;
onWorkerReady: any;
get stringHooks(): {};
}

View File

@@ -1,2 +0,0 @@
declare function _default(): Worker;
export default _default;

View File

@@ -1,25 +0,0 @@
/**
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
* @param {string} file the python file to run ina worker.
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
* @returns {Worker & {sync: ProxyHandler<object>}}
*/
export function PyWorker(file: string, options?: {
config?: string | object;
async?: boolean;
}): Worker & {
sync: ProxyHandler<object>;
};
export namespace hooks {
let onBeforeRun: Set<Function>;
let onBeforeRunAync: Set<Function>;
let onAfterRun: Set<Function>;
let onAfterRunAsync: Set<Function>;
let onInterpreterReady: Set<Function>;
let codeBeforeRunWorker: Set<string>;
let codeBeforeRunWorkerAsync: Set<string>;
let codeAfterRunWorker: Set<string>;
let codeAfterRunWorkerAsync: Set<string>;
}
declare let config: any;
export {};

View File

@@ -1,27 +0,0 @@
export function _createAlertBanner(message: any, level: any, messageType?: string, logMessage?: boolean): void;
export namespace ErrorCode {
let GENERIC: string;
let FETCH_ERROR: string;
let FETCH_NAME_ERROR: string;
let FETCH_UNAUTHORIZED_ERROR: string;
let FETCH_FORBIDDEN_ERROR: string;
let FETCH_NOT_FOUND_ERROR: string;
let FETCH_SERVER_ERROR: string;
let FETCH_UNAVAILABLE_ERROR: string;
let BAD_CONFIG: string;
let MICROPIP_INSTALL_ERROR: string;
let BAD_PLUGIN_FILE_EXTENSION: string;
let NO_DEFAULT_EXPORT: string;
let TOP_LEVEL_AWAIT: string;
}
export class UserError extends Error {
constructor(errorCode: any, message?: string, messageType?: string);
errorCode: any;
messageType: string;
}
export class FetchError extends UserError {
constructor(errorCode: any, message: any);
}
export class InstallError extends UserError {
constructor(errorCode: any, message: any);
}

View File

@@ -1,10 +0,0 @@
/**
* 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 function robustFetch(url: string, options?: Request): Promise<Response>;