@jupyterlite/terminal 0.1.6 → 0.2.0

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.
package/README.md CHANGED
@@ -11,7 +11,7 @@ A terminal for JupyterLite.
11
11
 
12
12
  ## Requirements
13
13
 
14
- - JupyterLite >= 0.4.0
14
+ - JupyterLite >= 0.6.0
15
15
 
16
16
  ## Install
17
17
 
@@ -46,18 +46,6 @@ Then build a new JupyterLite site:
46
46
  jupyter lite build
47
47
  ```
48
48
 
49
- ## Deployment
50
-
51
- If you would like to deploy a JupyterLite site with the terminal extension, you will need to configure your server to add the `Cross-Origin-Embedder-Policy` and `Cross-Origin-Opener-Policy` headers.
52
-
53
- As an example, this repository deploys the JupyterLite terminal to [Vercel](https://vercel.com), using the following files:
54
-
55
- - `vercel.json`: configure the COOP / COEP server headers
56
- - `deploy/requirements-deploy.txt`: dependencies for the JupyterLite deployment
57
- - `deploy/deploy.sh`: script to deploy to Vercel, using micromamba to have full control on the Python versions and isolate the build in a virtual environment
58
-
59
- For more information, have a look at the JupyterLite documentation: https://jupyterlite.readthedocs.io/
60
-
61
49
  ## Contributing
62
50
 
63
51
  ### Development install
@@ -97,6 +85,18 @@ jupyter lite build --contents contents
97
85
 
98
86
  And serve it either using:
99
87
 
88
+ ```bash
89
+ npx static-handler _output/
90
+ ```
91
+
92
+ or:
93
+
94
+ ```bash
95
+ jupyter lite serve
96
+ ```
97
+
98
+ To enable use of SharedArrayBuffer rather than ServiceWorker for `stdin` you will have to configure your server to add the `Cross-Origin-Embedder-Policy` and `Cross-Origin-Opener-Policy` headers. Do this using either:
99
+
100
100
  ```bash
101
101
  npx static-handler --cors --coop --coep --corp _output/
102
102
  ```
@@ -107,8 +107,6 @@ or:
107
107
  jupyter lite serve --LiteBuildConfig.extra_http_headers=Cross-Origin-Embedder-Policy=require-corp --LiteBuildConfig.extra_http_headers=Cross-Origin-Opener-Policy=same-origin
108
108
  ```
109
109
 
110
- The extra HTTP headers are require to ensure that `SharedArrayBuffer` is available.
111
-
112
110
  ### Packaging the extension
113
111
 
114
112
  See [RELEASE](RELEASE.md)
