@pyscript/core 0.2.10 → 0.3.1

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.
@@ -0,0 +1,2 @@
1
+ import{hooks as e}from"./core.js";function o(e){const o=document.createElement("div");o.className="py-error",o.textContent=e,o.style.cssText="\n border: 1px solid red;\n background: #ffdddd;\n color: black;\n font-family: courier, monospace;\n white-space: pre;\n overflow-x: auto;\n padding: 8px;\n margin-top: 8px;\n ",document.body.append(o)}e.main.onReady.add((function n(r){e.main.onReady.delete(n);const{stderr:t}=r.io;r.io.stderr=(e,...n)=>(o(e.message||e),t(e,...n)),addEventListener("error",(({message:e})=>{e.startsWith("Uncaught PythonError")&&o(e)}))}));export{o as notify};
2
+ //# sourceMappingURL=error-96hMSEw8.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-96hMSEw8.js","sources":["../src/plugins/error.js"],"sourcesContent":["// PyScript Error Plugin\nimport { hooks } from \"../core.js\";\n\nhooks.main.onReady.add(function override(pyScript) {\n // be sure this override happens only once\n hooks.main.onReady.delete(override);\n\n // trap generic `stderr` to propagate to it regardless\n const { stderr } = pyScript.io;\n\n // override it with our own logic\n pyScript.io.stderr = (error, ...rest) => {\n notify(error.message || error);\n // let other plugins or stderr hook, if any, do the rest\n return stderr(error, ...rest);\n };\n\n // be sure uncaught Python errors are also visible\n addEventListener(\"error\", ({ message }) => {\n if (message.startsWith(\"Uncaught PythonError\")) notify(message);\n });\n});\n\n// Error hook utilities\n\n// Custom function to show notifications\n\n/**\n * Add a banner to the top of the page, notifying the user of an error\n * @param {string} message\n */\nexport function notify(message) {\n const div = document.createElement(\"div\");\n div.className = \"py-error\";\n div.textContent = message;\n div.style.cssText = `\n border: 1px solid red;\n background: #ffdddd;\n color: black;\n font-family: courier, monospace;\n white-space: pre;\n overflow-x: auto;\n padding: 8px;\n margin-top: 8px;\n `;\n document.body.append(div);\n}\n"],"names":["notify","message","div","document","createElement","className","textContent","style","cssText","body","append","hooks","main","onReady","add","override","pyScript","delete","stderr","io","error","rest","addEventListener","startsWith"],"mappings":"kCA+BO,SAASA,EAAOC,GACnB,MAAMC,EAAMC,SAASC,cAAc,OACnCF,EAAIG,UAAY,WAChBH,EAAII,YAAcL,EAClBC,EAAIK,MAAMC,QAAU,6MAUpBL,SAASM,KAAKC,OAAOR,EACzB,CA3CAS,EAAMC,KAAKC,QAAQC,KAAI,SAASC,EAASC,GAErCL,EAAMC,KAAKC,QAAQI,OAAOF,GAG1B,MAAMG,OAAEA,GAAWF,EAASG,GAG5BH,EAASG,GAAGD,OAAS,CAACE,KAAUC,KAC5BrB,EAAOoB,EAAMnB,SAAWmB,GAEjBF,EAAOE,KAAUC,IAI5BC,iBAAiB,SAAS,EAAGrB,cACrBA,EAAQsB,WAAW,yBAAyBvB,EAAOC,EAAQ,GAEvE"}
@@ -0,0 +1,2 @@
1
+ import{TYPES as e,hooks as t}from"./core.js";const r="https://cdn.jsdelivr.net/npm/xterm",n="5.3.0",o=[...e.keys()].map((e=>`script[type="${e}"][terminal],${e}-script[terminal]`)).join(","),i=async()=>{const e=document.querySelectorAll(o);if(!e.length)return;e.length>1&&console.warn("Unable to satisfy multiple terminals"),s.disconnect();const[i]=e;if(i.matches('script[type="mpy"],mpy-script'))throw new Error("Unsupported terminal");document.querySelector(`link[href^="${r}"]`)||document.head.append(Object.assign(document.createElement("link"),{rel:"stylesheet",href:`${r}@${n}/css/xterm.min.css`}));const[{Terminal:d},{Readline:a}]=await Promise.all([import(`${r}@${n}/+esm`),import(`${r}-readline@1.1.1/+esm`)]),c=new a,l=e=>{let t=i;const r=i.getAttribute("target");if(r){if(t=document.getElementById(r)||document.querySelector(r),!t)throw new Error(`Unknown target ${r}`)}else t=document.createElement(`${i.type}-terminal`),t.style.display="block",i.after(t);const n=new d({theme:{background:"#191A19",foreground:"#F5F2E7"},...e});n.loadAddon(c),n.open(t),n.focus()};if(i.hasAttribute("worker")){const e=({interpreter:e},{sync:t})=>{t.pyterminal_drop_hooks();const r=new TextDecoder,n={isatty:!0,write:e=>(t.pyterminal_write(r.decode(e)),e.length)};e.setStdout(n),e.setStderr(n)},r="\n import builtins\n from pyscript import sync as _sync\n\n builtins.input = lambda prompt: _sync.pyterminal_read(prompt)\n ",n="\n import code as _code\n _code.interact()\n ";t.main.onWorker.add((function o(i,s){t.main.onWorker.delete(o),l({disableStdin:!1,cursorBlink:!0,cursorStyle:"block"}),s.sync.pyterminal_read=c.read.bind(c),s.sync.pyterminal_write=c.write.bind(c),s.sync.pyterminal_drop_hooks=()=>{t.worker.onReady.delete(e),t.worker.codeBeforeRun.delete(r),t.worker.codeAfterRun.delete(n)}})),t.worker.onReady.add(e),t.worker.codeBeforeRun.add(r),t.worker.codeAfterRun.add(n)}else t.main.onReady.add((function e({io:r}){console.warn("py-terminal is read only on main thread"),t.main.onReady.delete(e),l({disableStdin:!0,cursorBlink:!1,cursorStyle:"underline"}),r.stdout=e=>{c.write(`${e}\n`)},r.stderr=e=>{c.write(`${e.message||e}\n`)}}))},s=new MutationObserver(i);s.observe(document,{childList:!0,subtree:!0});var d=i();export{d as default};
2
+ //# sourceMappingURL=py-terminal-nF9DKGa8.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"py-terminal-nF9DKGa8.js","sources":["../src/plugins/py-terminal.js"],"sourcesContent":["// PyScript py-terminal plugin\nimport { TYPES, hooks } from \"../core.js\";\n\nconst CDN = \"https://cdn.jsdelivr.net/npm/xterm\";\nconst XTERM = \"5.3.0\";\nconst XTERM_READLINE = \"1.1.1\";\nconst SELECTOR = [...TYPES.keys()]\n .map((type) => `script[type=\"${type}\"][terminal],${type}-script[terminal]`)\n .join(\",\");\n\nconst pyTerminal = async () => {\n const terminals = document.querySelectorAll(SELECTOR);\n\n // no results will look further for runtime nodes\n if (!terminals.length) return;\n\n // we currently support only one terminal as in \"classic\"\n if (terminals.length > 1)\n console.warn(\"Unable to satisfy multiple terminals\");\n\n // if we arrived this far, let's drop the MutationObserver\n mo.disconnect();\n\n const [element] = terminals;\n // hopefully to be removed in the near future!\n if (element.matches('script[type=\"mpy\"],mpy-script'))\n throw new Error(\"Unsupported terminal\");\n\n // import styles once and lazily (only on valid terminal)\n if (!document.querySelector(`link[href^=\"${CDN}\"]`)) {\n document.head.append(\n Object.assign(document.createElement(\"link\"), {\n rel: \"stylesheet\",\n href: `${CDN}@${XTERM}/css/xterm.min.css`,\n }),\n );\n }\n\n // lazy load these only when a valid terminal is found\n const [{ Terminal }, { Readline }] = await Promise.all([\n import(/* webpackIgnore: true */ `${CDN}@${XTERM}/+esm`),\n import(\n /* webpackIgnore: true */ `${CDN}-readline@${XTERM_READLINE}/+esm`\n ),\n ]);\n\n const readline = new Readline();\n\n // common main thread initialization for both worker\n // or main case, bootstrapping the terminal on its target\n const init = (options) => {\n let target = element;\n const selector = element.getAttribute(\"target\");\n if (selector) {\n target =\n document.getElementById(selector) ||\n document.querySelector(selector);\n if (!target) throw new Error(`Unknown target ${selector}`);\n } else {\n target = document.createElement(`${element.type}-terminal`);\n target.style.display = \"block\";\n element.after(target);\n }\n const terminal = new Terminal({\n theme: {\n background: \"#191A19\",\n foreground: \"#F5F2E7\",\n },\n ...options,\n });\n terminal.loadAddon(readline);\n terminal.open(target);\n terminal.focus();\n };\n\n // branch logic for the worker\n if (element.hasAttribute(\"worker\")) {\n // when the remote thread onReady triggers:\n // setup the interpreter stdout and stderr\n const workerReady = ({ interpreter }, { sync }) => {\n sync.pyterminal_drop_hooks();\n const decoder = new TextDecoder();\n const generic = {\n isatty: true,\n write(buffer) {\n sync.pyterminal_write(decoder.decode(buffer));\n return buffer.length;\n },\n };\n interpreter.setStdout(generic);\n interpreter.setStderr(generic);\n };\n\n // run in python code able to replace builtins.input\n // using the xworker.sync non blocking prompt\n const codeBefore = `\n import builtins\n from pyscript import sync as _sync\n\n builtins.input = lambda prompt: _sync.pyterminal_read(prompt)\n `;\n\n // at the end of the code, make the terminal interactive\n const codeAfter = `\n import code as _code\n _code.interact()\n `;\n\n // add a hook on the main thread to setup all sync helpers\n // also bootstrapping the XTerm target on main\n hooks.main.onWorker.add(function worker(_, xworker) {\n hooks.main.onWorker.delete(worker);\n init({\n disableStdin: false,\n cursorBlink: true,\n cursorStyle: \"block\",\n });\n xworker.sync.pyterminal_read = readline.read.bind(readline);\n xworker.sync.pyterminal_write = readline.write.bind(readline);\n // allow a worker to drop main thread hooks ASAP\n xworker.sync.pyterminal_drop_hooks = () => {\n hooks.worker.onReady.delete(workerReady);\n hooks.worker.codeBeforeRun.delete(codeBefore);\n hooks.worker.codeAfterRun.delete(codeAfter);\n };\n });\n\n // setup remote thread JS/Python code for whenever the\n // worker is ready to become a terminal\n hooks.worker.onReady.add(workerReady);\n hooks.worker.codeBeforeRun.add(codeBefore);\n hooks.worker.codeAfterRun.add(codeAfter);\n } else {\n // in the main case, just bootstrap XTerm without\n // allowing any input as that's not possible / awkward\n hooks.main.onReady.add(function main({ io }) {\n console.warn(\"py-terminal is read only on main thread\");\n hooks.main.onReady.delete(main);\n init({\n disableStdin: true,\n cursorBlink: false,\n cursorStyle: \"underline\",\n });\n io.stdout = (value) => {\n readline.write(`${value}\\n`);\n };\n io.stderr = (error) => {\n readline.write(`${error.message || error}\\n`);\n };\n });\n }\n};\n\nconst mo = new MutationObserver(pyTerminal);\nmo.observe(document, { childList: true, subtree: true });\n\n// try to check the current document ASAP\nexport default pyTerminal();\n"],"names":["CDN","XTERM","SELECTOR","TYPES","keys","map","type","join","pyTerminal","async","terminals","document","querySelectorAll","length","console","warn","mo","disconnect","element","matches","Error","querySelector","head","append","Object","assign","createElement","rel","href","Terminal","Readline","Promise","all","import","readline","init","options","target","selector","getAttribute","getElementById","style","display","after","terminal","theme","background","foreground","loadAddon","open","focus","hasAttribute","workerReady","interpreter","sync","pyterminal_drop_hooks","decoder","TextDecoder","generic","isatty","write","buffer","pyterminal_write","decode","setStdout","setStderr","codeBefore","codeAfter","hooks","main","onWorker","add","worker","_","xworker","delete","disableStdin","cursorBlink","cursorStyle","pyterminal_read","read","bind","onReady","codeBeforeRun","codeAfterRun","io","stdout","value","stderr","error","message","MutationObserver","observe","childList","subtree","pyTerminal$1"],"mappings":"6CAGA,MAAMA,EAAM,qCACNC,EAAQ,QAERC,EAAW,IAAIC,EAAMC,QACtBC,KAAKC,GAAS,gBAAgBA,iBAAoBA,uBAClDC,KAAK,KAEJC,EAAaC,UACf,MAAMC,EAAYC,SAASC,iBAAiBV,GAG5C,IAAKQ,EAAUG,OAAQ,OAGnBH,EAAUG,OAAS,GACnBC,QAAQC,KAAK,wCAGjBC,EAAGC,aAEH,MAAOC,GAAWR,EAElB,GAAIQ,EAAQC,QAAQ,iCAChB,MAAM,IAAIC,MAAM,wBAGfT,SAASU,cAAc,eAAerB,QACvCW,SAASW,KAAKC,OACVC,OAAOC,OAAOd,SAASe,cAAc,QAAS,CAC1CC,IAAK,aACLC,KAAM,GAAG5B,KAAOC,yBAM5B,OAAO4B,SAAEA,IAAYC,SAAEA,UAAoBC,QAAQC,IAAI,CACnDC,OAAiC,GAAGjC,KAAOC,UAC3CgC,OAC8B,GAAGjC,2BAI/BkC,EAAW,IAAIJ,EAIfK,EAAQC,IACV,IAAIC,EAASnB,EACb,MAAMoB,EAAWpB,EAAQqB,aAAa,UACtC,GAAID,GAIA,GAHAD,EACI1B,SAAS6B,eAAeF,IACxB3B,SAASU,cAAciB,IACtBD,EAAQ,MAAM,IAAIjB,MAAM,kBAAkBkB,UAE/CD,EAAS1B,SAASe,cAAc,GAAGR,EAAQZ,iBAC3C+B,EAAOI,MAAMC,QAAU,QACvBxB,EAAQyB,MAAMN,GAElB,MAAMO,EAAW,IAAIf,EAAS,CAC1BgB,MAAO,CACHC,WAAY,UACZC,WAAY,cAEbX,IAEPQ,EAASI,UAAUd,GACnBU,EAASK,KAAKZ,GACdO,EAASM,OAAO,EAIpB,GAAIhC,EAAQiC,aAAa,UAAW,CAGhC,MAAMC,EAAc,EAAGC,gBAAiBC,WACpCA,EAAKC,wBACL,MAAMC,EAAU,IAAIC,YACdC,EAAU,CACZC,QAAQ,EACRC,MAAMC,IACFP,EAAKQ,iBAAiBN,EAAQO,OAAOF,IAC9BA,EAAOhD,SAGtBwC,EAAYW,UAAUN,GACtBL,EAAYY,UAAUP,EAAQ,EAK5BQ,EAAa,uKAQbC,EAAY,6EAOlBC,EAAMC,KAAKC,SAASC,KAAI,SAASC,EAAOC,EAAGC,GACvCN,EAAMC,KAAKC,SAASK,OAAOH,GAC3BrC,EAAK,CACDyC,cAAc,EACdC,aAAa,EACbC,YAAa,UAEjBJ,EAAQpB,KAAKyB,gBAAkB7C,EAAS8C,KAAKC,KAAK/C,GAClDwC,EAAQpB,KAAKQ,iBAAmB5B,EAAS0B,MAAMqB,KAAK/C,GAEpDwC,EAAQpB,KAAKC,sBAAwB,KACjCa,EAAMI,OAAOU,QAAQP,OAAOvB,GAC5BgB,EAAMI,OAAOW,cAAcR,OAAOT,GAClCE,EAAMI,OAAOY,aAAaT,OAAOR,EAAU,CAE3D,IAIQC,EAAMI,OAAOU,QAAQX,IAAInB,GACzBgB,EAAMI,OAAOW,cAAcZ,IAAIL,GAC/BE,EAAMI,OAAOY,aAAab,IAAIJ,EACtC,MAGQC,EAAMC,KAAKa,QAAQX,KAAI,SAASF,GAAKgB,GAAEA,IACnCvE,QAAQC,KAAK,2CACbqD,EAAMC,KAAKa,QAAQP,OAAON,GAC1BlC,EAAK,CACDyC,cAAc,EACdC,aAAa,EACbC,YAAa,cAEjBO,EAAGC,OAAUC,IACTrD,EAAS0B,MAAM,GAAG2B,MAAU,EAEhCF,EAAGG,OAAUC,IACTvD,EAAS0B,MAAM,GAAG6B,EAAMC,SAAWD,MAAU,CAE7D,GACK,EAGCzE,EAAK,IAAI2E,iBAAiBnF,GAChCQ,EAAG4E,QAAQjF,SAAU,CAAEkF,WAAW,EAAMC,SAAS,IAGjD,IAAAC,EAAevF"}
package/dist.zip ADDED
Binary file
package/docs/README.md CHANGED
@@ -159,7 +159,7 @@ The commonly shared utilities are:
159
159
  * **display** in both main and worker, refers to the good old `display` utility except:
