Skip to content

Commit 386cd25

Browse files
committed
iframe + web worker
1 parent 84768c2 commit 386cd25

11 files changed

Lines changed: 1885 additions & 457 deletions

File tree

README.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,115 @@ To remove the extension, execute:
2929
pip uninstall jupyterlite-javascript-kernel
3030
```
3131

32+
## Runtime modes
33+
34+
The extension currently registers two JavaScript kernelspecs:
35+
36+
- `JavaScript`:
37+
Runs code in a sandboxed `iframe`. Use this when your code needs browser DOM APIs like `document`, `window`, or canvas access through the page context.
38+
- `JavaScript (Worker)`:
39+
Runs code in a dedicated Web Worker. Use this for stronger isolation and to avoid blocking the main UI thread.
40+
41+
Pick either kernel from the notebook kernel selector in JupyterLite.
42+
43+
### Worker mode limitations
44+
45+
Web Workers do not expose DOM APIs. In `JavaScript (Worker)`, APIs such as `document`, direct element access, and other main-thread-only browser APIs are unavailable.
46+
47+
### Import side effects in iframe mode
48+
49+
In `JavaScript` (iframe mode), user code and imports execute in the runtime iframe scope.
50+
51+
By default, module-level side effects stay in the runtime iframe. To intentionally affect the main page (`window.parent`), access it directly.
52+
53+
Cell declarations like `var`, `let`, `const`, `function`, and `class` remain in the runtime scope. Host-page mutations happen when your code (or imported code) explicitly reaches `window.parent`.
54+
55+
#### Example: canvas-confetti
56+
57+
```javascript
58+
import confetti from 'canvas-confetti';
59+
60+
const canvas = window.parent.document.createElement('canvas');
61+
Object.assign(canvas.style, {
62+
position: 'fixed',
63+
inset: '0',
64+
width: '100%',
65+
height: '100%',
66+
pointerEvents: 'none',
67+
zIndex: '2147483647'
68+
});
69+
window.parent.document.body.appendChild(canvas);
70+
71+
const fire = confetti.create(canvas, { resize: true, useWorker: true });
72+
73+
fire({ particleCount: 20, spread: 70 });
74+
```
75+
76+
#### Example: p5.js
77+
78+
```javascript
79+
import p5 from 'p5';
80+
81+
const mount = window.parent.document.createElement('div');
82+
Object.assign(mount.style, {
83+
position: 'fixed',
84+
right: '16px',
85+
bottom: '16px',
86+
zIndex: '1000'
87+
});
88+
window.parent.document.body.appendChild(mount);
89+
90+
const sketch = new p5(p => {
91+
p.setup = () => {
92+
p.createCanvas(120, 80);
93+
p.noLoop();
94+
};
95+
}, mount);
96+
```
97+
98+
#### Can side effects be auto-detected and cleaned up?
99+
100+
Partially, yes, but not perfectly. This project currently does not provide automatic side-effect cleanup for host-page mutations.
101+
102+
Limits of automatic cleanup:
103+
104+
- It will not reliably undo monkey-patched globals.
105+
- It will not automatically remove all event listeners or timers.
106+
- It cannot safely revert all stateful third-party module internals.
107+
108+
### Enable or disable specific modes
109+
110+
The two runtime modes are registered by separate plugins:
111+
112+
- `@jupyterlite/javascript-kernel-extension:kernel-iframe`
113+
- `@jupyterlite/javascript-kernel-extension:kernel-worker`
114+
115+
You can disable either one using `disabledExtensions` in `jupyter-config-data`.
116+
117+
Disable worker mode:
118+
119+
```json
120+
{
121+
"jupyter-config-data": {
122+
"disabledExtensions": [
123+
"@jupyterlite/javascript-kernel-extension:kernel-worker"
124+
]
125+
}
126+
}
127+
```
128+
129+
Disable iframe mode:
130+
131+
```json
132+
{
133+
"jupyter-config-data": {
134+
"disabledExtensions": [
135+
"@jupyterlite/javascript-kernel-extension:kernel-iframe"
136+
]
137+
}
138+
}
139+
```
140+
32141
## Contributing
33142

34143
### Development install

examples/magic-imports.ipynb

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"source": [
1616
"## Importing npm Packages\n",
1717
"\n",
18-
"Just use standard ES module import syntax with a package name:"
18+
"Just use standard ES module import syntax with a package name.\n",
19+
"\n",
20+
"For host-page visuals in iframe mode, target `window.parent` explicitly.\n"
1921
]
2022
},
2123
{
@@ -26,12 +28,27 @@
2628
"source": [
2729
"import confetti from 'canvas-confetti';\n",
2830
"\n",
29-
"// Fire some confetti!\n",
30-
"confetti({\n",
31+
"const canvas = window.parent.document.createElement('canvas');\n",
32+
"Object.assign(canvas.style, {\n",
33+
" position: 'fixed',\n",
34+
" inset: '0',\n",
35+
" width: '100%',\n",
36+
" height: '100%',\n",
37+
" pointerEvents: 'none',\n",
38+
" zIndex: '2147483647'\n",
39+
"});\n",
40+
"window.parent.document.body.appendChild(canvas);\n",
41+
"\n",
42+
"const fire = confetti.create(canvas, {\n",
43+
" resize: true,\n",
44+
" useWorker: true\n",
45+
"});\n",
46+
"\n",
47+
"fire({\n",
3148
" particleCount: 100,\n",
3249
" spread: 70,\n",
3350
" origin: { y: 0.6 }\n",
34-
"});"
51+
"});\n"
3552
]
3653
},
3754
{
@@ -218,4 +235,4 @@
218235
},
219236
"nbformat": 4,
220237
"nbformat_minor": 4
221-
}
238+
}

examples/rich-output.ipynb

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,55 @@
237237
"canvas2"
238238
]
239239
},
240+
{
241+
"cell_type": "markdown",
242+
"metadata": {},
243+
"source": [
244+
"## DOM Elements\n",
245+
"\n",
246+
"HTMLElement and SVGElement instances should render as rich output:"
247+
]
248+
},
249+
{
250+
"cell_type": "code",
251+
"execution_count": null,
252+
"metadata": {},
253+
"outputs": [],
254+
"source": [
255+
"const card = document.createElement('div');\n",
256+
"card.style.cssText = 'padding:12px;border:1px solid #dadada;border-radius:8px;background:#fffbe6;font-family:sans-serif;';\n",
257+
"card.innerHTML = '<strong>DOM element test</strong><br/><span>Rendered from runtime iframe scope.</span>';\n",
258+
"\n",
259+
"card"
260+
]
261+
},
262+
{
263+
"cell_type": "code",
264+
"execution_count": null,
265+
"metadata": {},
266+
"outputs": [],
267+
"source": [
268+
"const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n",
269+
"svg.setAttribute('width', '220');\n",
270+
"svg.setAttribute('height', '80');\n",
271+
"\n",
272+
"const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');\n",
273+
"rect.setAttribute('width', '220');\n",
274+
"rect.setAttribute('height', '80');\n",
275+
"rect.setAttribute('rx', '10');\n",
276+
"rect.setAttribute('fill', '#d9f2ff');\n",
277+
"\n",
278+
"const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');\n",
279+
"text.setAttribute('x', '16');\n",
280+
"text.setAttribute('y', '46');\n",
281+
"text.setAttribute('font-size', '20');\n",
282+
"text.setAttribute('font-family', 'sans-serif');\n",
283+
"text.textContent = 'SVG element test';\n",
284+
"\n",
285+
"svg.append(rect, text);\n",
286+
"svg"
287+
]
288+
},
240289
{
241290
"cell_type": "markdown",
242291
"metadata": {},
@@ -361,4 +410,4 @@
361410
},
362411
"nbformat": 4,
363412
"nbformat_minor": 4
364-
}
413+
}

packages/javascript-kernel-extension/src/index.ts

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,45 +11,91 @@ import type { IKernel } from '@jupyterlite/services';
1111
import { IKernelSpecs } from '@jupyterlite/services';
1212

1313
import { JavaScriptKernel } from '@jupyterlite/javascript-kernel';
14+
import type { RuntimeMode } from '@jupyterlite/javascript-kernel';
1415

1516
import jsLogo32 from '../style/icons/logo-32x32.png';
1617

1718
import jsLogo64 from '../style/icons/logo-64x64.png';
1819

1920
/**
20-
* A plugin to register the JavaScript kernel.
21+
* Register a JavaScript kernelspec for a given runtime.
2122
*/
22-
const kernel: JupyterFrontEndPlugin<void> = {
23-
id: '@jupyterlite/javascript-kernel-extension:kernel',
24-
autoStart: true,
25-
requires: [IKernelSpecs],
26-
activate: (app: JupyterFrontEnd, kernelspecs: IKernelSpecs) => {
27-
kernelspecs.register({
23+
interface IRegisterKernelOptions {
24+
name: string;
25+
displayName: string;
26+
runtime: RuntimeMode;
27+
}
28+
29+
const registerKernel = (
30+
kernelspecs: IKernelSpecs,
31+
options: IRegisterKernelOptions
32+
) => {
33+
const { name, displayName, runtime } = options;
34+
35+
kernelspecs.register({
36+
spec: {
37+
name,
38+
display_name: displayName,
39+
language: 'javascript',
40+
argv: [],
2841
spec: {
29-
name: 'javascript',
30-
display_name: 'JavaScript',
31-
language: 'javascript',
3242
argv: [],
33-
spec: {
34-
argv: [],
35-
env: {},
36-
display_name: 'JavaScript',
37-
language: 'javascript',
38-
interrupt_mode: 'message',
39-
metadata: {}
40-
},
41-
resources: {
42-
'logo-32x32': jsLogo32,
43-
'logo-64x64': jsLogo64
43+
env: {},
44+
display_name: displayName,
45+
language: 'javascript',
46+
interrupt_mode: 'message',
47+
metadata: {
48+
runtime
4449
}
4550
},
46-
create: async (options: IKernel.IOptions): Promise<IKernel> => {
47-
return new JavaScriptKernel(options);
51+
resources: {
52+
'logo-32x32': jsLogo32,
53+
'logo-64x64': jsLogo64
4854
}
55+
},
56+
create: async (options: IKernel.IOptions): Promise<IKernel> => {
57+
return new JavaScriptKernel({
58+
...options,
59+
runtime
60+
} as JavaScriptKernel.IOptions);
61+
}
62+
});
63+
};
64+
65+
/**
66+
* Plugin registering the iframe JavaScript kernel.
67+
*/
68+
const kernelIFrame: JupyterFrontEndPlugin<void> = {
69+
id: '@jupyterlite/javascript-kernel-extension:kernel-iframe',
70+
autoStart: true,
71+
requires: [IKernelSpecs],
72+
activate: (app: JupyterFrontEnd, kernelspecs: IKernelSpecs) => {
73+
void app;
74+
registerKernel(kernelspecs, {
75+
name: 'javascript',
76+
displayName: 'JavaScript',
77+
runtime: 'iframe'
78+
});
79+
}
80+
};
81+
82+
/**
83+
* Plugin registering the worker JavaScript kernel.
84+
*/
85+
const kernelWorker: JupyterFrontEndPlugin<void> = {
86+
id: '@jupyterlite/javascript-kernel-extension:kernel-worker',
87+
autoStart: true,
88+
requires: [IKernelSpecs],
89+
activate: (app: JupyterFrontEnd, kernelspecs: IKernelSpecs) => {
90+
void app;
91+
registerKernel(kernelspecs, {
92+
name: 'javascript-worker',
93+
displayName: 'JavaScript (Worker)',
94+
runtime: 'worker'
4995
});
5096
}
5197
};
5298

53-
const plugins: JupyterFrontEndPlugin<void>[] = [kernel];
99+
const plugins: JupyterFrontEndPlugin<void>[] = [kernelIFrame, kernelWorker];
54100

55101
export default plugins;

0 commit comments

Comments
 (0)