Skip to content

Commit b45958c

Browse files
committed
Add startup extensions for JavaScript kernels
1 parent 4de2a30 commit b45958c

14 files changed

Lines changed: 422 additions & 18 deletions

File tree

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,51 @@ The extension currently registers two JavaScript kernelspecs:
4040

4141
Pick either kernel from the notebook kernel selector in JupyterLite.
4242

43+
### Kernel startup extensions
44+
45+
Frontend extensions can register startup work that runs before user code in
46+
both runtime modes. Use this to preload runtime modules and register comm
47+
targets without sending bootstrap code through `requestExecute`.
48+
49+
```typescript
50+
import type { JupyterFrontEndPlugin } from '@jupyterlab/application';
51+
import { IJavaScriptKernelStartup } from '@jupyterlite/javascript-kernel';
52+
53+
const plugin: JupyterFrontEndPlugin<void> = {
54+
id: 'my-extension:javascript-startup',
55+
autoStart: true,
56+
requires: [IJavaScriptKernelStartup],
57+
activate: (app, startup) => {
58+
const runtimeBootstrap = new URL(
59+
'./runtime-bootstrap.js',
60+
import.meta.url
61+
).toString();
62+
const lspCommTarget = new URL(
63+
'./lsp-comm-target.js',
64+
import.meta.url
65+
).toString();
66+
67+
startup.registerStartupExtension({
68+
id: 'my-extension:lsp',
69+
modulePreloads: [runtimeBootstrap],
70+
commTargets: [
71+
{
72+
targetName: 'my-extension:lsp',
73+
module: lspCommTarget,
74+
exportName: 'registerLspTarget'
75+
}
76+
]
77+
});
78+
}
79+
};
80+
```
81+
82+
Each `commTargets` entry imports the module in the kernel runtime and passes
83+
the exported handler to `Jupyter.comm.registerTarget(targetName, handler)`.
84+
The default export is used when `exportName` is omitted.
85+
Disposing a startup registration removes declared comm targets from active
86+
kernels; use an optional `deactivate` callback for extra cleanup.
87+
4388
### Worker mode limitations
4489

4590
Web Workers do not expose DOM APIs. In `JavaScript (Web Worker)`, APIs such as `document`, direct element access, and other main-thread-only browser APIs are unavailable.