160
160
  * in the *main* it automatically uses the current script `target` to display content
161
161
  * in the *worker* it still needs to know *where* to display content using the `target="dom-id"` named argument, as workers don't get a default target attached
162
- * in both main and worker, the `append=False` is the *default* behavior, which is a breaking change compared to classic PyScript, but because there is no `Element` with its `write(content)` utility, which would have used that `append=False` behind the scene, we've decided that `false` as append default is more desired, specially after porting most examples in *PyScript Next*, where `append=True` is the exception, not the norm.
162
+ * in both main and worker, the `append=True` is the *default* behavior, which is inherited from the classic PyScript.
163
163
 
164
164
  #### Extra main-only features
165
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyscript/core",
3
- "version": "0.2.10",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "PyScript",
6
6
  "module": "./index.js",
@@ -20,9 +20,17 @@
20
20
  },
21
21
  "scripts": {
22
22
  "server": "npx static-handler --coi .",
23
- "build": "node rollup/toml.cjs && node rollup/stdlib.cjs && node rollup/plugins.cjs && rm -rf dist && rollup --config rollup/core.config.js && eslint src/ && npm run ts",
23
+ "build": "npm run build:toml && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy",
24
+ "build:core": "rm -rf dist && rollup --config rollup/core.config.js",
25
+ "build:plugins": "node rollup/plugins.cjs",
26
+ "build:stdlib": "node rollup/stdlib.cjs",
27
+ "build:toml": "node rollup/toml.cjs",
28
+ "test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/ || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
29
+ "dev": "node dev.cjs",
30
+ "release": "npm run build && npm run zip",
24
31
  "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",
25
- "ts": "tsc -p ."
32
+ "ts": "tsc -p .",
33
+ "zip": "zip -r dist.zip ./dist"
26
34
  },
