Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,54 @@ The extension currently registers two JavaScript kernelspecs:

Pick either kernel from the notebook kernel selector in JupyterLite.

### Kernel startup extensions

Frontend extensions can register startup work that runs before user code in
both runtime modes. Use this to preload runtime modules and register comm
targets without sending bootstrap code through `requestExecute`.

```typescript
import type { JupyterFrontEndPlugin } from '@jupyterlab/application';
import { IJavaScriptKernelStartupRegistry } from '@jupyterlite/javascript-kernel';

const plugin: JupyterFrontEndPlugin<void> = {
id: 'my-extension:javascript-startup',
autoStart: true,
requires: [IJavaScriptKernelStartupRegistry],
activate: (app, startup) => {
const runtimeBootstrap = new URL(
'./runtime-bootstrap.js',
import.meta.url
).toString();
const lspCommTarget = new URL(
'./lsp-comm-target.js',
import.meta.url
).toString();

startup.registerStartupExtension({
id: 'my-extension:lsp',
activate: async context => {
await context.preloadModule(runtimeBootstrap);
await context.registerCommTarget({
targetName: 'my-extension:lsp',
module: lspCommTarget,
exportName: 'registerLspTarget'
});
},
deactivate: async context => {
await context.unregisterCommTarget('my-extension:lsp');
}
});
}
};
```

`context.registerCommTarget()` imports the module in the kernel runtime and
passes the exported handler to `Jupyter.comm.registerTarget(targetName,
handler)`. The default export is used when `exportName` is omitted. Disposing
a startup registration calls its optional `deactivate` callback for active
kernels.

