diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9f1a13..406d73f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: true - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 @@ -26,6 +28,11 @@ jobs: jlpm jlpm run lint:check + - name: Validate extension examples compile + run: | + set -eux + jlpm run check:extension-examples + - name: Build the extension run: | set -eux diff --git a/.gitignore b/.gitignore index ed41ab0..bec0c67 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ coverage.xml # Sphinx documentation docs/_build/ +docs/content/extension-examples/ # PyBuilder target/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8493ecc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "extension-examples"] + path = extension-examples + url = https://github.com/jupyterlab/extension-examples.git diff --git a/.prettierignore b/.prettierignore index a63524a..7eca757 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,4 +4,5 @@ node_modules **/package.json jupyterlab_plugin_playground src/modules.ts -docs/content \ No newline at end of file +docs/content +extension-examples diff --git a/.readthedocs.yaml b/.readthedocs.yaml index bac84e7..baaa6d9 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,5 +8,9 @@ build: conda: environment: docs/environment.yml +submodules: + include: all + recursive: true + sphinx: configuration: docs/conf.py diff --git a/README.md b/README.md index 509af11..84ffd13 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,17 @@ pip install jupyterlab-plugin-playground This extension provides a new command, `Load Current File As Extension`, available in the text editor. -It also adds a right sidebar panel listing token string IDs you can use in plugin `requires` and `optional` arrays, with search, copy, and import actions. +It also adds a single right sidebar panel with two collapsible sections: + +- **Service Tokens**: token string IDs you can use in plugin `requires` and `optional` arrays, with search, copy, and import actions. +- **Extension Examples**: discovered examples from a local checkout of [`jupyterlab/extension-examples`](https://github.com/jupyterlab/extension-examples), so you can open them directly from the panel. + +If examples are missing: + +- For source checkouts: run `git submodule update --init --recursive`. +- For PyPI installs: clone `https://github.com/jupyterlab/extension-examples` into an `extension-examples/` folder in your working directory. + +When reloading a plugin with the same `id`, the playground attempts to deactivate the previously loaded plugin first. Defining `deactivate()` in examples is recommended for clean reruns. As an example, open the text editor by creating a new text file and paste this small JupyterLab plugin into it. This plugin will create a simple command `My Super Cool Toggle` in the command palette that can be toggled on and off. diff --git a/binder/postBuild b/binder/postBuild index 17c703d..ec5c3ae 100755 --- a/binder/postBuild +++ b/binder/postBuild @@ -52,8 +52,27 @@ SETTINGS = Path(sys.prefix) / "share/jupyter/lab/settings" SETTINGS.mkdir(parents=True, exist_ok=True) shutil.copy2("binder/overrides.json", SETTINGS / "overrides.json") -# download examples -_("git", "clone", "https://github.com/jupyterlab/extension-examples.git") +# ensure examples are available (from submodule checkout or fallback clone) +EXAMPLES = ROOT / "extension-examples" +if (EXAMPLES / "README.md").exists(): + print("Using extension examples from repository checkout/submodule.") +else: + if EXAMPLES.exists() and (ROOT / ".git").exists(): + print("Attempting to initialize extension examples submodule.") + subprocess.call( + [ + "git", + "submodule", + "update", + "--init", + "--recursive", + "extension-examples", + ] + ) + if EXAMPLES.exists() and not (EXAMPLES / "README.md").exists(): + shutil.rmtree(EXAMPLES) + if not EXAMPLES.exists(): + _("git", "clone", "https://github.com/jupyterlab/extension-examples.git") print("JupyterLab with @jupyterlab/plugin-playground is ready to run with:\n") print("\tjupyter lab\n") diff --git a/docs/conf.py b/docs/conf.py index c0c3072..bc7fed6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,6 +21,80 @@ html_css_files = ["custom.css"] +def _ensure_extension_examples(root): + import subprocess + + examples = root / "extension-examples" + if (examples / "README.md").exists(): + return examples + + if (root / ".git").exists(): + try: + subprocess.check_call( + [ + "git", + "submodule", + "update", + "--init", + "--recursive", + "extension-examples", + ], + cwd=str(root), + ) + except subprocess.CalledProcessError as exc: + raise RuntimeError( + "Failed to initialize the 'extension-examples' submodule." + ) from exc + + if (examples / "README.md").exists(): + return examples + + raise RuntimeError( + "Submodule update completed but 'extension-examples' was not found." + ) + + raise RuntimeError( + "Missing 'extension-examples'. Build from a git checkout with submodules " + "initialized." + ) + + +def _sync_examples_to_lite_contents(root): + import shutil + + examples = _ensure_extension_examples(root) + lite_examples_root = root / "docs" / "content" / "extension-examples" + if lite_examples_root.exists(): + shutil.rmtree(lite_examples_root) + lite_examples_root.mkdir(parents=True, exist_ok=True) + + ignored = shutil.ignore_patterns( + ".git", + "node_modules", + "lib", + "dist", + ".ipynb_checkpoints", + ) + + copied_count = 0 + for example_dir in sorted(examples.iterdir()): + if not example_dir.is_dir() or example_dir.name.startswith("."): + continue + + src_dir = example_dir / "src" + if not ((src_dir / "index.ts").exists() or (src_dir / "index.js").exists()): + continue + + shutil.copytree( + example_dir, + lite_examples_root / example_dir.name, + ignore=ignored, + ) + copied_count += 1 + + print(f"Copied {copied_count} extension examples into docs/content for Lite.") + + def on_config_inited(*args): import sys import subprocess @@ -28,6 +102,8 @@ def on_config_inited(*args): HERE = Path(__file__) ROOT = HERE.parent.parent + _sync_examples_to_lite_contents(ROOT) + subprocess.check_call(["jlpm"], cwd=str(ROOT)) subprocess.check_call(["jlpm", "build"], cwd=str(ROOT)) diff --git a/eslint.config.js b/eslint.config.js index a643e99..0e84d7f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,6 +20,7 @@ module.exports = [ 'src/modules.ts', 'docs/content/**', 'docs/_build/**', + 'extension-examples/**', 'ui-tests/test-results/**' ] }, diff --git a/extension-examples b/extension-examples new file mode 160000 index 0000000..31dccfc --- /dev/null +++ b/extension-examples @@ -0,0 +1 @@ +Subproject commit 31dccfcf2fffb9e199c128744dc20596363932e1 diff --git a/package.json b/package.json index ead1546..a2f5f3b 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "eslint:check": "eslint . --cache", "lint": "jlpm prettier && jlpm eslint", "lint:check": "jlpm prettier:check && jlpm eslint:check", + "check:extension-examples": "node scripts/check-extension-examples.js", "prettier": "jlpm prettier:base --write --list-different", "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "prettier:check": "jlpm prettier:base --check", @@ -65,6 +66,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.3", + "@jupyter-notebook/application": "^7.0.0", "@jupyter/collaborative-drive": "^4.2.1", "@jupyter/docprovider": "^4.1.1", "@jupyter/eslint-plugin": "^0.0.1", @@ -97,6 +99,7 @@ "@jupyterlab/tooltip": "^4.5.5", "@jupyterlab/workspaces": "^4.5.5", "@lumino/datagrid": "^2.0.0", + "@rjsf/utils": "^5.13.4", "@types/codemirror": "^5.6.20", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", @@ -108,7 +111,8 @@ "npm-run-all": "^4.1.5", "prettier": "^2.8.0", "rimraf": "^3.0.2", - "typescript": "~5.5.4" + "typescript": "~5.5.4", + "yjs": "^13.5.40" }, "resolutions": { "globals": "13.24.0", diff --git a/scripts/check-extension-examples.js b/scripts/check-extension-examples.js new file mode 100644 index 0000000..22a9726 --- /dev/null +++ b/scripts/check-extension-examples.js @@ -0,0 +1,228 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); + +const ROOT = path.resolve(__dirname, '..'); +const EXAMPLES_ROOT = path.join(ROOT, 'extension-examples'); +const MODULES_FILE = path.join(ROOT, 'src', 'modules.ts'); +const SCRIPT_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']); + +function readKnownModules() { + const source = fs.readFileSync(MODULES_FILE, 'utf8'); + const modules = new Set(); + const pattern = /case '([^']+)':/g; + let match; + + while ((match = pattern.exec(source)) !== null) { + modules.add(match[1]); + } + + return modules; +} + +function listExampleEntrypoints() { + if (!fs.existsSync(EXAMPLES_ROOT)) { + throw new Error( + "Missing 'extension-examples'. Run `git submodule update --init --recursive`." + ); + } + + const entries = []; + const examples = fs + .readdirSync(EXAMPLES_ROOT, { withFileTypes: true }) + .filter(entry => entry.isDirectory() && !entry.name.startsWith('.')) + .map(entry => entry.name) + .sort(); + + for (const example of examples) { + const srcDir = path.join(EXAMPLES_ROOT, example, 'src'); + const candidates = [ + path.join(srcDir, 'index.ts'), + path.join(srcDir, 'index.tsx'), + path.join(srcDir, 'index.js'), + path.join(srcDir, 'index.jsx') + ]; + const entrypoint = candidates.find(candidate => fs.existsSync(candidate)); + if (entrypoint) { + entries.push({ example, entrypoint }); + } + } + + return entries; +} + +function createSourceFile(filePath, source) { + const extension = path.extname(filePath); + let scriptKind = ts.ScriptKind.TS; + + if (extension === '.tsx') { + scriptKind = ts.ScriptKind.TSX; + } else if (extension === '.js') { + scriptKind = ts.ScriptKind.JS; + } else if (extension === '.jsx') { + scriptKind = ts.ScriptKind.JSX; + } + + return ts.createSourceFile( + filePath, + source, + ts.ScriptTarget.Latest, + true, + scriptKind + ); +} + +function collectModuleSpecifiers(filePath, source) { + const sourceFile = createSourceFile(filePath, source); + const specifiers = []; + + function visit(node) { + if ( + (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && + node.moduleSpecifier && + ts.isStringLiteral(node.moduleSpecifier) + ) { + specifiers.push(node.moduleSpecifier.text); + } else if ( + ts.isCallExpression(node) && + node.expression.kind === ts.SyntaxKind.ImportKeyword && + node.arguments.length === 1 && + ts.isStringLiteral(node.arguments[0]) + ) { + specifiers.push(node.arguments[0].text); + } else if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'require' && + node.arguments.length === 1 && + ts.isStringLiteral(node.arguments[0]) + ) { + specifiers.push(node.arguments[0].text); + } + + ts.forEachChild(node, visit); + } + + visit(sourceFile); + return specifiers; +} + +function resolveRelativeImport(fromFile, specifier) { + const base = path.resolve(path.dirname(fromFile), specifier); + if (path.extname(base)) { + return fs.existsSync(base) ? base : null; + } + + const candidates = [ + `${base}.ts`, + `${base}.tsx`, + `${base}.js`, + `${base}.jsx`, + path.join(base, 'index.ts'), + path.join(base, 'index.tsx'), + path.join(base, 'index.js'), + path.join(base, 'index.jsx') + ]; + + return candidates.find(candidate => fs.existsSync(candidate)) ?? null; +} + +function transpileDiagnostics(filePath, source) { + const result = ts.transpileModule(source, { + fileName: filePath, + compilerOptions: { + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES2017, + jsx: ts.JsxEmit.React, + allowJs: true + }, + reportDiagnostics: true + }); + + return result.diagnostics ?? []; +} + +function formatDiagnostic(filePath, diagnostic) { + const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + if (diagnostic.file && typeof diagnostic.start === 'number') { + const position = diagnostic.file.getLineAndCharacterOfPosition( + diagnostic.start + ); + return `${filePath}:${position.line + 1}:${ + position.character + 1 + } ${message}`; + } + return `${filePath}: ${message}`; +} + +function validateEntrypoint(entrypoint, knownModules) { + const queue = [entrypoint]; + const visited = new Set(); + const failures = []; + + while (queue.length > 0) { + const current = queue.pop(); + if (!current || visited.has(current)) { + continue; + } + visited.add(current); + + const source = fs.readFileSync(current, 'utf8'); + const diagnostics = transpileDiagnostics(current, source); + failures.push( + ...diagnostics.map(diagnostic => formatDiagnostic(current, diagnostic)) + ); + + for (const specifier of collectModuleSpecifiers(current, source)) { + if (specifier.startsWith('.')) { + const resolved = resolveRelativeImport(current, specifier); + if (!resolved) { + failures.push(`${current}: unresolved relative import ${specifier}`); + continue; + } + if (SCRIPT_EXTENSIONS.has(path.extname(resolved))) { + queue.push(resolved); + } + continue; + } + + if (!knownModules.has(specifier)) { + failures.push(`${current}: unknown external module ${specifier}`); + } + } + } + + return failures; +} + +function main() { + const knownModules = readKnownModules(); + const entrypoints = listExampleEntrypoints(); + let checked = 0; + let failed = false; + + for (const { example, entrypoint } of entrypoints) { + const failures = validateEntrypoint(entrypoint, knownModules); + if (failures.length > 0) { + failed = true; + console.error(`\n[${example}] validation failed`); + for (const failure of failures) { + console.error(` ${failure}`); + } + continue; + } + checked += 1; + } + + if (failed) { + process.exit(1); + } + + console.log( + `Validated ${checked} extension examples for transpilation and import resolution.` + ); +} + +main(); diff --git a/scripts/modules.py b/scripts/modules.py index 030b726..2aaf15b 100644 --- a/scripts/modules.py +++ b/scripts/modules.py @@ -5,8 +5,10 @@ # Additional modules to include in the generated known-module map. # Keep this as an explicit supplement to the modules discovered from CoreConfig. EXTRA_MODULES = { + '@rjsf/utils', '@jupyter/collaborative-drive', '@jupyter/docprovider', + '@jupyter-notebook/application', '@jupyterlab/attachments', '@jupyterlab/cells', '@jupyterlab/csvviewer', @@ -17,15 +19,15 @@ '@jupyterlab/property-inspector', '@jupyterlab/running', '@jupyter-widgets/base', - '@lumino/datagrid' + '@lumino/datagrid', + 'yjs' } # modules which are implementation detail and unlikely to be used directly in playground IGNORED_MODULES = { '@jupyterlab/nbconvert-css', '@microsoft/fast-element', - '@microsoft/fast-foundation', - 'yjs' + '@microsoft/fast-foundation' } TEMPLATE = """\ diff --git a/src/contents.ts b/src/contents.ts new file mode 100644 index 0000000..a0f4577 --- /dev/null +++ b/src/contents.ts @@ -0,0 +1,115 @@ +import { Contents, ServiceManager } from '@jupyterlab/services'; + +export type IDirectoryModel = Contents.IModel & { + type: 'directory'; + content: Contents.IModel[]; +}; + +export type IFileModel = Contents.IModel & { + type: 'file'; + content: unknown; + format?: string | null; +}; + +export function normalizeContentsPath(path: string | null | undefined): string { + return (path ?? '').replace(/^\/+/g, ''); +} + +export function contentsPathCandidates(path: string): string[] { + // Jupyter Server and JupyterLite do not always agree on whether contents + // paths should be rooted. Try both forms so callers can use one code path. + const trimmed = normalizeContentsPath(path); + if (trimmed.length === 0) { + return []; + } + return [trimmed, `/${trimmed}`]; +} + +async function getContentsModel( + serviceManager: ServiceManager.IManager, + path: string, + options: Contents.IFetchOptions +): Promise { + try { + return await serviceManager.contents.get(path, options); + } catch { + return null; + } +} + +export async function getDirectoryModel( + serviceManager: ServiceManager.IManager, + path: string +): Promise { + for (const candidatePath of contentsPathCandidates(path)) { + const model = await getContentsModel(serviceManager, candidatePath, { + content: true + }); + if (!model || model.type !== 'directory' || !Array.isArray(model.content)) { + continue; + } + return model as IDirectoryModel; + } + return null; +} + +export async function getFileModel( + serviceManager: ServiceManager.IManager, + path: string +): Promise { + for (const candidatePath of contentsPathCandidates(path)) { + const model = await getContentsModel(serviceManager, candidatePath, { + content: true + }); + if (!model || model.type !== 'file') { + continue; + } + if (model.content !== null) { + return model as IFileModel; + } + + const textModel = await getContentsModel(serviceManager, candidatePath, { + content: true, + format: 'text' + }); + if (textModel && textModel.type === 'file' && textModel.content !== null) { + return textModel as IFileModel; + } + } + return null; +} + +export function fileModelToText(fileModel: IFileModel | null): string | null { + if (!fileModel) { + return null; + } + + if (typeof fileModel.content === 'string') { + if (fileModel.format === 'base64') { + try { + return atob(fileModel.content); + } catch { + return null; + } + } + return fileModel.content; + } + + if ( + fileModel.content !== null && + typeof fileModel.content === 'object' && + !Array.isArray(fileModel.content) + ) { + return JSON.stringify(fileModel.content); + } + + return null; +} + +export async function readContentsFileAsText( + serviceManager: ServiceManager.IManager, + path: string +): Promise { + const fileModel = await getFileModel(serviceManager, path); + return fileModelToText(fileModel); +} diff --git a/src/errors.tsx b/src/errors.tsx index a39fe34..a60fce6 100644 --- a/src/errors.tsx +++ b/src/errors.tsx @@ -4,7 +4,7 @@ import { PluginLoader } from './loader'; export function formatErrorWithResult( error: Error, - result: Omit + result: Omit ): JSX.Element { return (
diff --git a/src/example-sidebar.tsx b/src/example-sidebar.tsx new file mode 100644 index 0000000..4968216 --- /dev/null +++ b/src/example-sidebar.tsx @@ -0,0 +1,172 @@ +import { Dialog, ReactWidget, showDialog } from '@jupyterlab/apputils'; + +import * as React from 'react'; + +import { Message } from '@lumino/messaging'; + +export namespace ExampleSidebar { + export interface IExampleRecord { + name: string; + path: string; + description: string; + } + + export interface IOptions { + fetchExamples: () => Promise>; + onOpenExample: (examplePath: string) => Promise | void; + } +} + +export class ExampleSidebar extends ReactWidget { + constructor(options: ExampleSidebar.IOptions) { + super(); + this._fetchExamples = options.fetchExamples; + this._onOpenExample = options.onOpenExample; + this.addClass('jp-PluginPlayground-sidebar'); + this.addClass('jp-PluginPlayground-exampleSidebar'); + } + + protected onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); + void this._loadExamples(); + } + + render(): JSX.Element { + const query = this._query.trim().toLowerCase(); + const filteredExamples = + query.length > 0 + ? this._examples.filter(example => { + return ( + example.name.toLowerCase().includes(query) || + example.description.toLowerCase().includes(query) + ); + }) + : this._examples; + + return ( +
+ +

+ {filteredExamples.length} of {this._examples.length} extension + examples +

+ {this._isLoading ? ( +

+ Loading extension examples… +

+ ) : null} + {this._errorMessage ? ( +

+ Failed to load extension examples: {this._errorMessage} +

+ ) : null} + {!this._isLoading && + !this._errorMessage && + filteredExamples.length === 0 ? ( +
+

+ No extension examples found in extension-examples/. +

+

+ If this repository was cloned from source, run{' '} + git submodule update --init --recursive from the + project root. +

+

+ If installed from PyPI, clone{' '} + https://github.com/jupyterlab/extension-examples as{' '} + extension-examples/ in your working directory and + refresh JupyterLab. +

+
+ ) : null} + {filteredExamples.length > 0 ? ( +
    + {filteredExamples.map(example => ( +
  • +
    + + {example.name} + + +
    +

    + {example.description} +

    +
  • + ))} +
+ ) : null} +
+ ); + } + + private _onQueryChange = (event: React.ChangeEvent) => { + this._query = event.currentTarget.value; + this.update(); + }; + + private async _loadExamples(): Promise { + this._isLoading = true; + this._errorMessage = ''; + this.update(); + try { + this._examples = await this._fetchExamples(); + } catch (error) { + this._examples = []; + this._errorMessage = + error instanceof Error + ? error.message + : 'Could not load extension examples.'; + } finally { + this._isLoading = false; + this.update(); + } + } + + private async _openExample( + example: ExampleSidebar.IExampleRecord + ): Promise { + try { + await this._onOpenExample(example.path); + } catch (error) { + const message = + error instanceof Error ? error.message : 'Unknown opening error'; + await showDialog({ + title: 'Failed to open extension example', + body: `Could not open "${example.path}". ${message}`, + buttons: [Dialog.okButton()] + }); + } + } + + private readonly _fetchExamples: () => Promise< + ReadonlyArray + >; + private readonly _onOpenExample: ( + examplePath: string + ) => Promise | void; + private _query = ''; + private _examples: ReadonlyArray = []; + private _isLoading = false; + private _errorMessage = ''; +} diff --git a/src/index.ts b/src/index.ts index 9896ad9..5e1d54b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,10 +22,12 @@ import { FileEditor, IEditorTracker } from '@jupyterlab/fileeditor'; import { ILauncher } from '@jupyterlab/launcher'; -import { extensionIcon } from '@jupyterlab/ui-components'; +import { extensionIcon, SidePanel } from '@jupyterlab/ui-components'; import { IDocumentManager } from '@jupyterlab/docmanager'; +import { Contents } from '@jupyterlab/services'; + import { PluginLoader, PluginLoadingError } from './loader'; import { PluginTranspiler } from './transpiler'; @@ -40,10 +42,24 @@ import { IRequireJS, RequireJSLoader } from './requirejs'; import { TokenSidebar } from './token-sidebar'; +import { ExampleSidebar } from './example-sidebar'; + import { tokenSidebarIcon } from './icons'; +import { + fileModelToText, + getDirectoryModel, + getFileModel, + IFileModel, + normalizeContentsPath +} from './contents'; + import { Token } from '@lumino/coreutils'; +import { AccordionPanel } from '@lumino/widgets'; + +import { IPlugin } from '@lumino/application'; + namespace CommandIDs { export const createNewFile = 'plugin-playground:create-new-plugin'; export const loadCurrentAsExtension = 'plugin-playground:load-as-extension'; @@ -93,6 +109,8 @@ interface IPrivatePluginData { }; } +const EXTENSION_EXAMPLES_ROOT = 'extension-examples'; + class PluginPlayground { constructor( protected app: JupyterFrontEnd, @@ -111,6 +129,7 @@ class PluginPlayground { app.commands.addCommand(CommandIDs.loadCurrentAsExtension, { label: 'Load Current File As Extension', + describedBy: { args: null }, icon: extensionIcon, isEnabled: () => editorTracker.currentWidget !== null && @@ -133,6 +152,7 @@ class PluginPlayground { app.commands.addCommand(CommandIDs.createNewFile, { label: 'TypeScript File (Playground)', caption: 'Create a new TypeScript file', + describedBy: { args: null }, icon: extensionIcon, execute: async args => { const model = await app.commands.execute('docmanager:new-untitled', { @@ -178,9 +198,29 @@ class PluginPlayground { isImportEnabled: this._canInsertImport.bind(this) }); tokenSidebar.id = 'jp-plugin-token-sidebar'; + tokenSidebar.title.label = 'Service Tokens'; tokenSidebar.title.caption = 'Available service token strings for plugin'; tokenSidebar.title.icon = tokenSidebarIcon; - this.app.shell.add(tokenSidebar, 'right', { rank: 650 }); + + const exampleSidebar = new ExampleSidebar({ + fetchExamples: this._discoverExtensionExamples.bind(this), + onOpenExample: this._openExtensionExample.bind(this) + }); + exampleSidebar.id = 'jp-plugin-example-sidebar'; + exampleSidebar.title.label = 'Extension Examples'; + exampleSidebar.title.caption = + 'Browse plugin examples from jupyterlab/extension-examples'; + + const playgroundSidebar = new SidePanel(); + playgroundSidebar.id = 'jp-plugin-playground-sidebar'; + playgroundSidebar.title.caption = 'Plugin Playground helper panels'; + playgroundSidebar.title.icon = tokenSidebarIcon; + playgroundSidebar.addWidget(tokenSidebar); + playgroundSidebar.addWidget(exampleSidebar); + (playgroundSidebar.content as AccordionPanel).expand(0); + (playgroundSidebar.content as AccordionPanel).expand(1); + this.app.shell.add(playgroundSidebar, 'right', { rank: 650 }); + app.shell.currentChanged?.connect(() => { tokenSidebar.update(); }); @@ -270,17 +310,23 @@ class PluginPlayground { } return; } - const plugin = result.plugin; + const plugins = result.plugins.map(plugin => + this._ensureDeactivateSupport(plugin) + ); - if (result.schema) { + for (const plugin of plugins) { + const schema = result.schemas[plugin.id]; + if (!schema) { + continue; + } // TODO: this is mostly fine to get the menus and toolbars, but: // - transforms are not applied // - any refresh from the server might overwrite the data // - it is not a good long term solution in general this.settingRegistry.plugins[plugin.id] = { id: plugin.id, - schema: JSON.parse(result.schema), - raw: result.schema, + schema: JSON.parse(schema), + raw: schema, data: { composite: {}, user: {} @@ -292,12 +338,24 @@ class PluginPlayground { ).emit(plugin.id); } - // Unregister plugin if already registered. - if (this.app.hasPlugin(plugin.id)) { - this.app.deregisterPlugin(plugin.id, true); + for (const plugin of plugins) { + await this._deactivateAndDeregisterPlugin(plugin.id); + this.app.registerPlugin(plugin); } - this.app.registerPlugin(plugin); - if (plugin.autoStart) { + + for (const plugin of plugins) { + if (!plugin.autoStart) { + continue; + } + const missingRequiredTokens = this._missingRequiredTokens(plugin); + if (missingRequiredTokens.length > 0) { + console.warn( + `Skipping plugin ${ + plugin.id + }: missing required services ${missingRequiredTokens.join(', ')}` + ); + continue; + } try { await this.app.activatePlugin(plugin.id); } catch (e) { @@ -310,12 +368,234 @@ class PluginPlayground { } } + private _missingRequiredTokens( + plugin: IPlugin + ): string[] { + try { + this._populateTokenMap(); + } catch { + return (plugin.requires ?? []).map(token => token.name); + } + + return (plugin.requires ?? []) + .filter(token => !this._tokenMap.has(token.name)) + .map(token => token.name); + } + + private _ensureDeactivateSupport( + plugin: IPlugin + ): IPlugin { + const trackedCommandDisposables: Array<{ dispose: () => void }> = []; + const originalActivate = plugin.activate; + const originalDeactivate = plugin.deactivate; + + plugin.activate = async (app: JupyterFrontEnd, ...services: unknown[]) => { + const originalAddCommand = app.commands.addCommand.bind(app.commands); + app.commands.addCommand = ((id, options) => { + const disposable = originalAddCommand(id, options); + trackedCommandDisposables.push(disposable); + return disposable; + }) as typeof app.commands.addCommand; + + try { + return await originalActivate(app, ...services); + } catch (error) { + this._disposeTrackedCommands(trackedCommandDisposables); + throw error; + } finally { + app.commands.addCommand = originalAddCommand; + } + }; + + plugin.deactivate = async ( + app: JupyterFrontEnd, + ...services: unknown[] + ) => { + try { + if (originalDeactivate) { + await originalDeactivate(app, ...services); + } + } finally { + this._disposeTrackedCommands(trackedCommandDisposables); + } + }; + + return plugin; + } + + private _disposeTrackedCommands( + trackedCommandDisposables: Array<{ dispose: () => void }> + ): void { + while (trackedCommandDisposables.length > 0) { + const disposable = trackedCommandDisposables.pop(); + if (!disposable) { + continue; + } + try { + disposable.dispose(); + } catch (error) { + console.warn('Failed to dispose plugin command registration', error); + } + } + } + + private async _deactivateAndDeregisterPlugin( + pluginId: string + ): Promise { + if (!this.app.hasPlugin(pluginId)) { + return; + } + + if (this.app.isPluginActivated(pluginId)) { + try { + await this.app.deactivatePlugin(pluginId); + } catch (error) { + const message = + error instanceof Error ? error.message : 'Unknown deactivation error'; + await showDialog({ + title: 'Plugin deactivation failed', + body: + `Could not deactivate "${pluginId}" before reload. ` + + 'Falling back to forced reload. Add `deactivate()` to the plugin ' + + 'and dependent plugins for clean reruns. ' + + message, + buttons: [Dialog.okButton()] + }); + } + } + + if (this.app.hasPlugin(pluginId)) { + this.app.deregisterPlugin(pluginId, true); + } + } + private async _getModule(url: string) { const response = await fetch(url); const jsBody = await response.text(); this._loadPlugin(jsBody, null); } + private async _openExtensionExample(examplePath: string): Promise { + await this.app.commands.execute('docmanager:open', { + path: normalizeContentsPath(examplePath), + factory: 'Editor' + }); + } + + private async _discoverExtensionExamples(): Promise< + ReadonlyArray + > { + const rootDirectory = await getDirectoryModel( + this.app.serviceManager, + EXTENSION_EXAMPLES_ROOT + ); + if (!rootDirectory) { + return []; + } + const rootPath = + normalizeContentsPath(rootDirectory.path) || EXTENSION_EXAMPLES_ROOT; + + const discovered: ExampleSidebar.IExampleRecord[] = []; + for (const item of rootDirectory.content) { + if (item.type !== 'directory' || item.name.startsWith('.')) { + continue; + } + const exampleDirectory = this._joinPath(rootPath, item.name); + const entrypoint = await this._findExampleEntrypoint(exampleDirectory); + if (!entrypoint) { + continue; + } + const description = await this._readExampleDescription(exampleDirectory); + discovered.push({ + name: item.name, + path: entrypoint, + description + }); + } + + return discovered.sort((left, right) => + left.name.localeCompare(right.name) + ); + } + + private async _findExampleEntrypoint( + directoryPath: string + ): Promise { + const srcDirectory = await getDirectoryModel( + this.app.serviceManager, + this._joinPath(directoryPath, 'src') + ); + if (!srcDirectory) { + return null; + } + const entrypoint = srcDirectory.content.find( + (item: Contents.IModel) => + item.type === 'file' && + (item.name === 'index.ts' || item.name === 'index.js') + ); + if (!entrypoint) { + return null; + } + return normalizeContentsPath( + this._joinPath(srcDirectory.path, entrypoint.name) + ); + } + + private async _readExampleDescription( + directoryPath: string + ): Promise { + const packageJsonPath = this._joinPath(directoryPath, 'package.json'); + const packageJson = await getFileModel( + this.app.serviceManager, + packageJsonPath + ); + if (!packageJson) { + return this._fallbackExampleDescription; + } + const packageData = this._parseJsonObject(packageJson); + + if (packageData) { + const description = this._stringValue(packageData.description); + if (description) { + return description; + } + } + + return this._fallbackExampleDescription; + } + + private _joinPath(base: string, child: string): string { + const normalizedBase = base.replace(/\/+$/g, ''); + const normalizedChild = normalizeContentsPath(child); + if (!normalizedBase) { + return normalizedChild; + } + return `${normalizedBase}/${normalizedChild}`; + } + + private _parseJsonObject( + fileModel: IFileModel + ): { description?: unknown } | null { + const raw = fileModelToText(fileModel); + if (raw === null) { + return null; + } + + try { + const parsed = JSON.parse(raw) as unknown; + if ( + parsed !== null && + typeof parsed === 'object' && + !Array.isArray(parsed) + ) { + return parsed as { description?: unknown }; + } + } catch { + return null; + } + return null; + } + private _populateTokenMap(): void { const app = this.app as unknown as IPrivateServiceStore; this._tokenMap.clear(); @@ -488,6 +768,8 @@ class PluginPlayground { return !!(sourceModel && sourceModel.sharedModel); } + private readonly _fallbackExampleDescription = + 'No description provided by this example.'; private readonly _tokenMap = new Map>(); private readonly _tokenDescriptionMap = new Map(); } @@ -497,6 +779,8 @@ class PluginPlayground { */ const plugin: JupyterFrontEndPlugin = { id: '@jupyterlab/plugin-playground:plugin', + description: + 'Provide a playground for developing and testing JupyterLab plugins.', autoStart: true, requires: [ISettingRegistry, ICommandPalette, IEditorTracker], optional: [ILauncher, IDocumentManager], diff --git a/src/loader.ts b/src/loader.ts index 721029e..b054da1 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -3,7 +3,7 @@ import { IPlugin } from '@lumino/application'; import { PathExt } from '@jupyterlab/coreutils'; -import { ServiceManager } from '@jupyterlab/services'; +import { Contents, ServiceManager } from '@jupyterlab/services'; import { NoDefaultExportError, PluginTranspiler } from './transpiler'; @@ -11,6 +11,8 @@ import { IRequireJS } from './requirejs'; import { IModule, IModuleMember } from './types'; +import { getDirectoryModel, readContentsFileAsText } from './contents'; + export namespace PluginLoader { export interface IOptions { transpiler: PluginTranspiler; @@ -25,10 +27,10 @@ export namespace PluginLoader { serviceManager: ServiceManager.IManager | null; } export interface IResult { - plugin: IPlugin; + plugins: IPlugin[]; code: string; transpiled: boolean; - schema?: string | null; + schemas: Record; } } @@ -44,7 +46,9 @@ export class PluginLoader { return await this._createAsyncFunctionModule(functionBody); } - private async _createAsyncFunctionModule(transpiledCode: string) { + private async _createAsyncFunctionModule( + transpiledCode: string + ): Promise { const module = new AsyncFunction( this._options.transpiler.importFunctionName, transpiledCode @@ -53,52 +57,197 @@ export class PluginLoader { } private async _discoverSchema( - pluginPath: string | null - ): Promise { + pluginPath: string | null, + plugins: ReadonlyArray> + ): Promise> { + const schemas: Record = {}; if (!pluginPath) { - console.warn('Not looking for schema: no path'); - return null; + return schemas; } const serviceManager = this._options.serviceManager; if (!serviceManager) { - console.warn('Not looking for schema: no document manager'); - return null; - } - const candidatePaths = [ - // canonical - PathExt.join(PathExt.dirname(pluginPath), '..', 'schema', 'plugin.json'), - // simplification for dynamic plugins - PathExt.join(PathExt.dirname(pluginPath), 'plugin.json') + return schemas; + } + const sourceDirectory = PathExt.dirname(pluginPath); + const packageJsonPaths = [ + PathExt.join(sourceDirectory, 'package.json'), + PathExt.join(sourceDirectory, '..', 'package.json') ]; - for (const path of candidatePaths) { - console.log(`Looking for schema in ${path}`); - try { - const file = await serviceManager.contents.get(path); - console.log(`Found schema in ${path}`); - return file.content; - } catch (e) { - console.log(`Did not find schema in ${path}`); + + for (const packageJsonPath of packageJsonPaths) { + const packageSchemas = await this._discoverPackageSchemas( + packageJsonPath, + plugins + ); + if (Object.keys(packageSchemas).length > 0) { + return packageSchemas; } } - return null; + + if (plugins.length !== 1) { + return schemas; + } + + const schema = await readContentsFileAsText( + serviceManager, + PathExt.join(sourceDirectory, 'plugin.json') + ); + if (schema !== null) { + schemas[plugins[0].id] = schema; + } + return schemas; + } + + private async _discoverPackageSchemas( + packageJsonPath: string, + plugins: ReadonlyArray> + ): Promise> { + const schemas: Record = {}; + const serviceManager = this._options.serviceManager; + if (!serviceManager) { + return schemas; + } + + const packageJson = await readContentsFileAsText( + serviceManager, + packageJsonPath + ); + if (packageJson === null) { + return schemas; + } + + let schemaDirectoryPath: string | null = null; + try { + const packageData = JSON.parse(packageJson) as { + jupyterlab?: { schemaDir?: unknown }; + }; + const schemaDir = packageData.jupyterlab?.schemaDir; + if (typeof schemaDir === 'string' && schemaDir.trim().length > 0) { + schemaDirectoryPath = PathExt.join( + PathExt.dirname(packageJsonPath), + schemaDir.trim() + ); + } + } catch { + return schemas; + } + + if (!schemaDirectoryPath) { + return schemas; + } + + const schemaDirectory = await getDirectoryModel( + serviceManager, + schemaDirectoryPath + ); + if (!schemaDirectory) { + return schemas; + } + + const schemaFiles = schemaDirectory.content.filter( + (item: Contents.IModel) => + item.type === 'file' && item.name.endsWith('.json') + ); + if (schemaFiles.length === 0) { + return schemas; + } + + if (plugins.length === 1) { + const schemaFile = + schemaFiles.find( + (item: Contents.IModel) => item.name === 'plugin.json' + ) ?? (schemaFiles.length === 1 ? schemaFiles[0] : null); + if (!schemaFile) { + return schemas; + } + const schema = await readContentsFileAsText( + serviceManager, + PathExt.join(schemaDirectory.path, schemaFile.name) + ); + if (schema !== null) { + schemas[plugins[0].id] = schema; + } + return schemas; + } + + // Multi-plugin examples, such as metadata-form, name schema files after + // the plugin id suffix (for example, `:advanced` -> `advanced.json`). + const schemaPaths = new Map( + schemaFiles.map((item: Contents.IModel) => [ + item.name, + PathExt.join(schemaDirectory.path, item.name) + ]) + ); + + for (const plugin of plugins) { + const pluginSuffix = plugin.id.split(':').pop()?.trim(); + if (!pluginSuffix) { + continue; + } + const schemaPath = schemaPaths.get(`${pluginSuffix}.json`); + if (!schemaPath) { + continue; + } + const schema = await readContentsFileAsText(serviceManager, schemaPath); + if (schema !== null) { + schemas[plugin.id] = schema; + } + } + + return schemas; + } + + private async _resolvePlugins( + pluginSource: unknown + ): Promise[]> { + let plugin = pluginSource; + if (typeof plugin === 'function') { + plugin = plugin(); + } + + const loaded = await Promise.resolve(plugin); + return (Array.isArray(loaded) ? loaded : [loaded]).map( + item => item as IPlugin + ); + } + + private _resolvePluginTokens(plugin: IPlugin): void { + plugin.requires = plugin.requires?.map((value: string | Token) => { + if (!isString(value)) { + return value; + } + const token = this._options.tokenMap.get(value); + if (!token) { + throw Error('Required token' + value + 'not found in the token map'); + } + return token; + }); + plugin.optional = plugin.optional + ?.map((value: string | Token) => { + if (!isString(value)) { + return value; + } + const token = this._options.tokenMap.get(value); + if (!token) { + console.log('Optional token' + value + 'not found in the token map'); + } + return token; + }) + .filter((token): token is Token => token != null); } - /** - * Create a plugin from TypeScript code. - */ async load( code: string, basePath: string | null ): Promise { let functionBody: string; - let plugin; + let pluginSource: unknown; let transpiled = true; try { functionBody = this._options.transpiler.transpile(code, true); } catch (error) { if (error instanceof NoDefaultExportError) { - // no export statment - // for compatibility with older version + // Fall back to object-style plugin definitions used by older examples. console.log( 'No default export was found in the plugin code, falling back to object-based evaluation' ); @@ -109,80 +258,58 @@ export class PluginLoader { } } - console.log(functionBody); - let schema: string | null = null; + let schemas: Record = {}; try { if (transpiled) { const module = await this._createAsyncFunctionModule(functionBody); - plugin = module.default; - schema = await this._discoverSchema(basePath); + pluginSource = module.default; } else { const requirejs = this._options.requirejs; - plugin = new Function('require', 'requirejs', 'define', functionBody)( - requirejs.require, - requirejs.require, - requirejs.define - ); + pluginSource = new Function( + 'require', + 'requirejs', + 'define', + functionBody + )(requirejs.require, requirejs.require, requirejs.define); } } catch (e) { throw new PluginLoadingError(e as Error, { code: functionBody, + schemas: {}, transpiled }); } - // We allow one level of indirection (return a function instead of a plugin) - if (typeof plugin === 'function') { - plugin = plugin(); + const plugins = await this._resolvePlugins(pluginSource); + for (const plugin of plugins) { + this._resolvePluginTokens(plugin); } - // Finally, we allow returning a promise (or an async function above). - plugin = (await Promise.resolve(plugin)) as IPlugin; + if (transpiled) { + schemas = await this._discoverSchema(basePath, plugins); + } - plugin.requires = plugin.requires?.map((value: string | Token) => { - if (!isString(value)) { - // already a token - return value; - } - const token = this._options.tokenMap.get(value); - if (!token) { - throw Error('Required token' + value + 'not found in the token map'); - } - return token; - }); - plugin.optional = plugin.optional - ?.map((value: string | Token) => { - if (!isString(value)) { - // already a token - return value; - } - const token = this._options.tokenMap.get(value); - if (!token) { - console.log('Optional token' + value + 'not found in the token map'); - } - return token; - }) - .filter((token): token is Token => token != null); return { - schema, - plugin, + schemas, + plugins, code: functionBody, transpiled }; } } -function isString(value: any): value is string { +function isString(value: unknown): value is string { return typeof value === 'string' || value instanceof String; } export class PluginLoadingError extends Error { constructor( public error: Error, - public partialResult: Omit + public partialResult: Omit ) { - super(); + super(error.message); + this.name = 'PluginLoadingError'; } } diff --git a/src/modules.ts b/src/modules.ts index 3edb5b6..1c1edaf 100644 --- a/src/modules.ts +++ b/src/modules.ts @@ -9,6 +9,8 @@ export function loadKnownModule(name: string): Promise { return import('@codemirror/state') as any; case '@codemirror/view': return import('@codemirror/view') as any; + case '@jupyter-notebook/application': + return import('@jupyter-notebook/application') as any; case '@jupyter-widgets/base': return import('@jupyter-widgets/base') as any; case '@jupyter/collaborative-drive': @@ -151,10 +153,14 @@ export function loadKnownModule(name: string): Promise { return import('@lumino/virtualdom') as any; case '@lumino/widgets': return import('@lumino/widgets') as any; + case '@rjsf/utils': + return import('@rjsf/utils') as any; case 'react': return import('react') as any; case 'react-dom': return import('react-dom') as any; + case 'yjs': + return import('yjs') as any; default: return Promise.resolve(null); } diff --git a/src/resolver.ts b/src/resolver.ts index d854723..b18754a 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -10,12 +10,14 @@ import { IRequireJS } from './requirejs'; import { IModule, IModuleMember } from './types'; -import { ServiceManager, Contents } from '@jupyterlab/services'; +import { ServiceManager } from '@jupyterlab/services'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { formatCDNConsentDialog } from './dialogs'; +import { fileModelToText, getFileModel } from './contents'; + function handleImportError(error: Error, module: string) { return showDialog({ title: `Import in plugin code failed: ${error.message}`, @@ -216,31 +218,27 @@ export class ImportResolver { ); } const base = PathExt.dirname(path); - const candidatePaths = [ - PathExt.join(base, module + '.ts'), - PathExt.join(base, module + '.tsx') - ]; - if (module.endsWith('.svg')) { - candidatePaths.push(PathExt.join(base, module)); - } + const candidatePaths = this._localImportCandidates(base, module); for (const candidatePath of candidatePaths) { - const directory = await serviceManager.contents.get( - PathExt.dirname(candidatePath) - ); - const files = directory.content as Contents.IModel[]; - const filePaths = new Set(files.map(file => file.path)); - - if (filePaths.has(candidatePath)) { - console.log(`Resolved ${module} to ${candidatePath}`); - const file = await serviceManager.contents.get(candidatePath); - if (candidatePath.endsWith('.svg')) { - return { - default: file.content - }; - } - return await this._options.dynamicLoader(file.content); + const file = await getFileModel(serviceManager, candidatePath); + if (!file) { + continue; + } + + console.log(`Resolved ${module} to ${file.path}`); + const content = fileModelToText(file); + if (content === null) { + continue; + } + + if (file.path.endsWith('.svg')) { + return { + default: content as unknown as IModuleMember + }; } + + return await this._options.dynamicLoader(content); } console.warn( `Could not resolve ${module}, candidate paths:`, @@ -248,4 +246,23 @@ export class ImportResolver { ); return null; } + + private _localImportCandidates(basePath: string, module: string): string[] { + const baseCandidate = PathExt.join(basePath, module); + const extension = PathExt.extname(baseCandidate); + const candidates = new Set(); + + if (extension) { + candidates.add(baseCandidate); + } else { + candidates.add(`${baseCandidate}.ts`); + candidates.add(`${baseCandidate}.tsx`); + candidates.add(`${baseCandidate}.js`); + candidates.add(PathExt.join(baseCandidate, 'index.ts')); + candidates.add(PathExt.join(baseCandidate, 'index.tsx')); + candidates.add(PathExt.join(baseCandidate, 'index.js')); + } + + return Array.from(candidates); + } } diff --git a/src/token-sidebar.tsx b/src/token-sidebar.tsx index ddbaa5f..3f83f59 100644 --- a/src/token-sidebar.tsx +++ b/src/token-sidebar.tsx @@ -35,6 +35,7 @@ export class TokenSidebar extends ReactWidget { this._tokens = options.tokens; this._onInsertImport = options.onInsertImport; this._isImportEnabled = options.isImportEnabled; + this.addClass('jp-PluginPlayground-sidebar'); this.addClass('jp-PluginPlayground-tokenSidebar'); } @@ -58,35 +59,35 @@ export class TokenSidebar extends ReactWidget { : this._tokens; return ( -
+
-

+

{filteredTokens.length} of {this._tokens.length} token strings

{filteredTokens.length === 0 ? ( -

+

No matching token strings.

) : ( -
    +
      {filteredTokens.map(token => (
    • -
      - +
      + {token.name}
      {token.description ? ( -

      +

      {token.description}

      ) : null} diff --git a/style/base.css b/style/base.css index 327a7f9..1e3d770 100644 --- a/style/base.css +++ b/style/base.css @@ -4,13 +4,13 @@ https://jupyterlab.readthedocs.io/en/stable/developer/css.html */ -.jp-PluginPlayground-tokenSidebar { +.jp-PluginPlayground-sidebar { background: var(--jp-layout-color1); color: var(--jp-ui-font-color1); height: 100%; } -.jp-PluginPlayground-tokenSidebarInner { +.jp-PluginPlayground-sidebarInner { box-sizing: border-box; display: flex; flex-direction: column; @@ -20,13 +20,13 @@ padding: var(--jp-code-padding); } -.jp-PluginPlayground-tokenCount { +.jp-PluginPlayground-count { color: var(--jp-ui-font-color2); font-size: var(--jp-ui-font-size0); margin: 5px 0px; } -.jp-PluginPlayground-tokenFilter { +.jp-PluginPlayground-filter { background: var(--jp-layout-color1); border: var(--jp-border-width) solid var(--jp-border-color1); border-radius: var(--jp-border-radius); @@ -35,7 +35,7 @@ padding: 4px 8px; } -.jp-PluginPlayground-tokenList { +.jp-PluginPlayground-list { display: flex; flex-direction: column; gap: var(--jp-layout-spacing-standard); @@ -44,21 +44,21 @@ padding: 0; } -.jp-PluginPlayground-tokenListItem { +.jp-PluginPlayground-listItem { border: var(--jp-border-width) solid var(--jp-border-color2); border-radius: var(--jp-border-radius); margin: 0; padding: 8px; } -.jp-PluginPlayground-tokenRow { +.jp-PluginPlayground-row { align-items: center; display: flex; gap: 8px; justify-content: space-between; } -.jp-PluginPlayground-tokenString { +.jp-PluginPlayground-entryLabel { display: block; flex: 1 1 auto; overflow-wrap: anywhere; @@ -69,8 +69,7 @@ gap: 4px; } -.jp-PluginPlayground-copyButton, -.jp-PluginPlayground-importButton { +.jp-PluginPlayground-actionButton { align-items: center; background: var(--jp-layout-color2); border: var(--jp-border-width) solid var(--jp-border-color1); @@ -86,13 +85,12 @@ justify-content: center; } -.jp-PluginPlayground-copyButton:hover, -.jp-PluginPlayground-importButton:hover { +.jp-PluginPlayground-actionButton:hover { background: var(--jp-layout-color3); } -.jp-PluginPlayground-copyButton:focus-visible, -.jp-PluginPlayground-importButton:focus-visible { +.jp-PluginPlayground-actionButton:focus-visible, +.jp-PluginPlayground-filter:focus-visible { outline: var(--jp-border-width) solid var(--jp-brand-color1); outline-offset: 1px; } @@ -111,9 +109,17 @@ width: 14px; } -.jp-PluginPlayground-tokenDescription { +.jp-PluginPlayground-description { color: var(--jp-ui-font-color2); font-size: var(--jp-ui-font-size0); margin: 8px 0 0; white-space: normal; } + +.jp-PluginPlayground-tokenString { + font-family: var(--jp-code-font-family); +} + +.jp-PluginPlayground-exampleError { + color: var(--jp-error-color1); +} diff --git a/ui-tests/tests/plugin-playground.spec.ts b/ui-tests/tests/plugin-playground.spec.ts index fbcc96d..f166d44 100644 --- a/ui-tests/tests/plugin-playground.spec.ts +++ b/ui-tests/tests/plugin-playground.spec.ts @@ -8,7 +8,9 @@ const CREATE_FILE_COMMAND = 'plugin-playground:create-new-plugin'; const TEST_PLUGIN_ID = 'playground-integration-test:plugin'; const TEST_TOGGLE_COMMAND = 'playground-integration-test:toggle'; const TEST_FILE = 'playground-integration-test.ts'; -const TOKEN_SIDEBAR_ID = 'jp-plugin-token-sidebar'; +const PLAYGROUND_SIDEBAR_ID = 'jp-plugin-playground-sidebar'; +const TOKEN_SECTION_ID = 'jp-plugin-token-sidebar'; +const EXAMPLE_SECTION_ID = 'jp-plugin-example-sidebar'; test.use({ autoGoto: false }); @@ -31,22 +33,29 @@ const plugin = { export default plugin; `; -async function openTokenSidebarPanel( - page: IJupyterLabPageFixture +async function openSidebarPanel( + page: IJupyterLabPageFixture, + sectionId?: string ): Promise { - const tokenSidebarTab = page.sidebar.getTabLocator(TOKEN_SIDEBAR_ID); - await expect(tokenSidebarTab).toBeVisible(); - await page.sidebar.openTab(TOKEN_SIDEBAR_ID); + const sidebarTab = page.sidebar.getTabLocator(PLAYGROUND_SIDEBAR_ID); + await expect(sidebarTab).toBeVisible(); + await page.sidebar.openTab(PLAYGROUND_SIDEBAR_ID); - const sidebarSide = await page.sidebar.getTabPosition(TOKEN_SIDEBAR_ID); + const sidebarSide = await page.sidebar.getTabPosition(PLAYGROUND_SIDEBAR_ID); const panel = page.sidebar.getContentPanelLocator(sidebarSide ?? 'right'); await expect(panel).toBeVisible(); - await expect(panel).toHaveAttribute('id', TOKEN_SIDEBAR_ID); - return panel; + await expect(panel).toHaveAttribute('id', PLAYGROUND_SIDEBAR_ID); + if (!sectionId) { + return panel; + } + + const section = panel.locator(`#${sectionId}`); + await expect(section).toBeVisible(); + return section; } async function findImportableToken(panel: Locator): Promise { - const tokenEntries = panel.locator('.jp-PluginPlayground-tokenString'); + const tokenEntries = panel.locator('.jp-PluginPlayground-entryLabel'); const count = await tokenEntries.count(); for (let i = 0; i < count; i++) { const tokenName = (await tokenEntries.nth(i).innerText()).trim(); @@ -94,6 +103,52 @@ test('registers plugin playground commands', async ({ page }) => { ).resolves.toBe(true); }); +test('opens a dummy extension example from the sidebar', async ({ page }) => { + const integrationExampleName = 'integration-example'; + const integrationExampleRoot = `extension-examples/${integrationExampleName}`; + const expectedPath = `${integrationExampleRoot}/src/index.ts`; + + await page.contents.uploadContent( + JSON.stringify( + { + name: '@jupyterlab-examples/integration-example', + description: 'Integration test extension example' + }, + null, + 2 + ), + 'text', + `${integrationExampleRoot}/package.json` + ); + await page.contents.uploadContent( + "const plugin = { id: 'integration-example:plugin', autoStart: true, activate: () => undefined }; export default plugin;\n", + 'text', + expectedPath + ); + + await page.goto(); + const section = await openSidebarPanel(page, EXAMPLE_SECTION_ID); + + const filterInput = section.getByPlaceholder('Filter extension examples'); + await expect(filterInput).toBeVisible(); + await filterInput.fill(integrationExampleName); + + const exampleItems = section.locator('.jp-PluginPlayground-listItem'); + await expect(exampleItems).toHaveCount(1); + const openButton = exampleItems + .first() + .locator('.jp-PluginPlayground-exampleOpenButton'); + await expect(openButton).toBeVisible(); + await openButton.click(); + + await page.waitForFunction((pathToOpen: string) => { + const current = window.jupyterapp.shell + .currentWidget as FileEditorWidget | null; + const path = current?.context?.path; + return path === pathToOpen; + }, expectedPath); +}); + test('loads current editor file as a plugin extension', async ({ page, tmpPath @@ -152,30 +207,30 @@ test('opens token sidebar, shows tokens, and filters by exact token', async ({ page }) => { await page.goto(); - const panel = await openTokenSidebarPanel(page); + const section = await openSidebarPanel(page, TOKEN_SECTION_ID); - const tokenListItems = panel.locator('.jp-PluginPlayground-tokenListItem'); + const tokenListItems = section.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItems.first()).toBeVisible(); expect(await tokenListItems.count()).toBeGreaterThan(0); const firstToken = ( - await panel.locator('.jp-PluginPlayground-tokenString').first().innerText() + await section.locator('.jp-PluginPlayground-entryLabel').first().innerText() ).trim(); expect(firstToken.length).toBeGreaterThan(0); - const filterInput = panel.getByPlaceholder('Filter token strings'); + const filterInput = section.getByPlaceholder('Filter token strings'); await filterInput.fill(firstToken); await expect(tokenListItems).toHaveCount(1); - await expect(panel.locator('.jp-PluginPlayground-tokenString')).toHaveText([ + await expect(section.locator('.jp-PluginPlayground-entryLabel')).toHaveText([ firstToken ]); }); test('token sidebar copy button shows copied state', async ({ page }) => { await page.goto(); - const panel = await openTokenSidebarPanel(page); + const section = await openSidebarPanel(page, TOKEN_SECTION_ID); - const tokenListItem = panel.locator('.jp-PluginPlayground-tokenListItem'); + const tokenListItem = section.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItem.first()).toBeVisible(); const copyButton = tokenListItem @@ -201,11 +256,12 @@ test('token sidebar inserts import statement into active editor', async ({ await page.filebrowser.open(editorPath); expect(await page.activity.activateTab('token-sidebar-import.ts')).toBe(true); - const panel = await openTokenSidebarPanel(page); - const tokenName = await findImportableToken(panel); - const filterInput = panel.getByPlaceholder('Filter token strings'); + const section = await openSidebarPanel(page, TOKEN_SECTION_ID); + + const tokenName = await findImportableToken(section); + const filterInput = section.getByPlaceholder('Filter token strings'); await filterInput.fill(tokenName); - const tokenListItem = panel.locator('.jp-PluginPlayground-tokenListItem'); + const tokenListItem = section.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItem).toHaveCount(1); const importButton = tokenListItem.locator( diff --git a/yarn.lock b/yarn.lock index 8bb27d8..9b1eb6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -600,6 +600,25 @@ __metadata: languageName: node linkType: hard +"@jupyter-notebook/application@npm:^7.0.0": + version: 7.5.5 + resolution: "@jupyter-notebook/application@npm:7.5.5" + dependencies: + "@jupyterlab/application": ~4.5.6 + "@jupyterlab/coreutils": ~6.5.6 + "@jupyterlab/docregistry": ~4.5.6 + "@jupyterlab/rendermime-interfaces": ~3.13.6 + "@jupyterlab/ui-components": ~4.5.6 + "@lumino/algorithm": ^2.0.4 + "@lumino/coreutils": ^2.2.2 + "@lumino/messaging": ^2.0.4 + "@lumino/polling": ^2.1.5 + "@lumino/signaling": ^2.1.5 + "@lumino/widgets": ^2.7.3 + checksum: bbf398597f30d5b6e4ca44a976f3e32c25193bb5903cfdcd598fe0a62bffdc4782395f304ba795d0fbfff327eb528ba38917659ac8d6627dd8985b5f6912ff47 + languageName: node + linkType: hard + "@jupyter-widgets/base@npm:^6.0.0": version: 6.0.11 resolution: "@jupyter-widgets/base@npm:6.0.11" @@ -697,20 +716,20 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/application@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/application@npm:4.5.5" +"@jupyterlab/application@npm:^4.5.5, @jupyterlab/application@npm:~4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/application@npm:4.5.6" dependencies: "@fortawesome/fontawesome-free": ^5.12.0 - "@jupyterlab/apputils": ^4.6.5 - "@jupyterlab/coreutils": ^6.5.5 - "@jupyterlab/docregistry": ^4.5.5 - "@jupyterlab/rendermime": ^4.5.5 - "@jupyterlab/rendermime-interfaces": ^3.13.5 - "@jupyterlab/services": ^7.5.5 - "@jupyterlab/statedb": ^4.5.5 - "@jupyterlab/translation": ^4.5.5 - "@jupyterlab/ui-components": ^4.5.5 + "@jupyterlab/apputils": ^4.6.6 + "@jupyterlab/coreutils": ^6.5.6 + "@jupyterlab/docregistry": ^4.5.6 + "@jupyterlab/rendermime": ^4.5.6 + "@jupyterlab/rendermime-interfaces": ^3.13.6 + "@jupyterlab/services": ^7.5.6 + "@jupyterlab/statedb": ^4.5.6 + "@jupyterlab/translation": ^4.5.6 + "@jupyterlab/ui-components": ^4.5.6 "@lumino/algorithm": ^2.0.4 "@lumino/application": ^2.4.8 "@lumino/commands": ^2.3.3 @@ -721,23 +740,23 @@ __metadata: "@lumino/properties": ^2.0.4 "@lumino/signaling": ^2.1.5 "@lumino/widgets": ^2.7.5 - checksum: 70df5afff4f0c84f1bdaa521711ae0b6b82a009b7c7b7f60180142e633c3c5397e9ceb324a74d797edd104d221d8fd6bf10306faded96a360e508780ca2a3d61 + checksum: 69b3c188d6d1ab6df1ec97575ba93bdf20512cb92f148654552aaed6334a5d619e1f8212fc9b367e206986e3de00c1c1c502af9bb1e9c74aef8783fd90cff3dc languageName: node linkType: hard -"@jupyterlab/apputils@npm:^4.5.0, @jupyterlab/apputils@npm:^4.6.5": - version: 4.6.5 - resolution: "@jupyterlab/apputils@npm:4.6.5" - dependencies: - "@jupyterlab/coreutils": ^6.5.5 - "@jupyterlab/observables": ^5.5.5 - "@jupyterlab/rendermime-interfaces": ^3.13.5 - "@jupyterlab/services": ^7.5.5 - "@jupyterlab/settingregistry": ^4.5.5 - "@jupyterlab/statedb": ^4.5.5 - "@jupyterlab/statusbar": ^4.5.5 - "@jupyterlab/translation": ^4.5.5 - "@jupyterlab/ui-components": ^4.5.5 +"@jupyterlab/apputils@npm:^4.5.0, @jupyterlab/apputils@npm:^4.6.5, @jupyterlab/apputils@npm:^4.6.6": + version: 4.6.6 + resolution: "@jupyterlab/apputils@npm:4.6.6" + dependencies: + "@jupyterlab/coreutils": ^6.5.6 + "@jupyterlab/observables": ^5.5.6 + "@jupyterlab/rendermime-interfaces": ^3.13.6 + "@jupyterlab/services": ^7.5.6 + "@jupyterlab/settingregistry": ^4.5.6 + "@jupyterlab/statedb": ^4.5.6 + "@jupyterlab/statusbar": ^4.5.6 + "@jupyterlab/translation": ^4.5.6 + "@jupyterlab/ui-components": ^4.5.6 "@lumino/algorithm": ^2.0.4 "@lumino/commands": ^2.3.3 "@lumino/coreutils": ^2.2.2 @@ -750,7 +769,7 @@ __metadata: "@types/react": ^18.0.26 react: ^18.2.0 sanitize-html: ~2.12.1 - checksum: f1459a948bded9ec1bf40193f6a2c7cefc2483c619eb6e6008a78fd0f1bfffc76053afbd3b844ef5e3d94130b41218f20f5782541c37a70c475d87d16b04e745 + checksum: 5c72ae37b8f4671786b7ca94a019fc8bdf743afe76f33bc471767fa54b861bda564b50dd2b2eae5b57ee862b169e0155f190f76e4bdcc7d33a1cd0ee2842bed3 languageName: node linkType: hard @@ -866,19 +885,19 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/codeeditor@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/codeeditor@npm:4.5.5" +"@jupyterlab/codeeditor@npm:^4.5.5, @jupyterlab/codeeditor@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/codeeditor@npm:4.5.6" dependencies: "@codemirror/state": ^6.5.4 "@jupyter/ydoc": ^3.1.0 - "@jupyterlab/apputils": ^4.6.5 - "@jupyterlab/coreutils": ^6.5.5 - "@jupyterlab/nbformat": ^4.5.5 - "@jupyterlab/observables": ^5.5.5 - "@jupyterlab/statusbar": ^4.5.5 - "@jupyterlab/translation": ^4.5.5 - "@jupyterlab/ui-components": ^4.5.5 + "@jupyterlab/apputils": ^4.6.6 + "@jupyterlab/coreutils": ^6.5.6 + "@jupyterlab/nbformat": ^4.5.6 + "@jupyterlab/observables": ^5.5.6 + "@jupyterlab/statusbar": ^4.5.6 + "@jupyterlab/translation": ^4.5.6 + "@jupyterlab/ui-components": ^4.5.6 "@lumino/coreutils": ^2.2.2 "@lumino/disposable": ^2.1.5 "@lumino/dragdrop": ^2.1.8 @@ -886,7 +905,7 @@ __metadata: "@lumino/signaling": ^2.1.5 "@lumino/widgets": ^2.7.5 react: ^18.2.0 - checksum: b74ba6c6b24924f2fd63d35e25ebbc7ddbfa32e8ae77763b01b3dea647d141ff796b8639b9e1b1221c8dd0646ea9837e0d32f3936e41918213173fb613f887aa + checksum: 298cab335372ba900dcf1f811f31272f2fb214b42411eca44b39c66c1910b865a223015642b3f8ddc0a24e1a5eba7b424f771ded735f74e602d21491ad3c945f languageName: node linkType: hard @@ -986,9 +1005,9 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/coreutils@npm:^6.5.0, @jupyterlab/coreutils@npm:^6.5.5": - version: 6.5.5 - resolution: "@jupyterlab/coreutils@npm:6.5.5" +"@jupyterlab/coreutils@npm:^6.5.0, @jupyterlab/coreutils@npm:^6.5.5, @jupyterlab/coreutils@npm:^6.5.6, @jupyterlab/coreutils@npm:~6.5.6": + version: 6.5.6 + resolution: "@jupyterlab/coreutils@npm:6.5.6" dependencies: "@lumino/coreutils": ^2.2.2 "@lumino/disposable": ^2.1.5 @@ -996,7 +1015,7 @@ __metadata: minimist: ~1.2.0 path-browserify: ^1.0.0 url-parse: ~1.5.4 - checksum: 044e7639afacb53cfcb75ab5e9020a9ee042b2ef13ff2906531a774f31177bdcec3380a7e60313c8ee632f035e825e6900b8e0f3a54c88a6efa05560a6f55275 + checksum: e693f47d86dcff9762340d8a7df504b9b258a03785243b8aac8606321216efac9c965bbf4197abbcb1f175dade44d0f53f0a9ff70f14a16e5243fae3a5ef16cf languageName: node linkType: hard @@ -1083,20 +1102,20 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/docregistry@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/docregistry@npm:4.5.5" +"@jupyterlab/docregistry@npm:^4.5.5, @jupyterlab/docregistry@npm:^4.5.6, @jupyterlab/docregistry@npm:~4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/docregistry@npm:4.5.6" dependencies: "@jupyter/ydoc": ^3.1.0 - "@jupyterlab/apputils": ^4.6.5 - "@jupyterlab/codeeditor": ^4.5.5 - "@jupyterlab/coreutils": ^6.5.5 - "@jupyterlab/observables": ^5.5.5 - "@jupyterlab/rendermime": ^4.5.5 - "@jupyterlab/rendermime-interfaces": ^3.13.5 - "@jupyterlab/services": ^7.5.5 - "@jupyterlab/translation": ^4.5.5 - "@jupyterlab/ui-components": ^4.5.5 + "@jupyterlab/apputils": ^4.6.6 + "@jupyterlab/codeeditor": ^4.5.6 + "@jupyterlab/coreutils": ^6.5.6 + "@jupyterlab/observables": ^5.5.6 + "@jupyterlab/rendermime": ^4.5.6 + "@jupyterlab/rendermime-interfaces": ^3.13.6 + "@jupyterlab/services": ^7.5.6 + "@jupyterlab/translation": ^4.5.6 + "@jupyterlab/ui-components": ^4.5.6 "@lumino/algorithm": ^2.0.4 "@lumino/coreutils": ^2.2.2 "@lumino/disposable": ^2.1.5 @@ -1105,7 +1124,7 @@ __metadata: "@lumino/signaling": ^2.1.5 "@lumino/widgets": ^2.7.5 react: ^18.2.0 - checksum: fc07e1dbaabdea83638e1d737b1f33d77c016b07cb1bb6c7358d5eb83e0ec9cae7ab44057d9ec391ebfc5590e37807f4ff94e62524cf06f23b798578ac4c0194 + checksum: e57126380d3abca32165c659fc3dd63c4642abc2dfe35017c38fd4ca91ae9cfcb95427c98fd47aa0f546c8030eb1af42be60c2a40bc03f4411fa9f92b77a4fe3 languageName: node linkType: hard @@ -1415,12 +1434,12 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/nbformat@npm:^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0, @jupyterlab/nbformat@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/nbformat@npm:4.5.5" +"@jupyterlab/nbformat@npm:^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0, @jupyterlab/nbformat@npm:^4.5.5, @jupyterlab/nbformat@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/nbformat@npm:4.5.6" dependencies: "@lumino/coreutils": ^2.2.2 - checksum: 3db7d9fa500161bd1d0a5abcd7c05f7a45abc6180ccab0023bc3f80cfdc6a354de61970f67c6835de87d2ad40c520d59e6cc3bf27834dc8f18d0dc867f130b3e + checksum: 136f00cf215a7a4a9061d8874ac35191f5f65c62cdb7687578d77772d8d6fb866905710511986ef533a383826d1cc2807cfc58e5f7076b853703cae15b3c8bc8 languageName: node linkType: hard @@ -1463,16 +1482,16 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/observables@npm:^5.5.5": - version: 5.5.5 - resolution: "@jupyterlab/observables@npm:5.5.5" +"@jupyterlab/observables@npm:^5.5.5, @jupyterlab/observables@npm:^5.5.6": + version: 5.5.6 + resolution: "@jupyterlab/observables@npm:5.5.6" dependencies: "@lumino/algorithm": ^2.0.4 "@lumino/coreutils": ^2.2.2 "@lumino/disposable": ^2.1.5 "@lumino/messaging": ^2.0.4 "@lumino/signaling": ^2.1.5 - checksum: 107880c918f2c73ac8e4d5bcbedf2fa221607c50752e3f1d930f4054a0470603f7987083d875f179eb12ac83e0a179466c616ecf4102ec04024ddd5422e89563 + checksum: 1c1e04644929f2f8535126244c71bc916e1759b256d6442cfeef2fb68ebeaa872aa4cabc7dadce1c14ab419a742be9ef30b6e940b09d559705d47858a55b4174 languageName: node linkType: hard @@ -1503,6 +1522,7 @@ __metadata: resolution: "@jupyterlab/plugin-playground@workspace:." dependencies: "@eslint/js": ^9.39.3 + "@jupyter-notebook/application": ^7.0.0 "@jupyter-widgets/base": ^6.0.0 "@jupyter/collaborative-drive": ^4.2.1 "@jupyter/docprovider": ^4.1.1 @@ -1540,6 +1560,7 @@ __metadata: "@jupyterlab/tooltip": ^4.5.5 "@jupyterlab/workspaces": ^4.5.5 "@lumino/datagrid": ^2.0.0 + "@rjsf/utils": ^5.13.4 "@types/codemirror": ^5.6.20 "@types/react": ^18.0.0 "@types/react-dom": ^18.0.0 @@ -1554,6 +1575,7 @@ __metadata: requirejs: ^2.3.6 rimraf: ^3.0.2 typescript: ~5.5.4 + yjs: ^13.5.40 languageName: unknown linkType: soft @@ -1591,33 +1613,33 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/rendermime-interfaces@npm:^3.13.5": - version: 3.13.5 - resolution: "@jupyterlab/rendermime-interfaces@npm:3.13.5" +"@jupyterlab/rendermime-interfaces@npm:^3.13.5, @jupyterlab/rendermime-interfaces@npm:^3.13.6, @jupyterlab/rendermime-interfaces@npm:~3.13.6": + version: 3.13.6 + resolution: "@jupyterlab/rendermime-interfaces@npm:3.13.6" dependencies: "@lumino/coreutils": ^1.11.0 || ^2.2.2 "@lumino/widgets": ^1.37.2 || ^2.7.5 - checksum: b128c5babd0728383f8e35af16aa76cd63e8a4e922f74643298baf647419885f1c7cacf1f7bcd6f6094ba5e7d7d0b20900d1a127c297c1c8df7d6a78f9ee6463 + checksum: 23bbf8f18712d6f14444ef20ed851fa8d787f4cdfaf4e43348365a3d6042ff1fe43e738b99a0e6a921d8205f7b97cc8d4016eb0863950266e105699fcd13a8d6 languageName: node linkType: hard -"@jupyterlab/rendermime@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/rendermime@npm:4.5.5" +"@jupyterlab/rendermime@npm:^4.5.5, @jupyterlab/rendermime@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/rendermime@npm:4.5.6" dependencies: - "@jupyterlab/apputils": ^4.6.5 - "@jupyterlab/coreutils": ^6.5.5 - "@jupyterlab/nbformat": ^4.5.5 - "@jupyterlab/observables": ^5.5.5 - "@jupyterlab/rendermime-interfaces": ^3.13.5 - "@jupyterlab/services": ^7.5.5 - "@jupyterlab/translation": ^4.5.5 + "@jupyterlab/apputils": ^4.6.6 + "@jupyterlab/coreutils": ^6.5.6 + "@jupyterlab/nbformat": ^4.5.6 + "@jupyterlab/observables": ^5.5.6 + "@jupyterlab/rendermime-interfaces": ^3.13.6 + "@jupyterlab/services": ^7.5.6 + "@jupyterlab/translation": ^4.5.6 "@lumino/coreutils": ^2.2.2 "@lumino/messaging": ^2.0.4 "@lumino/signaling": ^2.1.5 "@lumino/widgets": ^2.7.5 lodash.escape: ^4.0.1 - checksum: 9ce76bbfa007830ad622eaf421a74d3cd8f4f9cd72d489cf70fa7141d5dbe009d8bd8991390afc3efe898574ae494c0d1a73a373c16e0279829886829f4b4d81 + checksum: 8c944ea0358cdabc5f8c7a4b3df1f8da48898dd0c3f3da0afe97bb36155b3172ce8061c87cdf98f71970893b05199d7e420eefdd85c7c89fd355639a493370c0 languageName: node linkType: hard @@ -1641,22 +1663,22 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/services@npm:^6 || ^7, @jupyterlab/services@npm:^7.5.0, @jupyterlab/services@npm:^7.5.5": - version: 7.5.5 - resolution: "@jupyterlab/services@npm:7.5.5" +"@jupyterlab/services@npm:^6 || ^7, @jupyterlab/services@npm:^7.5.0, @jupyterlab/services@npm:^7.5.5, @jupyterlab/services@npm:^7.5.6": + version: 7.5.6 + resolution: "@jupyterlab/services@npm:7.5.6" dependencies: "@jupyter/ydoc": ^3.1.0 - "@jupyterlab/coreutils": ^6.5.5 - "@jupyterlab/nbformat": ^4.5.5 - "@jupyterlab/settingregistry": ^4.5.5 - "@jupyterlab/statedb": ^4.5.5 + "@jupyterlab/coreutils": ^6.5.6 + "@jupyterlab/nbformat": ^4.5.6 + "@jupyterlab/settingregistry": ^4.5.6 + "@jupyterlab/statedb": ^4.5.6 "@lumino/coreutils": ^2.2.2 "@lumino/disposable": ^2.1.5 "@lumino/polling": ^2.1.5 "@lumino/properties": ^2.0.4 "@lumino/signaling": ^2.1.5 ws: ^8.11.0 - checksum: 54b0483c72835367085dc03f2066f8a4458f234d94b8ff16921f05821d06a742148d8977c38e0c73224dbecf6eb42b8519853884afd2a7003a515a4c1ba7fc1a + checksum: c5b0d9ba7f2ccc245eb7bbf6f32c174116ff70f7d173168b66649b6d35cbca118e2e1be6fa7f305979eac3c8478dc228990b1c5537aec96b51cd637b2ab792a3 languageName: node linkType: hard @@ -1690,12 +1712,12 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/settingregistry@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/settingregistry@npm:4.5.5" +"@jupyterlab/settingregistry@npm:^4.5.5, @jupyterlab/settingregistry@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/settingregistry@npm:4.5.6" dependencies: - "@jupyterlab/nbformat": ^4.5.5 - "@jupyterlab/statedb": ^4.5.5 + "@jupyterlab/nbformat": ^4.5.6 + "@jupyterlab/statedb": ^4.5.6 "@lumino/commands": ^2.3.3 "@lumino/coreutils": ^2.2.2 "@lumino/disposable": ^2.1.5 @@ -1705,28 +1727,28 @@ __metadata: json5: ^2.2.3 peerDependencies: react: ">=16" - checksum: 722f0d404cb56167e49bda3d4ad269a880d790adc25708d74287ab2980865e0d9c8f98bac45e4c58af7df14dfcc486916c18951d9b8f9c978d07643257be5a3a + checksum: fd8490e0a2e7a8359fc6c87e1988bda6ce168be202fba3888b77a9bbd4b0967a2486d39c73464f2295a17615cc715ab48733e6fa4d52b4565bb003e3d586c99b languageName: node linkType: hard -"@jupyterlab/statedb@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/statedb@npm:4.5.5" +"@jupyterlab/statedb@npm:^4.5.5, @jupyterlab/statedb@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/statedb@npm:4.5.6" dependencies: "@lumino/commands": ^2.3.3 "@lumino/coreutils": ^2.2.2 "@lumino/disposable": ^2.1.5 "@lumino/properties": ^2.0.4 "@lumino/signaling": ^2.1.5 - checksum: a3bd24f190420aa631af6311ff2cb91bcd23aeebd041f3a211c30d39be4593af48795f2d9c9efd25f004310547ba001f3b1509a1b0992fba43208082ac322efe + checksum: 5a9a237ed1cd215ddb672bc080da5966320716b7ea6a018eeca2c5cc3689b0b9709843e1ef2452657976b751ce00cf64c82375f3dc796d844f94dcf856c50884 languageName: node linkType: hard -"@jupyterlab/statusbar@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/statusbar@npm:4.5.5" +"@jupyterlab/statusbar@npm:^4.5.5, @jupyterlab/statusbar@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/statusbar@npm:4.5.6" dependencies: - "@jupyterlab/ui-components": ^4.5.5 + "@jupyterlab/ui-components": ^4.5.6 "@lumino/algorithm": ^2.0.4 "@lumino/coreutils": ^2.2.2 "@lumino/disposable": ^2.1.5 @@ -1734,7 +1756,7 @@ __metadata: "@lumino/signaling": ^2.1.5 "@lumino/widgets": ^2.7.5 react: ^18.2.0 - checksum: dfe4da0b2c373e8e2c3458c720b1940e574cf2e26869319cb6018b8eddebfa4bf21b7022ba65fe1cd33049c9c139045052bbadf61a1ae749fec7490d115cec9f + checksum: 80a5bc008827f32f8fb99e913262eaeba27e5ec92ca8e194d856e071efa1f62189a369b2c9276808377c7740ea27bbd5f8430b51e3c7a9ea1d8017f166594425 languageName: node linkType: hard @@ -1799,29 +1821,29 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/translation@npm:^4.5.0, @jupyterlab/translation@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/translation@npm:4.5.5" +"@jupyterlab/translation@npm:^4.5.0, @jupyterlab/translation@npm:^4.5.5, @jupyterlab/translation@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/translation@npm:4.5.6" dependencies: - "@jupyterlab/coreutils": ^6.5.5 - "@jupyterlab/rendermime-interfaces": ^3.13.5 - "@jupyterlab/services": ^7.5.5 - "@jupyterlab/statedb": ^4.5.5 + "@jupyterlab/coreutils": ^6.5.6 + "@jupyterlab/rendermime-interfaces": ^3.13.6 + "@jupyterlab/services": ^7.5.6 + "@jupyterlab/statedb": ^4.5.6 "@lumino/coreutils": ^2.2.2 - checksum: a8965bae1806361470c0799180df930f9d9be030d74424753c4e43effd3f2db2032be56b35d0cf77eb37663d58f71d988e14fd6d9def2fbb53571fbab981b110 + checksum: 05db816acc6b908a02c80ff67c2239a54f1b43260b57ffc0038224ec5d082ed6167d27d4e3cccfd871f7408c10e6c36c8ab9fcd419b69e7a9ded4307bf3e16ad languageName: node linkType: hard -"@jupyterlab/ui-components@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/ui-components@npm:4.5.5" +"@jupyterlab/ui-components@npm:^4.5.5, @jupyterlab/ui-components@npm:^4.5.6, @jupyterlab/ui-components@npm:~4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/ui-components@npm:4.5.6" dependencies: "@jupyter/react-components": ^0.16.6 "@jupyter/web-components": ^0.16.6 - "@jupyterlab/coreutils": ^6.5.5 - "@jupyterlab/observables": ^5.5.5 - "@jupyterlab/rendermime-interfaces": ^3.13.5 - "@jupyterlab/translation": ^4.5.5 + "@jupyterlab/coreutils": ^6.5.6 + "@jupyterlab/observables": ^5.5.6 + "@jupyterlab/rendermime-interfaces": ^3.13.6 + "@jupyterlab/translation": ^4.5.6 "@lumino/algorithm": ^2.0.4 "@lumino/commands": ^2.3.3 "@lumino/coreutils": ^2.2.2 @@ -1839,7 +1861,7 @@ __metadata: typestyle: ^2.0.4 peerDependencies: react: ^18.2.0 - checksum: 51dd8aecaf5ced1e82418daf13e9c1f8196146bad95f4e7b59bc9810544fbecffc31354a70d15a2344eaa686a981db35f6c5e5ba341084700c57fa86b7c8ae8b + checksum: 23749d9ba442a49676c95c371c04440b55843d18b121723e170bcb086c4e447d7484745e873cdf47f256e1497304d07370bef875a8c80a5d15bbd5f54c2662a5 languageName: node linkType: hard @@ -2161,7 +2183,7 @@ __metadata: languageName: node linkType: hard -"@lumino/widgets@npm:^1 || ^2, @lumino/widgets@npm:^1.37.2 || ^2.7.5, @lumino/widgets@npm:^2.7.0, @lumino/widgets@npm:^2.7.5": +"@lumino/widgets@npm:^1 || ^2, @lumino/widgets@npm:^1.37.2 || ^2.7.5, @lumino/widgets@npm:^2.7.0, @lumino/widgets@npm:^2.7.3, @lumino/widgets@npm:^2.7.5": version: 2.7.5 resolution: "@lumino/widgets@npm:2.7.5" dependencies: @@ -3242,20 +3264,7 @@ __metadata: languageName: node linkType: hard -"abstract-leveldown@npm:^6.2.1": - version: 6.3.0 - resolution: "abstract-leveldown@npm:6.3.0" - dependencies: - buffer: ^5.5.0 - immediate: ^3.2.3 - level-concat-iterator: ~2.0.0 - level-supports: ~1.0.0 - xtend: ~4.0.0 - checksum: 121a8509d8c6a540e656c2a69e5b8d853d4df71072011afefc868b98076991bb00120550e90643de9dc18889c675f62413409eeb4c8c204663124c7d215e4ec3 - languageName: node - linkType: hard - -"abstract-leveldown@npm:~6.2.1, abstract-leveldown@npm:~6.2.3": +"abstract-leveldown@npm:^6.2.1, abstract-leveldown@npm:~6.2.1, abstract-leveldown@npm:~6.2.3": version: 6.2.3 resolution: "abstract-leveldown@npm:6.2.3" dependencies: @@ -5343,21 +5352,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.3": - version: 7.2.3 - resolution: "glob@npm:7.2.3" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.1.1 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 - languageName: node - linkType: hard - -"glob@npm:~7.1.6": +"glob@npm:^7.1.3, glob@npm:~7.1.6": version: 7.1.7 resolution: "glob@npm:7.1.7" dependencies: @@ -5663,14 +5658,7 @@ __metadata: languageName: node linkType: hard -"internmap@npm:1 - 2": - version: 2.0.3 - resolution: "internmap@npm:2.0.3" - checksum: 7ca41ec6aba8f0072fc32fa8a023450a9f44503e2d8e403583c55714b25efd6390c38a87161ec456bf42d7bc83aab62eb28f5aef34876b1ac4e60693d5e1d241 - languageName: node - linkType: hard - -"internmap@npm:^1.0.0": +"internmap@npm:1 - 2, internmap@npm:^1.0.0": version: 1.0.1 resolution: "internmap@npm:1.0.1" checksum: 9d00f8c0cf873a24a53a5a937120dab634c41f383105e066bb318a61864e6292d24eb9516e8e7dccfb4420ec42ca474a0f28ac9a6cc82536898fa09bbbe53813 @@ -6714,7 +6702,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2, minimatch@npm:^3.1.3": +"minimatch@npm:^3.0.4, minimatch@npm:^3.1.2, minimatch@npm:^3.1.3": version: 3.1.5 resolution: "minimatch@npm:3.1.5" dependencies: @@ -8639,7 +8627,7 @@ __metadata: languageName: node linkType: hard -"vscode-jsonrpc@npm:8.2.0": +"vscode-jsonrpc@npm:8.2.0, vscode-jsonrpc@npm:^8.0.2": version: 8.2.0 resolution: "vscode-jsonrpc@npm:8.2.0" checksum: f302a01e59272adc1ae6494581fa31c15499f9278df76366e3b97b2236c7c53ebfc71efbace9041cfd2caa7f91675b9e56f2407871a1b3c7f760a2e2ee61484a @@ -8653,13 +8641,6 @@ __metadata: languageName: node linkType: hard -"vscode-jsonrpc@npm:^8.0.2": - version: 8.2.1 - resolution: "vscode-jsonrpc@npm:8.2.1" - checksum: 2af2c333d73f6587896a7077978b8d4b430e55c674d5dbb90597a84a6647057c1655a3bff398a9b08f1f8ba57dbd2deabf05164315829c297b0debba3b8bc19e - languageName: node - linkType: hard - "vscode-languageserver-protocol@npm:3.17.5, vscode-languageserver-protocol@npm:^3.17.0": version: 3.17.5 resolution: "vscode-languageserver-protocol@npm:3.17.5"