27
35
  "keywords": [
28
36
  "pyscript",
@@ -33,16 +41,19 @@
33
41
  "dependencies": {
34
42
  "@ungap/with-resolvers": "^0.1.0",
35
43
  "basic-devtools": "^0.1.6",
36
- "polyscript": "^0.4.20",
44
+ "polyscript": "^0.5.6",
37
45
  "sticky-module": "^0.1.0",
46
+ "to-json-callback": "^0.1.1",
38
47
  "type-checked-collections": "^0.1.7"
39
48
  },
40
49
  "devDependencies": {
50
+ "@playwright/test": "^1.39.0",
41
51
  "@rollup/plugin-node-resolve": "^15.2.3",
42
52
  "@rollup/plugin-terser": "^0.4.4",
43
53
  "@webreflection/toml-j0.4": "^1.1.3",
44
- "eslint": "^8.51.0",
45
- "rollup": "^4.1.4",
54
+ "chokidar": "^3.5.3",
55
+ "eslint": "^8.52.0",
56
+ "rollup": "^4.2.0",
46
57
  "rollup-plugin-postcss": "^4.0.2",
47
58
  "rollup-plugin-string": "^3.0.0",
48
59
  "static-handler": "^0.4.3",
package/src/config.js CHANGED
@@ -113,7 +113,7 @@ for (const [TYPE] of TYPES) {
113
113
  value().then(({ notify }) => notify(error.message));
114
114
  }
115
115
  } else if (!parsed?.plugins?.includes(`!${key}`)) {
116
- toBeAwaited.push(value());
116
+ toBeAwaited.push(value().then(({ default: p }) => p));
117
117
  }
118
118
  }