@@ -0,0 +1,28 @@
1
+ import { ServerConnection, Terminal } from '@jupyterlab/services';
2
+ import { IExternalCommand, IStdinReply, IStdinRequest } from '@jupyterlite/cockle';
3
+ import { ILiteTerminalAPIClient } from './tokens';
4
+ export declare class LiteTerminalAPIClient implements ILiteTerminalAPIClient {
5
+ constructor(options?: {
6
+ serverSettings?: ServerConnection.ISettings;
7
+ });
8
+ /**
9
+ * Set identifier for communicating with service worker.
10
+ */
11
+ set browsingContextId(browsingContextId: string);
12
+ /**
13
+ * Function that handles stdin requests received from service worker.
14
+ */
15
+ handleStdin(request: IStdinRequest): Promise<IStdinReply>;
16
+ get isAvailable(): boolean;
17
+ readonly serverSettings: ServerConnection.ISettings;
18
+ startNew(options?: Terminal.ITerminal.IOptions): Promise<Terminal.IModel>;
19
+ listRunning(): Promise<Terminal.IModel[]>;
20
+ registerExternalCommand(options: IExternalCommand.IOptions): void;
21
+ shutdown(name: string): Promise<void>;
22
+ private get _models();
23
+ private _nextAvailableName;
24
+ private _browsingContextId?;
25
+ private _externalCommands;
26
+ private _shellManager;
27
+ private _shells;
28
+ }
package/lib/client.js ADDED
@@ -0,0 +1,118 @@
1
+ import { PageConfig, URLExt } from '@jupyterlab/coreutils';
2
+ import { ServerConnection } from '@jupyterlab/services';
3
+ import { ShellManager } from '@jupyterlite/cockle';
4
+ import { Server as WebSocketServer } from 'mock-socket';
5
+ import { Shell } from './shell';
6
+ export class LiteTerminalAPIClient {
7
+ constructor(options = {}) {
8
+ var _a;
9
+ this._externalCommands = [];
10
+ this._shells = new Map();
11
+ this.serverSettings =
12
+ (_a = options.serverSettings) !== null && _a !== void 0 ? _a : ServerConnection.makeSettings();
13
+ this._shellManager = new ShellManager();
14
+ }
15
+ /**
16
+ * Set identifier for communicating with service worker.
17
+ */
18
+ set browsingContextId(browsingContextId) {
19
+ console.log('LiteTerminalAPIClient browsingContextId', browsingContextId);
20
+ this._browsingContextId = browsingContextId;
21
+ }
22
+ /**
23
+ * Function that handles stdin requests received from service worker.
24
+ */
25
+ async handleStdin(request) {
26
+ return await this._shellManager.handleStdin(request);
27
+ }
28
+ get isAvailable() {
29
+ const available = String(PageConfig.getOption('terminalsAvailable'));
30
+ return available.toLowerCase() === 'true';
31
+ }
32
+ async startNew(options) {
33
+ var _a;
34
+ // Create shell.
35
+ const name = (_a = options === null || options === void 0 ? void 0 : options.name) !== null && _a !== void 0 ? _a : this._nextAvailableName();
36
+ const { baseUrl, wsUrl } = this.serverSettings;
37
+ const shell = new Shell({
38
+ mountpoint: '/drive',
39
+ baseUrl,
40
+ wasmBaseUrl: URLExt.join(baseUrl, 'extensions/@jupyterlite/terminal/static/wasm/'),
41
+ browsingContextId: this._browsingContextId,
42
+ shellId: name,
43
+ shellManager: this._shellManager,
44
+ outputCallback: text => {
45
+ var _a;
46
+ const msg = JSON.stringify(['stdout', text]);
47
+ (_a = shell.socket) === null || _a === void 0 ? void 0 : _a.send(msg);
48
+ }
49
+ });
50
+ this._shells.set(name, shell);
51
+ for (const externalCommand of this._externalCommands) {
52
+ shell.registerExternalCommand(externalCommand);
53
+ }
54
+ // Hook to connect socket to shell.
55
+ const hook = async (shell, socket) => {
56
+ shell.socket = socket;
57
+ socket.on('message', async (message) => {
58
+ // Message from xtermjs to pass to shell.
59
+ const data = JSON.parse(message);
60
+ const message_type = data[0];
61
+ const content = data.slice(1);
62
+ await shell.ready;
63
+ if (message_type === 'stdin') {
64
+ await shell.input(content[0]);
65
+ }
66
+ else if (message_type === 'set_size') {
67
+ const rows = content[0];
68
+ const columns = content[1];
69
+ await shell.setSize(rows, columns);
70
+ }
71
+ });
72
+ // Return handshake.
73
+ const res = JSON.stringify(['setup']);
74
+ console.log('Terminal returning handshake via socket');
75
+ socket.send(res);
76
+ shell.start();
77
+ };
78
+ const url = URLExt.join(wsUrl, 'terminals', 'websocket', name);
79
+ const wsServer = new WebSocketServer(url);
80
+ wsServer.on('connection', (socket) => {
81
+ hook(shell, socket);
82
+ });
83
+ shell.disposed.connect(() => {
84
+ this.shutdown(name);
85
+ wsServer.close();
86
+ });
87
+ return { name };
88
+ }
89
+ async listRunning() {
90
+ return this._models;
91
+ }
92
+ registerExternalCommand(options) {
93
+ this._externalCommands.push(options);
94
+ }
95
+ async shutdown(name) {
96
+ var _a, _b;
97
+ const shell = this._shells.get(name);
98
+ if (shell !== undefined) {
99
+ (_a = shell.socket) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify(['disconnect']));
100
+ (_b = shell.socket) === null || _b === void 0 ? void 0 : _b.close();
101
+ this._shells.delete(name);
102
+ shell.dispose();
103
+ }
104
+ }
105
+ get _models() {
106
+ return Array.from(this._shells.keys(), name => {
107
+ return { name };
108
+ });
109
+ }
110
+ _nextAvailableName() {
111
+ for (let i = 1;; ++i) {
112
+ const name = `${i}`;
113
+ if (!this._shells.has(name)) {
114
+ return name;
115
+ }
116
+ }
117
+ }
118
+ }
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { JupyterLiteServerPlugin } from '@jupyterlite/server';
2
- import { ITerminalManager } from './tokens';
3
- declare const _default: (JupyterLiteServerPlugin<ITerminalManager> | JupyterLiteServerPlugin<void>)[];
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ import { ServiceManagerPlugin, Terminal } from '@jupyterlab/services';
3
+ declare const _default: (ServiceManagerPlugin<Terminal.ITerminalAPIClient> | ServiceManagerPlugin<Terminal.IManager> | JupyterFrontEndPlugin<void>)[];
4
4
  export default _default;