packages/javascript-kernel-extension/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"dependencies": {
4242
"@jupyterlab/application": "^4.5.5",
4343
"@jupyterlite/javascript-kernel": "^0.4.0-alpha.4",
44-
"@jupyterlite/services": "^0.7.0"
44+
"@jupyterlite/services": "^0.7.0",
45+
"@lumino/disposable": "^2.1.5"
4546
},
4647
"devDependencies": {
4748
"@jupyterlab/builder": "^4.5.5",
@@ -56,6 +57,9 @@
5657
"extension": true,
5758
"outputDir": "../../jupyterlite_javascript_kernel/labextension",
5859
"sharedPackages": {
60+
"@jupyterlite/javascript-kernel": {
61+
"singleton": true
62+
},
5963
"@jupyterlite/services": {
6064
"bundled": false,
6165
"singleton": true

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

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ import {
99
import type { IKernel } from '@jupyterlite/services';
1010

1111
import { IKernelSpecs } from '@jupyterlite/services';
12+
import { DisposableDelegate } from '@lumino/disposable';
13+
import type { IDisposable } from '@lumino/disposable';
1214

13-
import { JavaScriptKernel } from '@jupyterlite/javascript-kernel';
15+
import {
16+
IJavaScriptKernelStartup,
17+
JavaScriptKernel
18+
} from '@jupyterlite/javascript-kernel';
1419
import type { RuntimeMode } from '@jupyterlite/javascript-kernel';
1520

1621
import jsLogo32 from '../style/icons/logo-32x32.png';
@@ -24,13 +29,14 @@ interface IRegisterKernelOptions {
2429
name: string;
2530
displayName: string;
2631
runtime: RuntimeMode;
32+
startup: IJavaScriptKernelStartup;
2733
}
2834

2935
const registerKernel = (
3036
kernelspecs: IKernelSpecs,
3137
options: IRegisterKernelOptions
3238
) => {
33-
const { name, displayName, runtime } = options;
39+
const { name, displayName, runtime, startup } = options;
3440

3541
kernelspecs.register({
3642
spec: {
@@ -54,26 +60,111 @@ const registerKernel = (
5460
}
5561
},
5662
create: async (options: IKernel.IOptions): Promise<IKernel> => {
57-
return new JavaScriptKernel({
63+
const kernel = new JavaScriptKernel({
5864
...options,
59-
runtime
65+
runtime,
66+
startupExtensions: startup.startupExtensions
6067
} as JavaScriptKernel.IOptions);
68+
if (startup instanceof JavaScriptKernelStartup) {
69+
startup.trackKernel(kernel);
70+
}
71+
return kernel;
6172
}
6273
});
6374
};
6475

76+
/**
77+
* In-memory registry for JavaScript kernel startup extensions.
78+
*/
79+
class JavaScriptKernelStartup implements IJavaScriptKernelStartup {
80+
get startupExtensions(): readonly JavaScriptKernel.IStartupExtension[] {
81+
return [...this._startupExtensions];
82+
}
83+
84+
registerStartupExtension(
85+
extension: JavaScriptKernel.IStartupExtension
86+
): IDisposable {
87+
const existing = this._startupExtensions.findIndex(
88+
item => item.id === extension.id
89+
);
90+
91+
if (existing !== -1) {
92+
throw new Error(
93+
`JavaScript kernel startup extension "${extension.id}" is already registered`
94+
);
95+
}
96+
97+
this._startupExtensions.push(extension);
98+
void Promise.all(
99+
[...this._kernels].map(kernel => kernel.applyStartupExtension(extension))
100+
).catch(error => {
101+
console.error(
102+
`[javascript-kernel] Failed to apply startup extension "${extension.id}".`,
103+
error
104+
);
105+
});
106+
107+
return new DisposableDelegate(() => {
108+
const index = this._startupExtensions.indexOf(extension);
109+
if (index !== -1) {
110+
this._startupExtensions.splice(index, 1);
111+
void Promise.all(
112+
[...this._kernels].map(kernel =>
113+
kernel.removeStartupExtension(extension)
114+
)
115+
).catch(error => {
116+
console.error(
117+
`[javascript-kernel] Failed to remove startup extension "${extension.id}".`,
118+
error
119+
);
120+
});
121+
}
122+
});
123+
}
124+
125+
/**
126+
* Track an active JavaScript kernel for late startup registrations.
127+
*/
128+
trackKernel(kernel: JavaScriptKernel): void {
129+
this._kernels.add(kernel);
130+
const untrackKernel = (sender: JavaScriptKernel): void => {
131+
this._kernels.delete(sender);
132+
sender.disposed.disconnect(untrackKernel);
133+
};
134+
kernel.disposed.connect(untrackKernel);
135+
}
136+
137+
private _startupExtensions: JavaScriptKernel.IStartupExtension[] = [];
138+
private _kernels = new Set<JavaScriptKernel>();
139+
}
140+
141+
/**
142+
* Plugin providing the JavaScript kernel startup extension registry.
143+
*/
144+
const startupExtensions: JupyterFrontEndPlugin<IJavaScriptKernelStartup> = {
145+
id: '@jupyterlite/javascript-kernel-extension:startup-extensions',
146+
autoStart: true,
147+
provides: IJavaScriptKernelStartup,
148+
activate: () => new JavaScriptKernelStartup()
149+
};
150+
65151
/**
66152
* Plugin registering the iframe JavaScript kernel.
67153
*/
68154
const kernelIFrame: JupyterFrontEndPlugin<void> = {
69155
id: '@jupyterlite/javascript-kernel-extension:kernel-iframe',
70156
autoStart: true,
71-
requires: [IKernelSpecs],
72-
activate: (app: JupyterFrontEnd, kernelspecs: IKernelSpecs) => {
157+
requires: [IKernelSpecs, IJavaScriptKernelStartup],
158+
activate: (
159+
app: JupyterFrontEnd,
160+
kernelspecs: IKernelSpecs,
161+
startup: IJavaScriptKernelStartup
162+
) => {
73163
registerKernel(kernelspecs, {
74164
name: 'javascript',
75165
displayName: 'JavaScript (IFrame)',
76-
runtime: 'iframe'
166+
runtime: 'iframe',
167+
startup
77168
});
78169
}
79170
};
@@ -84,16 +175,23 @@ const kernelIFrame: JupyterFrontEndPlugin<void> = {
84175
const kernelWorker: JupyterFrontEndPlugin<void> = {
85176
id: '@jupyterlite/javascript-kernel-extension:kernel-worker',
86177
autoStart: true,
87-
requires: [IKernelSpecs],
88-
activate: (app: JupyterFrontEnd, kernelspecs: IKernelSpecs) => {
178+
requires: [IKernelSpecs, IJavaScriptKernelStartup],
179+
activate: (
180+
app: JupyterFrontEnd,
181+
kernelspecs: IKernelSpecs,
182+
startup: IJavaScriptKernelStartup
183+
) => {
89184
registerKernel(kernelspecs, {
90185
name: 'javascript-worker',
91186
displayName: 'JavaScript (Web Worker)',
92-
runtime: 'worker'
187+
runtime: 'worker',
188+
startup
93189
});
94190
}
95191
};
96192

97-
const plugins: JupyterFrontEndPlugin<void>[] = [kernelIFrame, kernelWorker];
193+
const plugins: Array<
194+
JupyterFrontEndPlugin<IJavaScriptKernelStartup> | JupyterFrontEndPlugin<void>
195+
> = [startupExtensions, kernelIFrame, kernelWorker];
98196

99197
export default plugins;

packages/javascript-kernel/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@jupyterlab/nbformat": "^4.5.0",
4848
"@jupyterlite/services": "^0.7.0",
4949
"@lumino/coreutils": "^2.2.2",
50+
"@lumino/disposable": "^2.1.5",
5051
"astring": "^1.9.0",
5152
"comlink": "^4.3.1",
5253
"meriyah": "^4.3.9"

packages/javascript-kernel/src/comm/manager.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ export class CommManager {
7676
this._targets.set(targetName, handler);
7777
}
7878

79+
/**
80+
* Whether a comm target handler is registered.
81+
*/
82+
hasTarget(targetName: string): boolean {
83+
return this._targets.has(targetName);
84+
}
85+
86+
/**
87+
* Remove a comm target handler registration.
88+
*/
89+
unregisterTarget(targetName: string): void {
90+
this._targets.delete(targetName);
91+
}
92+
7993
/**
8094
* Register a widget instance by comm ID.
8195
*/

packages/javascript-kernel/src/executor.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,18 @@ export class JavaScriptExecutor {
292292
return lines.join('\n');
293293
}
294294

295+
/**
296+
* Import a module using the executor's import source resolution.
297+
*/
298+
async importModule(source: string): Promise<Record<string, any>> {
299+
const importSource = this._transformImportSource(source);
300+
const importFunction = this._createScopedFunction(
301+
'source',
302+
'return import(source);'
303+
) as (source: string) => Promise<Record<string, any>>;
304+
return importFunction.call(this._globalScope, importSource);
305+
}
306+
295307
/**
296308
* Create a new empty code registry.
297309
*/

packages/javascript-kernel/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './runtime_backends';
99
export * from './runtime_evaluator';
1010
export * from './comm';
1111
export * from './widgets';
12+
export * from './startup';

0 commit comments

Comments
 (0)