119
119
 
package/src/core.js CHANGED
@@ -14,16 +14,17 @@ import {
14
14
  dispatch,
15
15
  queryTarget,
16
16
  unescape,
17
+ whenDefined,
17
18
  } from "polyscript/exports";
18
19
 
19
20
  import "./all-done.js";
20
21
  import TYPES from "./types.js";
21
22
  import configs from "./config.js";
22
- import hooks from "./hooks.js";
23
23
  import sync from "./sync.js";
24
- import stdlib from "./stdlib.js";
24
+ import bootstrapNodeAndPlugins from "./plugins-helper.js";
25
25
  import { ErrorCode } from "./exceptions.js";
26
26
  import { robustFetch as fetch, getText } from "./fetch.js";
27
+ import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
27
28
 
28
29
  // allows lazy element features on code evaluation
29
30
  let currentElement;
@@ -31,25 +32,6 @@ let currentElement;
31
32
  // generic helper to disambiguate between custom element and script
32
33
  const isScript = ({ tagName }) => tagName === "SCRIPT";
33
34
 
34
- // helper for all script[type="py"] out there
35
- const before = (script) => {
36
- defineProperty(document, "currentScript", {
37
- configurable: true,
38
- get: () => script,
39
- });
40
- };
41
-
42
- const after = () => {
43
- delete document.currentScript;
44
- };
45
-
46
- // common life-cycle handlers for any node
47
- const bootstrapNodeAndPlugins = (wrap, element, callback, hook) => {
48
- // make it possible to reach the current target node via Python
49
- callback(element);
50
- for (const fn of hooks[hook]) fn(wrap, element);
51
- };
52
-
53
35
  let shouldRegister = true;