### Worker mode limitations

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.
Expand Down
7 changes: 6 additions & 1 deletion packages/javascript-kernel-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
"dependencies": {
"@jupyterlab/application": "^4.6.0",
"@jupyterlite/javascript-kernel": "^0.4.0-alpha.4",
"@jupyterlite/services": "^0.8.0"
"@jupyterlite/services": "^0.8.0",
"@lumino/disposable": "^2.1.5",
"@lumino/signaling": "^2.1.5"
},
"devDependencies": {
"@jupyter/builder": "^1.0.2",
Expand All @@ -56,6 +58,9 @@
"extension": true,
"outputDir": "../../jupyterlite_javascript_kernel/labextension",
"sharedPackages": {
"@jupyterlite/javascript-kernel": {
"singleton": true
},
"@jupyterlite/services": {
"bundled": false,
"singleton": true
Expand Down
120 changes: 116 additions & 4 deletions packages/javascript-kernel-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ import {
import type { IKernel } from '@jupyterlite/services';

import { IKernelSpecs } from '@jupyterlite/services';
import { DisposableDelegate } from '@lumino/disposable';
import type { IDisposable } from '@lumino/disposable';
import { Signal } from '@lumino/signaling';

import { JavaScriptKernel } from '@jupyterlite/javascript-kernel';
import {
IJavaScriptKernelStartupRegistry,
JavaScriptKernel
} from '@jupyterlite/javascript-kernel';
import type { RuntimeMode } from '@jupyterlite/javascript-kernel';

import jsLogo32 from '../style/icons/logo-32x32.png';
Expand Down Expand Up @@ -54,14 +60,117 @@ const registerKernel = (
}
},
create: async (options: IKernel.IOptions): Promise<IKernel> => {
return new JavaScriptKernel({
const kernel = new JavaScriptKernel({
...options,
runtime
runtime,
startupExtensions: Private.startupExtensions
} as JavaScriptKernel.IOptions);
Private.kernelCreated.emit(kernel);
return kernel;
}
});
};

/**
* In-memory registry for JavaScript kernel startup extensions.
*/
class JavaScriptKernelStartupRegistry implements IJavaScriptKernelStartupRegistry {
get startupExtensions(): readonly JavaScriptKernel.IStartupExtension[] {
return [...Private.startupExtensions];
}

registerStartupExtension(
extension: JavaScriptKernel.IStartupExtension
): IDisposable {
const existing = Private.startupExtensions.findIndex(
item => item.id === extension.id
);

if (existing !== -1) {
throw new Error(
`JavaScript kernel startup extension "${extension.id}" is already registered`
);
}

Private.startupExtensions.push(extension);
void Promise.all(
[...Private.kernels].map(kernel =>
kernel.applyStartupExtension(extension)
)
).catch(error => {
console.error(
`[javascript-kernel] Failed to apply startup extension "${extension.id}".`,
error
);
});

return new DisposableDelegate(() => {
const index = Private.startupExtensions.indexOf(extension);
if (index !== -1) {
Private.startupExtensions.splice(index, 1);
void Promise.all(
[...Private.kernels].map(kernel =>
kernel.removeStartupExtension(extension)
)
).catch(error => {
console.error(
`[javascript-kernel] Failed to remove startup extension "${extension.id}".`,
error
);
});
}
});
}
}

namespace Private {
export const startupExtensions: JavaScriptKernel.IStartupExtension[] = [];
export const kernels = new Set<JavaScriptKernel>();

const kernelCreatedOwner = {};
export const kernelCreated = new Signal<
typeof kernelCreatedOwner,
JavaScriptKernel
>(kernelCreatedOwner);

/**
* Track an active JavaScript kernel for late startup registrations.
*/
const trackKernel = (kernel: JavaScriptKernel): void => {
kernels.add(kernel);
void Promise.all(
startupExtensions.map(extension =>
kernel.applyStartupExtension(extension)
)
).catch(error => {
console.error(
'[javascript-kernel] Failed to apply startup extensions.',
error
);
});
const untrackKernel = (sender: JavaScriptKernel): void => {
kernels.delete(sender);
sender.disposed.disconnect(untrackKernel);
};
kernel.disposed.connect(untrackKernel);
};

kernelCreated.connect((_sender, kernel) => {
trackKernel(kernel);
});
}

/**
* Plugin providing the JavaScript kernel startup extension registry.
*/
const startupExtensionsRegistry: JupyterFrontEndPlugin<IJavaScriptKernelStartupRegistry> =
{
id: '@jupyterlite/javascript-kernel-extension:startup-extensions',
autoStart: true,
provides: IJavaScriptKernelStartupRegistry,
activate: () => new JavaScriptKernelStartupRegistry()
};

/**
* Plugin registering the iframe JavaScript kernel.
*/
Expand Down Expand Up @@ -94,6 +203,9 @@ const kernelWorker: JupyterFrontEndPlugin<void> = {
}
};

const plugins: JupyterFrontEndPlugin<void>[] = [kernelIFrame, kernelWorker];
const plugins: Array<
| JupyterFrontEndPlugin<IJavaScriptKernelStartupRegistry>
| JupyterFrontEndPlugin<void>
> = [startupExtensionsRegistry, kernelIFrame, kernelWorker];

export default plugins;
1 change: 1 addition & 0 deletions packages/javascript-kernel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@jupyterlab/nbformat": "^4.6.0",
"@jupyterlite/services": "^0.8.0",
"@lumino/coreutils": "^2.2.2",
"@lumino/disposable": "^2.1.5",
"astring": "^1.9.0",
"comlink": "^4.3.1",
"meriyah": "^4.3.9"
Expand Down
14 changes: 14 additions & 0 deletions packages/javascript-kernel/src/comm/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ export class CommManager {
this._targets.set(targetName, handler);
}

/**
* Whether a comm target handler is registered.
*/
hasTarget(targetName: string): boolean {
return this._targets.has(targetName);
}

/**
* Remove a comm target handler registration.
*/
unregisterTarget(targetName: string): void {
this._targets.delete(targetName);
}

/**
* Register a widget instance by comm ID.
*/
Expand Down
12 changes: 12 additions & 0 deletions packages/javascript-kernel/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,18 @@ export class JavaScriptExecutor {
return lines.join('\n');
}

/**
* Import a module using the executor's import source resolution.
*/
async importModule(source: string): Promise<Record<string, unknown>> {
const importSource = this._transformImportSource(source);
const importFunction = this._createScopedFunction(
'source',
'return import(source);'
) as (source: string) => Promise<Record<string, unknown>>;
return importFunction.call(this._globalScope, importSource);
}

/**
* Create a new empty code registry.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/javascript-kernel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './runtime_backends';
export * from './runtime_evaluator';
export * from './comm';
export * from './widgets';
export * from './startup';
Loading
Loading