|
1 | 1 | # Plugins
|
2 | 2 |
|
3 |
| -** TODO: Finish this ** |
| 3 | + |
| 4 | +PyScript, like many other software plaforms, offers a Plugin API that can be used to extend its |
| 5 | +own functionality without the need to modify its own core. By using this API, users can add new |
| 6 | +features and distribute them as plugins. |
| 7 | + |
| 8 | +At the moment, PyScript supports plugins written in Javascript. These plugins can use PyScript |
| 9 | +Plugins API to define entry points and hooks so that the plugin can be collected and hook into |
| 10 | +the PyScript lifecycle events, with the ablity to modify and integrate the features of PyScript |
| 11 | +core itself. |
| 12 | + |
| 13 | +Here's an example of how a PyScript plugin looks like: |
| 14 | + |
| 15 | +```js |
| 16 | +// import the hooks from PyScript first... |
| 17 | +import { hooks } from "https://pyscript.net/releases/2023.11.1/core.js"; |
| 18 | + |
| 19 | +// Use the `main` attribute on hooks do define plugins that run on the main thread |
| 20 | +hooks.main.onReady.add((wrap, element) => { |
| 21 | + console.log("main", "onReady"); |
| 22 | + if (location.search === '?debug') { |
| 23 | + console.debug("main", "wrap", wrap); |
| 24 | + console.debug("main", "element", element); |
| 25 | + } |
| 26 | +}); |
| 27 | +hooks.main.onBeforeRun.add(() => { |
| 28 | + console.log("main", "onBeforeRun"); |
| 29 | +}); |
| 30 | +hooks.main.codeBeforeRun.add('print("main", "codeBeforeRun")'); |
| 31 | +hooks.main.codeAfterRun.add('print("main", "codeAfterRun")'); |
| 32 | +hooks.main.onAfterRun.add(() => { |
| 33 | + console.log("main", "onAfterRun"); |
| 34 | +}); |
| 35 | + |
| 36 | +// Use the `worker` attribute on hooks do define plugins that run on workers |
| 37 | +hooks.worker.onReady.add((wrap, xworker) => { |
| 38 | + console.log("worker", "onReady"); |
| 39 | + if (location.search === '?debug') { |
| 40 | + console.debug("worker", "wrap", wrap); |
| 41 | + console.debug("worker", "xworker", xworker); |
| 42 | + } |
| 43 | +}); |
| 44 | + |
| 45 | +hooks.worker.onBeforeRun.add(() => { |
| 46 | + console.log("worker", "onBeforeRun"); |
| 47 | +}); |
| 48 | + |
| 49 | +hooks.worker.codeBeforeRun.add('print("worker", "codeBeforeRun")'); |
| 50 | +hooks.worker.codeAfterRun.add('print("worker", "codeAfterRun")'); |
| 51 | +hooks.worker.onAfterRun.add(() => { |
| 52 | + console.log("worker", "onAfterRun"); |
| 53 | +}); |
| 54 | +``` |
| 55 | + |
| 56 | +That's it. |
| 57 | + |
| 58 | + |
| 59 | +## Plugins API |
| 60 | + |
| 61 | +As mentioned above, PyScript Plugins API exposes a set of hooks that can be used to intercept |
| 62 | +specific events in the lifecycle of a PyScript application and add or modify specific features |
| 63 | +of the platform itself. To better understand how it works it's important to understand the concepts |
| 64 | +around a PyScript application and plugins. |
| 65 | + |
| 66 | +### Code Execution Methods |
| 67 | + |
| 68 | +There are 2 mains PyScript Applications can execute code: the browser main |
| 69 | +[thread](https://developer.mozilla.org/en-US/docs/Glossary/Main_thread) and on |
| 70 | +[web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers). |
| 71 | + |
| 72 | +We highly recommend users to independently search the difference between the 2 methods to fully understand |
| 73 | +the difference and consequences but here's a short summary: |
| 74 | + |
| 75 | +* main thread: executing code in the browser main thread means the code is being executed in the same |
| 76 | +process where the browser processes user events and paints. By default, the browser uses this single thread |
| 77 | +to run all the JavaScript code in your page, as well as to perform layout, reflows, and garbage collection. |
| 78 | +This means that long-running code or blocking calls can or will block the thread, leading to an unresponsive |
| 79 | +page and a bad user experience. |
| 80 | +* web workers: code executed in workers actually run on "background" threads. This means the code can perform |
| 81 | +tasks without interfering with the user interface or other operations being perfomed in the main thread. While |
| 82 | +this adds great flexibility it's important to understand that workers actually have limited capabilities when |
| 83 | +comparing to code executed on the main thread. For instace, while PyScript offers a DOM API that actually can |
| 84 | +be used in web workers on the browser, by default, does not allow DOM operation in workers. So, in this case, |
| 85 | +if you just use `window` and `document` directly mapping the Javascript FFI provided directly by the interpreters |
| 86 | +we support (Pyodide and MicroPython). With that in mind, `from pyscript import window, document` will work and |
| 87 | +allow you to interact with the DOM while the following will not: |
| 88 | + |
| 89 | +``` |
| 90 | +from js import document, window |
| 91 | +``` |
| 92 | + |
| 93 | +or |
| 94 | + |
| 95 | +``` |
| 96 | +import js |
| 97 | +js.document |
| 98 | +``` |
| 99 | + |
| 100 | +or |
| 101 | + |
| 102 | +``` |
| 103 | +import js |
| 104 | +js.window |
| 105 | +``` |
| 106 | + |
| 107 | +will not. |
| 108 | + |
| 109 | +In general, we recommend executing your code on workers unless there are explicit reasons preventing users from |
| 110 | +doing that. |
| 111 | + |
| 112 | +### Lifecycle Events |
| 113 | + |
| 114 | +During the execution of a PyScript application there are specfic events that capture the beginning |
| 115 | +or the end of specific stages. Here are the main lifecycle events of a PyScript Application: |
| 116 | + |
| 117 | +Every script or tag running through PyScript inevitably passes through some main or worker thread related tasks. |
| 118 | + |
| 119 | +In both worlds (wither executing code in the main thread or on a web worker), the exact sequence of steps around code execution is the following: |
| 120 | + |
| 121 | + * **ready** - the DOM recognized the special script or tag and the associated interpreter is ready to work. A *JS* callback might be useful to instrument the interpreter before anything else happens. |
| 122 | + * **before run** - there could be some *JS* code setup specific for the script on the main thread, or the worker. This is similar to a generic *setup* callback in tests. |
| 123 | + * **code before run** - there could be some *PL* code to prepend to the one being executed. In this case the code is a string because it will be part of the evaluation. |
| 124 | + * **actual code** - the code in the script or tag or the `src` file specified in the script. This is not a hook, just the exact time the code gets executed in general. |
| 125 | + * **code after run** - there could be some *PL* code to append to the one being executed. Same as *before*, the code is a string targeting the foreign *PL*. |
| 126 | + * **after run** - there could be some *JS* to execute right after the whole code has been evaluated. This is similar to a generic *teardown* callback in tests. |
| 127 | + |
| 128 | +As most interpreters can run their code either *synchronously* or *asynchronously*, the very same sequence is guaranteed to run in order in both cases, and the difference is only around the naming convention [as we'll see below]. |
| 129 | + |
| 130 | +### Hooks |
| 131 | + |
| 132 | +Hooks are a especial mechanism that can be used to tell PyScript that your code wants to subscribe to specific events, allowing your code to get called |
| 133 | +by PyScript's event loop when a specific event happens. |
| 134 | + |
| 135 | +#### Main Hooks |
| 136 | + |
| 137 | +When it comes to *main* hooks all callbacks will receive a *wrapper* of the interpreter with its utilities, see the further section to know more, plus the element on the page that is going to execute its related code, being this a custom script/type or a custom tag. |
| 138 | + |
| 139 | +This is the list of all possible, yet **optional** hooks, a custom type can define for **main**: |
| 140 | + |
| 141 | +| name | example | behavior | |
| 142 | +| :------------------------ | :-------------------------------------------- | :-------- | |
| 143 | +| onReady | `onReady(wrap:Wrap, el:Element) {}` | If defined, it is invoked before any other hook to signal that the element is going to execute the code. For custom scripts, this hook is in charge of eventually running the content of the script, anyway it prefers to do so. | |
| 144 | +| onBeforeRun | `onBeforeRun(wrap:Wrap, el:Element) {}` | If defined, it is invoked before any other hook to signal that the element is going to execute the code. | |
| 145 | +| onBeforeRunAsync | `onBeforeRunAsync(wrap:Wrap, el:Element) {}` | Same as `onBeforeRun` except it's the one used whenever the script is `async`. | |
| 146 | +| codeBeforeRun | `codeBeforeRun: () => 'print("before")'` | If defined, prepend some code to evaluate right before the rest of the code gets executed. | |
| 147 | +| codeBeforeRunAsync | `codeBeforeRunAsync: () => 'print("before")'` | Same as `codeBeforeRun` except it's the one used whenever the script is `async`. | |
| 148 | +| codeAfterRun | `codeAfterRun: () => 'print("after")'` | If defined, append some code to evaluate right after the rest of the code already executed. | |
| 149 | +| codeAfterRunAsync | `codeAfterRunAsync: () => 'print("after")'` | Same as `codeAfterRun` except it's the one used whenever the script is `async`. | |
| 150 | +| onAfterRun | `onAfterRun(wrap:Wrap, el:Element) {}` | If defined, it is invoked after the foreign code has been executed already. | |
| 151 | +| onAfterRunAsync | `onAfterRunAsync(wrap:Wrap, el:Element) {}` | Same as `onAfterRun` except it's the one used whenever the script is `async`. | |
| 152 | +| onWorker | `onWorker(wrap = null, xworker) {}` | If defined, whenever a script or tag with a `worker` attribute is processed it gets triggered on the main thread, to allow to expose possible `xworker` features before the code gets executed within the worker thread. The `wrap` reference is most of the time `null` unless an explicit `XWorker` call has been initialized manually and/or there is an interpreter on the main thread (*very advanced use case*). Please **note** this is the only hook that doesn't exist in the *worker* counter list of hooks. | |
| 153 | + |
| 154 | +#### Worker Hooks |
| 155 | + |
| 156 | +When it comes to *worker* hooks, **all non code related callbacks must be serializable**, meaning that callbacks cannot use any outer scope reference, as these are forwarded as strings, hence evaluated after in the worker, to survive the main <-> worker `postMessage` dance. |
| 157 | + |
| 158 | +Here an example of what works and what doesn't: |
| 159 | + |
| 160 | +```js |
| 161 | +// this works 👍 |
| 162 | +define('pl', { |
| 163 | + interpreter: 'programming-lang', |
| 164 | + hooks: { |
| 165 | + worker: { |
| 166 | + onReady() { |
| 167 | + // NOT suggested, just as example! |
| 168 | + if (!('i' in globalThis)) |
| 169 | + globalThis.i = 0; |
| 170 | + console.log(++i); |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | +}); |
| 175 | + |
| 176 | +// this DOES NOT WORK ⚠️ |
| 177 | +let i = 0; |
| 178 | +define('pl', { |
| 179 | + interpreter: 'programming-lang', |
| 180 | + hooks: { |
| 181 | + worker: { |
| 182 | + onReady() { |
| 183 | + // that outer-scope `i` is nowhere understood |
| 184 | + // whenever this code executes in the worker |
| 185 | + // as this function gets stringified and re-evaluated |
| 186 | + console.log(++i); |
| 187 | + } |
| 188 | + } |
| 189 | + } |
| 190 | +}); |
| 191 | +``` |
| 192 | + |
| 193 | +At the same time, as the worker doesn't have any `element` strictly related, as workers can be created also procedurally, the second argument won't be an element but the related *xworker* that is driving the logic. |
| 194 | + |
| 195 | +As summary, this is the list of all possible, yet **optional** hooks, a custom type can define for **worker**: |
| 196 | + |
| 197 | +| name | example | behavior | |
| 198 | +| :------------------------ | :-------------------------------------------- | :--------| |
| 199 | +| onReady | `onReady(wrap:Wrap, xw:XWorker) {}` | If defined, it is invoked before any other hook to signal that the xworker is going to execute the code. Differently from **main**, the code here is already known so all other operations will be performed automatically. | |
| 200 | +| onBeforeRun | `onBeforeRun(wrap:Wrap, xw:XWorker) {}` | If defined, it is invoked before any other hook to signal that the xworker is going to execute the code. | |
| 201 | +| onBeforeRunAsync | `onBeforeRunAsync(wrap:Wrap, xw:XWorker) {}` | Same as `onBeforeRun` except it's the one used whenever the worker script is `async`. | |
| 202 | +| codeBeforeRun | `codeBeforeRun: () => 'print("before")'` | If defined, prepend some code to evaluate right before the rest of the code gets executed. | |
| 203 | +| codeBeforeRunAsync | `codeBeforeRunAsync: () => 'print("before")'` | Same as `codeBeforeRun` except it's the one used whenever the worker script is `async`. | |
| 204 | +| codeAfterRun | `codeAfterRun: () => 'print("after")'` | If defined, append some code to evaluate right after the rest of the code already executed. | |
| 205 | +| codeAfterRunAsync | `codeAfterRunAsync: () => 'print("after")'` | Same as `codeAfterRun` except it's the one used whenever the worker script is `async`. | |
| 206 | +| onAfterRun | `onAfterRun(wrap:Wrap, xw:XWorker) {}` | If defined, it is invoked after the foreign code has been executed already. | |
| 207 | +| onAfterRunAsync | `onAfterRunAsync(wrap:Wrap, xw:XWorker) {}` | Same as `onAfterRun` except it's the one used whenever the worker script is `async`. | |
0 commit comments