54
36
  const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
55
37
  // automatically use the pyscript stderr (when/if defined)
@@ -69,19 +51,6 @@ const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
69
51
  : currentElement.id;
70
52
  },
71
53
  });
72
-
73
- interpreter.runPython(stdlib, { globals: interpreter.runPython("{}") });
74
- };
75
-
76
- const workerHooks = {
77
- codeBeforeRunWorker: () =>
78
- [stdlib, ...hooks.codeBeforeRunWorker].map(dedent).join("\n"),
79
- codeBeforeRunWorkerAsync: () =>
80
- [stdlib, ...hooks.codeBeforeRunWorkerAsync].map(dedent).join("\n"),
81
- codeAfterRunWorker: () =>
82
- [...hooks.codeAfterRunWorker].map(dedent).join("\n"),
83
- codeAfterRunWorkerAsync: () =>
84
- [...hooks.codeAfterRunWorkerAsync].map(dedent).join("\n"),
85
54
  };
86
55
 
87
56
  // avoid multiple initialization of the same library
@@ -90,16 +59,26 @@ const [
90
59
  PyWorker: exportedPyWorker,
91
60
  hooks: exportedHooks,
92
61
  config: exportedConfig,
62
+ whenDefined: exportedWhenDefined,
93
63
  },
94
64
  alreadyLive,
95
- ] = stickyModule("@pyscript/core", { PyWorker, hooks, config: {} });
65
+ ] = stickyModule("@pyscript/core", {
66
+ PyWorker,
67
+ hooks,
68
+ config: {},
69
+ whenDefined,
70
+ });
96
71
 