package/lib/index.js CHANGED
@@ -1,61 +1,67 @@
1
1
  // Copyright (c) Jupyter Development Team.
2
2
  // Distributed under the terms of the Modified BSD License.
3
- import { TerminalManager } from './manager';
4
- import { ITerminalManager } from './tokens';
3
+ import { ITerminalManager, ServerConnection, IServerSettings, TerminalManager } from '@jupyterlab/services';
4
+ import { IServiceWorkerManager } from '@jupyterlite/server';
5
+ import { WebSocket } from 'mock-socket';
6
+ import { LiteTerminalAPIClient } from './client';
7
+ import { ILiteTerminalAPIClient } from './tokens';
5
8
  /**
6
- * The terminals service plugin.
9
+ * Plugin containing client for in-browser terminals.
7
10
  */
8
- const terminalsPlugin = {
9
- id: '@jupyterlite/terminal:plugin',
10
- description: 'A terminal for JupyterLite',
11
+ const terminalClientPlugin = {
12
+ id: '@jupyterlite/terminal:client',
13
+ description: 'The client for Lite terminals',
11
14
  autoStart: true,
12
- provides: ITerminalManager,
13
- activate: async (app) => {
14
- console.log('JupyterLite extension @jupyterlite/terminal:plugin is activated!');
15
- const { serviceManager } = app;
16
- const { serverSettings, terminals } = serviceManager;
17
- console.log('terminals available:', terminals.isAvailable());
18
- console.log('terminals ready:', terminals.isReady); // Not ready
19
- console.log('terminals active:', terminals.isActive);
20
- // Not sure this is necessary?
21
- await terminals.ready;
22
- console.log('terminals ready after await:', terminals.isReady); // Ready
23
- return new TerminalManager(serverSettings.wsUrl);
15
+ provides: ILiteTerminalAPIClient,
16
+ optional: [IServerSettings],
17
+ activate: (_, serverSettings) => {
18
+ return new LiteTerminalAPIClient({
19
+ serverSettings: {
20
+ ...ServerConnection.makeSettings(),
21
+ ...serverSettings,
22
+ WebSocket
23
+ }
24
+ });
24
25
  }
25
26
  };
26
27
  /**
27
- * A plugin providing the routes for the terminals service
28
+ * Plugin containing manager for in-browser terminals.
28
29
  */
29
- const terminalsRoutesPlugin = {
30
- id: '@jupyterlite/terminal:routes-plugin',
30
+ const terminalManagerPlugin = {
31
+ id: '@jupyterlite/terminal:manager',
32
+ description: 'A JupyterLite extension providing a custom terminal manager',
31
33
  autoStart: true,
32
- requires: [ITerminalManager],
33
- activate: (app, terminalManager) => {
34
- console.log('JupyterLite extension @jupyterlite/terminal:routes-plugin is activated!', terminalManager);
35
- // GET /api/terminals - List the running terminals
36
- app.router.get('/api/terminals', async (req) => {
37
- const res = await terminalManager.listRunning();
38
- // Should return last_activity for each too,
39
- return new Response(JSON.stringify(res));
40
- });
41
- // POST /api/terminals - Start a terminal
42
- app.router.post('/api/terminals', async (req) => {
43
- const res = await terminalManager.startNew();
44
- // Should return last_activity too.
45
- return new Response(JSON.stringify(res));
46
- });
47
- // DELETE /api/terminals/{terminal name} - Delete a terminal
48
- app.router.delete('/api/terminals/(.+)', async (req, name) => {
49
- const exists = terminalManager.has(name);
50
- if (exists) {
51
- await terminalManager.shutdownTerminal(name);
52
- }
53
- else {
54
- const msg = `The terminal session "${name}"" does not exist`;
55
- console.warn(msg);
56
- }
57
- return new Response(null, { status: exists ? 204 : 404 });
34
+ provides: ITerminalManager,
35
+ requires: [ILiteTerminalAPIClient],
36
+ activate: (_, terminalAPIClient) => {
37
+ console.log('JupyterLite extension @jupyterlite/terminal:manager activated');
38
+ return new TerminalManager({
39
+ terminalAPIClient,
40
+ serverSettings: terminalAPIClient.serverSettings
58
41
  });
59
42
  }
60
43
  };
61
- export default [terminalsPlugin, terminalsRoutesPlugin];
44
+ /**
45
+ * Plugin that connects in-browser terminals and service worker.
46
+ */
47
+ const terminalServiceWorkerPlugin = {
48
+ id: '@jupyterlite/terminal:service-worker',
49
+ autoStart: true,
50
+ requires: [ILiteTerminalAPIClient],
51
+ optional: [IServiceWorkerManager],
52
+ activate: (_, liteTerminalAPIClient, serviceWorkerManager) => {
53
+ if (serviceWorkerManager !== undefined) {
54
+ liteTerminalAPIClient.browsingContextId =
55
+ serviceWorkerManager.browsingContextId;
56
+ serviceWorkerManager.registerStdinHandler('terminal', liteTerminalAPIClient.handleStdin.bind(liteTerminalAPIClient));
57
+ }
58
+ else {
59
+ console.warn('Service worker is not available for terminals');
60
+ }
61
+ }
62
+ };
63
+ export default [
64
+ terminalClientPlugin,
65
+ terminalManagerPlugin,
66
+ terminalServiceWorkerPlugin
67
+ ];
package/lib/shell.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BaseShell, IShell } from '@jupyterlite/cockle';
2
+ import { Client as WebSocketClient } from 'mock-socket';
2
3
  /**
3
4
  * Shell class that uses web worker that plugs into a DriveFS via the service worker.
4
5
  */
@@ -13,4 +14,5 @@ export declare class Shell extends BaseShell {
13
14
  * Load the web worker.
14
15
  */
15
16
  protected initWorker(options: IShell.IOptions): Worker;
17
+ socket?: WebSocketClient;
16
18
  }
package/lib/tokens.d.ts CHANGED
@@ -1,52 +1,18 @@
1
- import { TerminalAPI } from '@jupyterlab/services';
1
+ import { Terminal } from '@jupyterlab/services';
2
+ import { IExternalCommand, IStdinReply, IStdinRequest } from '@jupyterlite/cockle';
2
3
  import { Token } from '@lumino/coreutils';
3
- import { IObservableDisposable } from '@lumino/disposable';
4
- /**
5
- * The token for the Terminals service.
6
- */
7
- export declare const ITerminalManager: Token<ITerminalManager>;
8
- /**
9
- * An interface for the TerminalManager service.
10
- */
11
- export interface ITerminalManager {
4
+ export declare const ILiteTerminalAPIClient: Token<ILiteTerminalAPIClient>;
5
+ export interface ILiteTerminalAPIClient extends Terminal.ITerminalAPIClient {
12
6
  /**
13
- * Return whether the named terminal exists.
7
+ * Identifier for communicating with service worker.
14
8
  */
15
- has(name: string): boolean;
9
+ browsingContextId: string;
16
10
  /**
17
- * List the running terminals.
11
+ * Function that handles stdin requests received from service worker.
18
12
  */
19
- listRunning: () => Promise<TerminalAPI.IModel[]>;
13
+ handleStdin(request: IStdinRequest): Promise<IStdinReply>;
20
14
  /**
21
- * Shutdown a terminal by name.
15
+ * Register an external command that will be available in all terminals.
22
16
  */
23
- shutdownTerminal: (name: string) => Promise<void>;
24
- /**
25
- * Start a new kernel.
26
- */
27
- startNew: () => Promise<TerminalAPI.IModel>;
28
- }
29
- /**
30
- * An interface for a server-side terminal running in the browser.
31
- */
32
- export interface ITerminal extends IObservableDisposable {
33
- /**
34
- * The name of the server-side terminal.
35
- */
36
- readonly name: string;
37
- }
38
- /**
39
- * A namespace for ITerminal statics.
40
- */
41
- export declare namespace ITerminal {
42
- /**
43
- * The instantiation options for an ITerminal.
44
- */
45
- interface IOptions {
46
- /**
47
- * The name of the terminal.
48
- */
49
- name: string;
50
- baseUrl: string;
51
- }
17
+ registerExternalCommand(options: IExternalCommand.IOptions): void;
52
18
  }
package/lib/tokens.js CHANGED
@@ -1,7 +1,2 @@
1
- // Copyright (c) Jupyter Development Team.
2
- // Distributed under the terms of the Modified BSD License.
3
1
  import { Token } from '@lumino/coreutils';
4
- /**
5
- * The token for the Terminals service.
6
- */
7
- export const ITerminalManager = new Token('@jupyterlite/terminal:ITerminalManager');
2
+ export const ILiteTerminalAPIClient = new Token('@jupyterlite/terminal:client');
package/lib/worker.js CHANGED
@@ -1,34 +1,36 @@
1
1
  import { expose } from 'comlink';
2
2
  import { BaseShellWorker } from '@jupyterlite/cockle';
3
- import { DriveFS, ServiceWorkerContentsAPI } from '@jupyterlite/contents';
4
- /**
5
- * Custom DriveFS implementation using the service worker.
6
- */
7
- class MyDriveFS extends DriveFS {
8
- createAPI(options) {
9
- return new ServiceWorkerContentsAPI(options.baseUrl, options.driveName, options.mountpoint, options.FS, options.ERRNO_CODES);
10
- }
11
- }
3
+ import { DriveFS } from '@jupyterlite/contents';
12
4
  /**
13
5
  * Shell web worker that uses DriveFS via service worker.
14
6
  * Note that this is not exported as it is accessed from Shell via the filename.
15
7
  */
16
8
  class ShellWorker extends BaseShellWorker {
17
9
  /**
18
- * Initialize the DriveFS to mount an external file system.
10
+ * Initialize the DriveFS to mount an external file system, if available.
19
11
  */
20
- initDriveFS(driveFsBaseUrl, mountpoint, fileSystem) {
21
- console.log('Terminal initDriveFS', driveFsBaseUrl, mountpoint);
22
- const { FS, ERRNO_CODES, PATH } = fileSystem;
23
- const driveFS = new MyDriveFS({
24
- FS,
25
- PATH,
26
- ERRNO_CODES,
27
- baseUrl: driveFsBaseUrl,
28
- driveName: '',
29
- mountpoint
30
- });
31
- FS.mount(driveFS, {}, mountpoint);
12
+ initDriveFS(options) {
13
+ const { baseUrl, browsingContextId, fileSystem, mountpoint } = options;
14
+ console.log('Terminal initDriveFS', baseUrl, mountpoint, browsingContextId);
15
+ if (mountpoint !== '' &&
16
+ baseUrl !== undefined &&
17
+ browsingContextId !== undefined) {
18
+ const { FS, ERRNO_CODES, PATH } = fileSystem;
19
+ const driveFS = new DriveFS({
20
+ FS,
21
+ PATH,
22
+ ERRNO_CODES,
23
+ baseUrl,
24
+ driveName: '',
25
+ mountpoint,
26
+ browsingContextId
27
+ });
28
+ FS.mount(driveFS, {}, mountpoint);
29
+ console.log('Terminal connected to shared drive');
30
+ }
31
+ else {
32
+ console.warn('Terminal not connected to shared drive');
33
+ }
32
34
  }
33
35
  }
34
36
  const worker = new ShellWorker();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlite/terminal",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "A terminal for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -59,19 +59,17 @@
59
59
  "watch:labextension": "jupyter labextension watch ."
60
60
  },
61
61
  "dependencies": {
62
- "@jupyterlab/coreutils": "^6.3.5",
63
- "@jupyterlab/services": "^7.3.5",
64
- "@jupyterlab/terminal": "^4.3.5",
65
- "@jupyterlab/terminal-extension": "^4.3.5",
66
- "@jupyterlite/cockle": "^0.0.16",
67
- "@jupyterlite/contents": "^0.5.1",
68
- "@jupyterlite/server": "^0.5.1",
62
+ "@jupyterlab/coreutils": "^6.4.3",
63
+ "@jupyterlab/services": "^7.4.3",
64
+ "@jupyterlite/cockle": "^0.1.0",
65
+ "@jupyterlite/contents": "0.6.0",
66
+ "@jupyterlite/server": "0.6.0",
69
67
  "@lumino/coreutils": "^2.2.0",
70
68
  "mock-socket": "^9.3.1"
71
69
  },
72
70
  "devDependencies": {
73
- "@jupyterlab/builder": "^4.3.5",
74
- "@jupyterlab/testutils": "^4.3.5",
71
+ "@jupyterlab/builder": "^4.4.3",
72
+ "@jupyterlab/testutils": "^4.4.3",
75
73
  "@types/jest": "^29.2.0",
76
74
  "@types/json-schema": "^7.0.11",
77
75
  "@types/react": "^18.0.26",
@@ -99,6 +97,9 @@
99
97
  "webpack-cli": "^5.1.4",
100
98
  "yjs": "^13.5.0"
101
99
  },
100
+ "resolutions": {
101
+ "parse5": "7.2.1"
102
+ },
102
103
  "sideEffects": [
103
104
  "style/*.css",
104
105
  "style/index.js"
@@ -111,9 +112,6 @@
111
112
  "extension": true,
112
113
  "outputDir": "jupyterlite_terminal/labextension"
113
114
  },
114
- "jupyterlite": {
115
- "liteExtension": true
116
- },
117
115
  "eslintIgnore": [
118
116
  "node_modules",
119
117
  "dist",