97
72
  export {
73
+ TYPES,
98
74
  exportedPyWorker as PyWorker,
99
75
  exportedHooks as hooks,
100
76
  exportedConfig as config,
77
+ exportedWhenDefined as whenDefined,
101
78
  };
102
79
 
80
+ const hooked = new Map();
81
+
103
82
  for (const [TYPE, interpreter] of TYPES) {
104
83
  // avoid any dance if the module already landed
105
84
  if (alreadyLive) break;
@@ -149,107 +128,141 @@ for (const [TYPE, interpreter] of TYPES) {
149
128
  // possible early errors sent by polyscript
150
129
  const errors = new Map();
151
130
 
131
+ // specific main and worker hooks
132
+ const hooks = {
133
+ main: {
134
+ ...codeFor(main),
135
+ async onReady(wrap, element) {
136
+ if (shouldRegister) {
137
+ shouldRegister = false;
138
+ registerModule(wrap);
139
+ }
140
+
141
+ // allows plugins to do whatever they want with the element
142
+ // before regular stuff happens in here
143
+ for (const callback of main("onReady"))
144
+ await callback(wrap, element);
145
+
146
+ // now that all possible plugins are configured,
147
+ // bail out if polyscript encountered an error
148
+ if (errors.has(element)) {
149
+ let { message } = errors.get(element);
150
+ errors.delete(element);
151
+ const clone = message === INVALID_CONTENT;
152
+ message = `(${ErrorCode.CONFLICTING_CODE}) ${message} for `;
153
+ message += element.cloneNode(clone).outerHTML;
154
+ wrap.io.stderr(message);
155
+ return;
156
+ }
157
+
158
+ if (isScript(element)) {
159
+ const {
160
+ attributes: { async: isAsync, target },
161
+ } = element;
162
+ const hasTarget = !!target?.value;
163
+ const show = hasTarget
164
+ ? queryTarget(element, target.value)
165
+ : document.createElement("script-py");
166
+
167
+ if (!hasTarget) {
168
+ const { head, body } = document;
169
+ if (head.contains(element)) body.append(show);
170
+ else element.after(show);
171
+ }
172
+ if (!show.id) show.id = getID();
173
+
174
+ // allows the code to retrieve the target element via
175
+ // document.currentScript.target if needed
176
+ defineProperty(element, "target", { value: show });
177
+
178
+ // notify before the code runs
179
+ dispatch(element, TYPE, "ready");
180
+ dispatchDone(
181
+ element,
182
+ isAsync,
183
+ wrap[`run${isAsync ? "Async" : ""}`](
184
+ await fetchSource(element, wrap.io, true),
185
+ ),
186
+ );
187
+ } else {
188
+ // resolve PyScriptElement to allow connectedCallback
189
+ element._wrap.resolve(wrap);
190
+ }
191
+ console.debug("[pyscript/main] PyScript Ready");
192
+ },
193
+ onWorker(_, xworker) {
194
+ assign(xworker.sync, sync);
195
+ for (const callback of main("onWorker"))
196
+ callback(_, xworker);
197
+ },
198
+ onBeforeRun(wrap, element) {
199
+ currentElement = element;
200
+ bootstrapNodeAndPlugins(
201
+ main,
202
+ wrap,
203
+ element,
204
+ "onBeforeRun",
205
+ );
206
+ },
207
+ onBeforeRunAsync(wrap, element) {
208
+ currentElement = element;
209
+ return bootstrapNodeAndPlugins(
210
+ main,
211
+ wrap,
212
+ element,
213
+ "onBeforeRunAsync",
214
+ );
215
+ },
216
+ onAfterRun(wrap, element) {
217
+ bootstrapNodeAndPlugins(
218
+ main,
219
+ wrap,
220
+ element,
221
+ "onAfterRun",
222
+ );
223
+ },
224
+ onAfterRunAsync(wrap, element) {
225
+ return bootstrapNodeAndPlugins(
226
+ main,
227
+ wrap,
228
+ element,
229
+ "onAfterRunAsync",
230
+ );
231
+ },
232
+ },
233
+ worker: {
234
+ ...codeFor(worker),
235
+ // these are lazy getters that returns a composition
236
+ // of the current hooks or undefined, if no hook is present
237
+ get onReady() {
238
+ return createFunction(this, "onReady", true);
239
+ },
240
+ get onBeforeRun() {
241
+ return createFunction(this, "onBeforeRun", false);
242
+ },
243
+ get onBeforeRunAsync() {
244
+ return createFunction(this, "onBeforeRunAsync", true);
245
+ },
246
+ get onAfterRun() {
247
+ return createFunction(this, "onAfterRun", false);
248
+ },
249
+ get onAfterRunAsync() {
250
+ return createFunction(this, "onAfterRunAsync", true);
251
+ },
252
+ },
253
+ };
254
+
255
+ hooked.set(TYPE, hooks);
256
+
152
257
  define(TYPE, {
153
258
  config,
154
259
  interpreter,
260
+ hooks,
155
261
  env: `${TYPE}-script`,
156
262
  version: config?.interpreter,
157
263
  onerror(error, element) {
158
264
  errors.set(element, error);
159
265
  },
160
- ...workerHooks,
161
- onWorkerReady(_, xworker) {
162
- assign(xworker.sync, sync);
163
- for (const callback of hooks.onWorkerReady)
164
- callback(_, xworker);
165
- },
166
- onBeforeRun(wrap, element) {
167
- currentElement = element;
168
- bootstrapNodeAndPlugins(
169
- wrap,
170
- element,
171
- before,
172
- "onBeforeRun",
173
- );
174
- },
175
- onBeforeRunAsync(wrap, element) {
176
- currentElement = element;
177
- bootstrapNodeAndPlugins(
178
- wrap,
179
- element,
180
- before,
181
- "onBeforeRunAsync",
182
- );
183
- },
184
- onAfterRun(wrap, element) {
185
- bootstrapNodeAndPlugins(wrap, element, after, "onAfterRun");
186
- },
187
- onAfterRunAsync(wrap, element) {
188
- bootstrapNodeAndPlugins(
189
- wrap,
190
- element,
191
- after,
192
- "onAfterRunAsync",
193
- );
194
- },
195
- async onInterpreterReady(wrap, element) {
196
- if (shouldRegister) {
197
- shouldRegister = false;
198
- registerModule(wrap);
199
- }
200
-
201
- // allows plugins to do whatever they want with the element
202
- // before regular stuff happens in here
203
- for (const callback of hooks.onInterpreterReady)
204
- callback(wrap, element);
205
-
206
- // now that all possible plugins are configured,
207
- // bail out if polyscript encountered an error
208
- if (errors.has(element)) {
209
- let { message } = errors.get(element);
210
- errors.delete(element);
211
- const clone = message === INVALID_CONTENT;
212
- message = `(${ErrorCode.CONFLICTING_CODE}) ${message} for `;
213
- message += element.cloneNode(clone).outerHTML;
214
- wrap.io.stderr(message);
215
- return;
216
- }
217
-
218
- if (isScript(element)) {
219
- const {
220
- attributes: { async: isAsync, target },
221
- } = element;
222
- const hasTarget = !!target?.value;
223
- const show = hasTarget
224
- ? queryTarget(element, target.value)
225
- : document.createElement("script-py");
226
-
227
- if (!hasTarget) {
228
- const { head, body } = document;
229
- if (head.contains(element)) body.append(show);
230
- else element.after(show);
231
- }
232
- if (!show.id) show.id = getID();
233
-
234
- // allows the code to retrieve the target element via
235
- // document.currentScript.target if needed
236
- defineProperty(element, "target", { value: show });
237
-
238
- // notify before the code runs
239
- dispatch(element, TYPE, "ready");
240
- dispatchDone(
241
- element,
242
- isAsync,
243
- wrap[`run${isAsync ? "Async" : ""}`](
244
- await fetchSource(element, wrap.io, true),
245
- ),
246
- );
247
- } else {
248
- // resolve PyScriptElement to allow connectedCallback
249
- element._wrap.resolve(wrap);
250
- }
251
- console.debug("[pyscript/main] PyScript Ready");
252
- },
253
266
  });
254
267
 
255
268
  customElements.define(
@@ -305,12 +318,13 @@ for (const [TYPE, interpreter] of TYPES) {
305
318
  * @returns {Worker & {sync: ProxyHandler<object>}}
306
319
  */
307
320
  function PyWorker(file, options) {
321
+ const hooks = hooked.get("py");
308
322
  // this propagates pyscript worker hooks without needing a pyscript
309
323
  // bootstrap + it passes arguments and enforces `pyodide`
310
324
  // as the interpreter to use in the worker, as all hooks assume that
311
325
  // and as `pyodide` is the only default interpreter that can deal with
312
326
  // all the features we need to deliver pyscript out there.
313
- const xworker = XWorker.call(new Hook(null, workerHooks), file, {
327
+ const xworker = XWorker.call(new Hook(null, hooks), file, {
314
328
  type: "pyodide",
315
329
  ...options,
316
330
  });
package/src/hooks.js CHANGED
@@ -1,28 +1,91 @@
1
1
  import { typedSet } from "type-checked-collections";
2
+ import { dedent } from "polyscript/exports";
3
+ import toJSONCallback from "to-json-callback";
4
+
5
+ import stdlib from "./stdlib.js";
6
+
7
+ export const main = (name) => hooks.main[name];
8
+ export const worker = (name) => hooks.worker[name];
9
+
10
+ const code = (hooks, branch, key, lib) => {
11
+ hooks[key] = () => {
12
+ const arr = lib ? [lib] : [];
13
+ arr.push(...branch(key));
14
+ return arr.map(dedent).join("\n");
15
+ };
16
+ };
17
+
18
+ export const codeFor = (branch) => {
19
+ const hooks = {};
20
+ code(hooks, branch, `codeBeforeRun`, stdlib);
21
+ code(hooks, branch, `codeBeforeRunAsync`, stdlib);
22
+ code(hooks, branch, `codeAfterRun`);
23
+ code(hooks, branch, `codeAfterRunAsync`);
24
+ return hooks;
25
+ };
26
+
27
+ export const createFunction = (self, name) => {
28
+ const cbs = [...worker(name)];
29
+ if (cbs.length) {
30
+ const cb = toJSONCallback(
31
+ self[`_${name}`] ||
32
+ (name.endsWith("Async")
33
+ ? async (wrap, xworker, ...cbs) => {
34
+ for (const cb of cbs) await cb(wrap, xworker);
35
+ }
36
+ : (wrap, xworker, ...cbs) => {
37
+ for (const cb of cbs) cb(wrap, xworker);
38
+ }),
39
+ );
40
+ const a = cbs.map(toJSONCallback).join(", ");
41
+ return Function(`return(w,x)=>(${cb})(w,x,...[${a}])`)();
42
+ }
43
+ };
2
44
 
3
45
  const SetFunction = typedSet({ typeof: "function" });
4
46
  const SetString = typedSet({ typeof: "string" });
5
47
 
6
- export default {
7
- /** @type {Set<function>} */
8
- onInterpreterReady: new SetFunction(),
9
- /** @type {Set<function>} */
10
- onBeforeRun: new SetFunction(),
11
- /** @type {Set<function>} */
12
- onBeforeRunAsync: new SetFunction(),
13
- /** @type {Set<function>} */
14
- onAfterRun: new SetFunction(),
15
- /** @type {Set<function>} */
16
- onAfterRunAsync: new SetFunction(),
17
-
18
- /** @type {Set<function>} */
19
- onWorkerReady: new SetFunction(),
20
- /** @type {Set<string>} */
21
- codeBeforeRunWorker: new SetString(),
22
- /** @type {Set<string>} */
23
- codeBeforeRunWorkerAsync: new SetString(),
24
- /** @type {Set<string>} */
25
- codeAfterRunWorker: new SetString(),
26
- /** @type {Set<string>} */
27
- codeAfterRunWorkerAsync: new SetString(),
48
+ export const hooks = {
49
+ main: {
50
+ /** @type {Set<function>} */
51
+ onWorker: new SetFunction(),
52
+ /** @type {Set<function>} */
53
+ onReady: new SetFunction(),
54
+ /** @type {Set<function>} */
55
+ onBeforeRun: new SetFunction(),
56
+ /** @type {Set<function>} */
57
+ onBeforeRunAsync: new SetFunction(),
58
+ /** @type {Set<function>} */
59
+ onAfterRun: new SetFunction(),
60
+ /** @type {Set<function>} */
61
+ onAfterRunAsync: new SetFunction(),
62
+ /** @type {Set<string>} */
63
+ codeBeforeRun: new SetString(),
64
+ /** @type {Set<string>} */
65
+ codeBeforeRunAsync: new SetString(),
66
+ /** @type {Set<string>} */
67
+ codeAfterRun: new SetString(),
68
+ /** @type {Set<string>} */
69
+ codeAfterRunAsync: new SetString(),
70
+ },
71
+ worker: {
72
+ /** @type {Set<function>} */
73
+ onReady: new SetFunction(),
74
+ /** @type {Set<function>} */
75
+ onBeforeRun: new SetFunction(),
76
+ /** @type {Set<function>} */
77
+ onBeforeRunAsync: new SetFunction(),
78
+ /** @type {Set<function>} */
79
+ onAfterRun: new SetFunction(),
80
+ /** @type {Set<function>} */
81
+ onAfterRunAsync: new SetFunction(),
82
+ /** @type {Set<string>} */
83
+ codeBeforeRun: new SetString(),
84
+ /** @type {Set<string>} */
85
+ codeBeforeRunAsync: new SetString(),
86
+ /** @type {Set<string>} */
87
+ codeAfterRun: new SetString(),
88
+ /** @type {Set<string>} */
89
+ codeAfterRunAsync: new SetString(),
90
+ },
28
91
  };
@@ -1,9 +1,9 @@
1
1
  // PyScript Error Plugin
2
2
  import { hooks } from "../core.js";
3
3
 
4
- hooks.onInterpreterReady.add(function override(pyScript) {
4
+ hooks.main.onReady.add(function override(pyScript) {
5
5
  // be sure this override happens only once
6
- hooks.onInterpreterReady.delete(override);
6
+ hooks.main.onReady.delete(override);
7
7
 
8
8
  // trap generic `stderr` to propagate to it regardless
9
9
  const { stderr } = pyScript.io;