From bc2e9457050bbba1327d3e0f1198ac6d296956a6 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 6 Mar 2026 16:55:38 +0530 Subject: [PATCH 01/23] Add extension-examples as submodule --- .gitmodules | 3 +++ extension-examples | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 extension-examples 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/extension-examples b/extension-examples new file mode 160000 index 0000000..31dccfc --- /dev/null +++ b/extension-examples @@ -0,0 +1 @@ +Subproject commit 31dccfcf2fffb9e199c128744dc20596363932e1 From 4025a5ded3486831c208c83fe9ace2b38521fe8e Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 6 Mar 2026 18:07:59 +0530 Subject: [PATCH 02/23] Integrate extension-examples submodule and add examples sidebar --- README.md | 2 + binder/postBuild | 23 +++- src/example-sidebar.tsx | 160 +++++++++++++++++++++++ src/index.ts | 158 ++++++++++++++++++++++ src/token-sidebar.tsx | 23 ++-- style/base.css | 36 ++--- ui-tests/tests/plugin-playground.spec.ts | 67 +++++++--- 7 files changed, 425 insertions(+), 44 deletions(-) create mode 100644 src/example-sidebar.tsx diff --git a/README.md b/README.md index 48104dd..b80438d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ This extension provides a new command, `Load Current File As Extension`, availab 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 second right sidebar panel with discovered examples from a local clone of [`jupyterlab/extension-examples`](https://github.com/jupyterlab/extension-examples), so you can open them directly from the panel. + 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. ```typescript 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/src/example-sidebar.tsx b/src/example-sidebar.tsx new file mode 100644 index 0000000..6cdec35 --- /dev/null +++ b/src/example-sidebar.tsx @@ -0,0 +1,160 @@ +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 { + private readonly _fetchExamples: () => Promise< + ReadonlyArray + >; + private readonly _onOpenExample: ( + examplePath: string + ) => Promise | void; + private _query = ''; + private _examples: ReadonlyArray = []; + private _isLoading = false; + private _errorMessage = ''; + + 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. Ensure the repository submodule is + initialized. +

+ ) : 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()] + }); + } + } +} diff --git a/src/index.ts b/src/index.ts index 9896ad9..d473341 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,8 @@ import { extensionIcon } from '@jupyterlab/ui-components'; import { IDocumentManager } from '@jupyterlab/docmanager'; +import { Contents } from '@jupyterlab/services'; + import { PluginLoader, PluginLoadingError } from './loader'; import { PluginTranspiler } from './transpiler'; @@ -40,6 +42,8 @@ import { IRequireJS, RequireJSLoader } from './requirejs'; import { TokenSidebar } from './token-sidebar'; +import { ExampleSidebar } from './example-sidebar'; + import { tokenSidebarIcon } from './icons'; import { Token } from '@lumino/coreutils'; @@ -93,6 +97,18 @@ interface IPrivatePluginData { }; } +type IDirectoryModel = Contents.IModel & { + type: 'directory'; + content: Contents.IModel[]; +}; + +type ITextFileModel = Contents.IModel & { + type: 'file'; + content: string; +}; + +const EXTENSION_EXAMPLES_ROOT = 'extension-examples'; + class PluginPlayground { constructor( protected app: JupyterFrontEnd, @@ -181,6 +197,17 @@ class PluginPlayground { 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.caption = + 'jupyterlab/extension-examples plugin entrypoints'; + exampleSidebar.title.icon = extensionIcon; + this.app.shell.add(exampleSidebar, 'right', { rank: 651 }); + app.shell.currentChanged?.connect(() => { tokenSidebar.update(); }); @@ -316,6 +343,135 @@ class PluginPlayground { this._loadPlugin(jsBody, null); } + private async _openExtensionExample(examplePath: string): Promise { + await this.app.commands.execute('docmanager:open', { + path: examplePath, + factory: 'Editor' + }); + } + + private async _discoverExtensionExamples(): Promise< + ReadonlyArray + > { + const rootDirectory = await this._getDirectoryModel( + EXTENSION_EXAMPLES_ROOT + ); + if (!rootDirectory) { + return []; + } + + const discovered: ExampleSidebar.IExampleRecord[] = []; + for (const item of rootDirectory.content) { + if (item.type !== 'directory' || item.name.startsWith('.')) { + continue; + } + const exampleDirectory = this._joinPath( + EXTENSION_EXAMPLES_ROOT, + 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 _getDirectoryModel( + path: string + ): Promise { + try { + const model = await this.app.serviceManager.contents.get(path, { + content: true + }); + if (model.type !== 'directory' || !Array.isArray(model.content)) { + return null; + } + return model as IDirectoryModel; + } catch { + return null; + } + } + + private async _findExampleEntrypoint( + directoryPath: string + ): Promise { + const srcDirectory = await this._getDirectoryModel( + 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 this._joinPath(srcDirectory.path, entrypoint.name); + } + + private async _readExampleDescription( + directoryPath: string + ): Promise { + const packageJsonPath = this._joinPath(directoryPath, 'package.json'); + const packageJson = await this._getTextFileModel(packageJsonPath); + if (!packageJson) { + return this._fallbackExampleDescription; + } + try { + const packageData = JSON.parse(packageJson.content) as { + description?: unknown; + }; + if (typeof packageData.description === 'string') { + const description = packageData.description.trim(); + if (description.length > 0) { + return description; + } + } + } catch { + // fall back to a default description + } + return this._fallbackExampleDescription; + } + + private _joinPath(base: string, child: string): string { + const normalizedBase = base.replace(/\/+$/g, ''); + const normalizedChild = child.replace(/^\/+/g, ''); + if (!normalizedBase) { + return normalizedChild; + } + return `${normalizedBase}/${normalizedChild}`; + } + + private async _getTextFileModel( + path: string + ): Promise { + try { + const model = await this.app.serviceManager.contents.get(path, { + content: true, + format: 'text' + }); + if (model.type !== 'file' || typeof model.content !== 'string') { + return null; + } + return model as ITextFileModel; + } catch { + return null; + } + } + private _populateTokenMap(): void { const app = this.app as unknown as IPrivateServiceStore; this._tokenMap.clear(); @@ -488,6 +644,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(); } 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..427e7a0 100644 --- a/ui-tests/tests/plugin-playground.spec.ts +++ b/ui-tests/tests/plugin-playground.spec.ts @@ -9,6 +9,7 @@ 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 EXAMPLE_SIDEBAR_ID = 'jp-plugin-example-sidebar'; test.use({ autoGoto: false }); @@ -31,22 +32,23 @@ const plugin = { export default plugin; `; -async function openTokenSidebarPanel( - page: IJupyterLabPageFixture +async function openSidebarPanel( + page: IJupyterLabPageFixture, + sidebarId: 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(sidebarId); + await expect(sidebarTab).toBeVisible(); + await page.sidebar.openTab(sidebarId); - const sidebarSide = await page.sidebar.getTabPosition(TOKEN_SIDEBAR_ID); + const sidebarSide = await page.sidebar.getTabPosition(sidebarId); const panel = page.sidebar.getContentPanelLocator(sidebarSide ?? 'right'); await expect(panel).toBeVisible(); - await expect(panel).toHaveAttribute('id', TOKEN_SIDEBAR_ID); + await expect(panel).toHaveAttribute('id', sidebarId); return panel; } 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 +96,39 @@ test('registers plugin playground commands', async ({ page }) => { ).resolves.toBe(true); }); +test('opens an extension example from the sidebar', async ({ page }) => { + await page.goto(); + const panel = await openSidebarPanel(page, EXAMPLE_SIDEBAR_ID); + + const exampleItems = panel.locator('.jp-PluginPlayground-listItem'); + await expect(exampleItems.first()).toBeVisible(); + expect(await exampleItems.count()).toBeGreaterThan(0); + + const firstExampleName = ( + await panel.locator('.jp-PluginPlayground-entryLabel').first().innerText() + ).trim(); + expect(firstExampleName.length).toBeGreaterThan(0); + + const openButton = exampleItems + .first() + .locator('.jp-PluginPlayground-exampleOpenButton'); + await expect(openButton).toBeVisible(); + await openButton.click(); + + await page.waitForFunction((exampleName: string) => { + const current = window.jupyterapp.shell + .currentWidget as FileEditorWidget | null; + const path = current?.context?.path; + if (typeof path !== 'string') { + return false; + } + return ( + path === `extension-examples/${exampleName}/src/index.ts` || + path === `extension-examples/${exampleName}/src/index.js` + ); + }, firstExampleName); +}); + test('loads current editor file as a plugin extension', async ({ page, tmpPath @@ -152,30 +187,30 @@ test('opens token sidebar, shows tokens, and filters by exact token', async ({ page }) => { await page.goto(); - const panel = await openTokenSidebarPanel(page); + const panel = await openSidebarPanel(page, TOKEN_SIDEBAR_ID); - const tokenListItems = panel.locator('.jp-PluginPlayground-tokenListItem'); + const tokenListItems = panel.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 panel.locator('.jp-PluginPlayground-entryLabel').first().innerText() ).trim(); expect(firstToken.length).toBeGreaterThan(0); const filterInput = panel.getByPlaceholder('Filter token strings'); await filterInput.fill(firstToken); await expect(tokenListItems).toHaveCount(1); - await expect(panel.locator('.jp-PluginPlayground-tokenString')).toHaveText([ + await expect(panel.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 panel = await openSidebarPanel(page, TOKEN_SIDEBAR_ID); - const tokenListItem = panel.locator('.jp-PluginPlayground-tokenListItem'); + const tokenListItem = panel.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItem.first()).toBeVisible(); const copyButton = tokenListItem @@ -201,11 +236,11 @@ 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 panel = await openSidebarPanel(page, TOKEN_SIDEBAR_ID); const tokenName = await findImportableToken(panel); const filterInput = panel.getByPlaceholder('Filter token strings'); await filterInput.fill(tokenName); - const tokenListItem = panel.locator('.jp-PluginPlayground-tokenListItem'); + const tokenListItem = panel.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItem).toHaveCount(1); const importButton = tokenListItem.locator( From 5e2e4c55597d0e4337bbfa12c8fc0c445b1a80a3 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 6 Mar 2026 18:37:25 +0530 Subject: [PATCH 03/23] Added a cool new icon --- src/icons.ts | 6 ++++++ src/index.ts | 4 ++-- style/icons/examples-sidebar.svg | 34 ++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 style/icons/examples-sidebar.svg diff --git a/src/icons.ts b/src/icons.ts index b81d92f..40ceb79 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -1,8 +1,14 @@ import { LabIcon } from '@jupyterlab/ui-components'; import tokenSidebarIconSvgstr from '!!raw-loader!../style/icons/token-sidebar.svg'; +import examplesSidebarIconSvgstr from '!!raw-loader!../style/icons/examples-sidebar.svg'; export const tokenSidebarIcon = new LabIcon({ name: 'plugin-playground:token-sidebar', svgstr: tokenSidebarIconSvgstr }); + +export const examplesSidebarIcon = new LabIcon({ + name: 'plugin-playground:examples-sidebar', + svgstr: examplesSidebarIconSvgstr +}); diff --git a/src/index.ts b/src/index.ts index d473341..b1eca07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,7 +44,7 @@ import { TokenSidebar } from './token-sidebar'; import { ExampleSidebar } from './example-sidebar'; -import { tokenSidebarIcon } from './icons'; +import { tokenSidebarIcon, examplesSidebarIcon } from './icons'; import { Token } from '@lumino/coreutils'; @@ -205,7 +205,7 @@ class PluginPlayground { exampleSidebar.id = 'jp-plugin-example-sidebar'; exampleSidebar.title.caption = 'jupyterlab/extension-examples plugin entrypoints'; - exampleSidebar.title.icon = extensionIcon; + exampleSidebar.title.icon = examplesSidebarIcon; this.app.shell.add(exampleSidebar, 'right', { rank: 651 }); app.shell.currentChanged?.connect(() => { diff --git a/style/icons/examples-sidebar.svg b/style/icons/examples-sidebar.svg new file mode 100644 index 0000000..cfbe036 --- /dev/null +++ b/style/icons/examples-sidebar.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 9dc5c8f1df524ccca5736859a555a4eca0135114 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 6 Mar 2026 23:42:45 +0530 Subject: [PATCH 04/23] add suggestion --- .gitignore | 1 + .readthedocs.yaml | 4 ++ README.md | 12 ++++- docs/conf.py | 82 ++++++++++++++++++++++++++++++ src/example-sidebar.tsx | 20 ++++++-- src/index.ts | 109 +++++++++++++++++++++++++++++++++++++--- 6 files changed, 214 insertions(+), 14 deletions(-) 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/.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 eebba93..84ffd13 100644 --- a/README.md +++ b/README.md @@ -18,9 +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: -It also adds a second right sidebar panel with discovered examples from a local clone of [`jupyterlab/extension-examples`](https://github.com/jupyterlab/extension-examples), so you can open them directly from the panel. +- **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/docs/conf.py b/docs/conf.py index c0c3072..5d01cdd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,6 +21,86 @@ html_css_files = ["custom.css"] +def _ensure_extension_examples(root): + import shutil + import subprocess + + examples = root / "extension-examples" + if (examples / "README.md").exists(): + return examples + + if (root / ".git").exists(): + subprocess.call( + [ + "git", + "submodule", + "update", + "--init", + "--recursive", + "extension-examples", + ], + cwd=str(root), + ) + + if (examples / "README.md").exists(): + return examples + + if examples.exists(): + shutil.rmtree(examples) + + subprocess.check_call( + [ + "git", + "clone", + "--depth", + "1", + "https://github.com/jupyterlab/extension-examples.git", + str(examples), + ], + cwd=str(root), + ) + + return examples + + +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.is_dir(): + continue + 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 +108,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/src/example-sidebar.tsx b/src/example-sidebar.tsx index 6cdec35..6030636 100644 --- a/src/example-sidebar.tsx +++ b/src/example-sidebar.tsx @@ -80,10 +80,22 @@ export class ExampleSidebar extends ReactWidget { {!this._isLoading && !this._errorMessage && filteredExamples.length === 0 ? ( -

      - No extension examples found. Ensure the repository submodule is - initialized. -

      +
      +

      + 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 ? (
        diff --git a/src/index.ts b/src/index.ts index b1eca07..31bab09 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,7 @@ 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'; @@ -48,6 +48,10 @@ import { tokenSidebarIcon, examplesSidebarIcon } from './icons'; 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'; @@ -194,19 +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 = 'jupyterlab/extension-examples plugin entrypoints'; exampleSidebar.title.icon = examplesSidebarIcon; - this.app.shell.add(exampleSidebar, 'right', { rank: 651 }); + + 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(); @@ -297,7 +311,7 @@ class PluginPlayground { } return; } - const plugin = result.plugin; + const plugin = this._ensureDeactivateSupport(result.plugin); if (result.schema) { // TODO: this is mostly fine to get the menus and toolbars, but: @@ -319,10 +333,7 @@ class PluginPlayground { ).emit(plugin.id); } - // Unregister plugin if already registered. - if (this.app.hasPlugin(plugin.id)) { - this.app.deregisterPlugin(plugin.id, true); - } + await this._deactivateAndDeregisterPlugin(plugin.id); this.app.registerPlugin(plugin); if (plugin.autoStart) { try { @@ -337,6 +348,88 @@ class PluginPlayground { } } + 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(); From 34cb2f20f5a873a3ead074a0891944853918b423 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 6 Mar 2026 23:49:55 +0530 Subject: [PATCH 05/23] run prettier --- src/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 31bab09..e8cebd6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -373,7 +373,10 @@ class PluginPlayground { } }; - plugin.deactivate = async (app: JupyterFrontEnd, ...services: unknown[]) => { + plugin.deactivate = async ( + app: JupyterFrontEnd, + ...services: unknown[] + ) => { try { if (originalDeactivate) { await originalDeactivate(app, ...services); @@ -402,7 +405,9 @@ class PluginPlayground { } } - private async _deactivateAndDeregisterPlugin(pluginId: string): Promise { + private async _deactivateAndDeregisterPlugin( + pluginId: string + ): Promise { if (!this.app.hasPlugin(pluginId)) { return; } From 470999d3968d4ca8aea6929064710887961e437e Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sat, 7 Mar 2026 00:07:59 +0530 Subject: [PATCH 06/23] update test --- ui-tests/tests/plugin-playground.spec.ts | 98 +++++++++++++++--------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/ui-tests/tests/plugin-playground.spec.ts b/ui-tests/tests/plugin-playground.spec.ts index 427e7a0..a604ce3 100644 --- a/ui-tests/tests/plugin-playground.spec.ts +++ b/ui-tests/tests/plugin-playground.spec.ts @@ -8,8 +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 EXAMPLE_SIDEBAR_ID = 'jp-plugin-example-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 }); @@ -33,17 +34,16 @@ export default plugin; `; async function openSidebarPanel( - page: IJupyterLabPageFixture, - sidebarId: string + page: IJupyterLabPageFixture ): Promise { - const sidebarTab = page.sidebar.getTabLocator(sidebarId); + const sidebarTab = page.sidebar.getTabLocator(PLAYGROUND_SIDEBAR_ID); await expect(sidebarTab).toBeVisible(); - await page.sidebar.openTab(sidebarId); + await page.sidebar.openTab(PLAYGROUND_SIDEBAR_ID); - const sidebarSide = await page.sidebar.getTabPosition(sidebarId); + 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', sidebarId); + await expect(panel).toHaveAttribute('id', PLAYGROUND_SIDEBAR_ID); return panel; } @@ -96,37 +96,52 @@ test('registers plugin playground commands', async ({ page }) => { ).resolves.toBe(true); }); -test('opens an extension example from the sidebar', async ({ page }) => { - await page.goto(); - const panel = await openSidebarPanel(page, EXAMPLE_SIDEBAR_ID); +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`; - const exampleItems = panel.locator('.jp-PluginPlayground-listItem'); - await expect(exampleItems.first()).toBeVisible(); - expect(await exampleItems.count()).toBeGreaterThan(0); + 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 + ); - const firstExampleName = ( - await panel.locator('.jp-PluginPlayground-entryLabel').first().innerText() - ).trim(); - expect(firstExampleName.length).toBeGreaterThan(0); + await page.goto(); + const panel = await openSidebarPanel(page); + const section = panel.locator(`#${EXAMPLE_SECTION_ID}`); + await expect(section).toBeVisible(); + + 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((exampleName: string) => { + await page.waitForFunction((pathToOpen: string) => { const current = window.jupyterapp.shell .currentWidget as FileEditorWidget | null; const path = current?.context?.path; - if (typeof path !== 'string') { - return false; - } - return ( - path === `extension-examples/${exampleName}/src/index.ts` || - path === `extension-examples/${exampleName}/src/index.js` - ); - }, firstExampleName); + return path === pathToOpen; + }, expectedPath); }); test('loads current editor file as a plugin extension', async ({ @@ -187,30 +202,34 @@ test('opens token sidebar, shows tokens, and filters by exact token', async ({ page }) => { await page.goto(); - const panel = await openSidebarPanel(page, TOKEN_SIDEBAR_ID); + const panel = await openSidebarPanel(page); + const section = panel.locator(`#${TOKEN_SECTION_ID}`); + await expect(section).toBeVisible(); - const tokenListItems = panel.locator('.jp-PluginPlayground-listItem'); + 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-entryLabel').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-entryLabel')).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 openSidebarPanel(page, TOKEN_SIDEBAR_ID); + const panel = await openSidebarPanel(page); + const section = panel.locator(`#${TOKEN_SECTION_ID}`); + await expect(section).toBeVisible(); - const tokenListItem = panel.locator('.jp-PluginPlayground-listItem'); + const tokenListItem = section.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItem.first()).toBeVisible(); const copyButton = tokenListItem @@ -236,11 +255,14 @@ 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 openSidebarPanel(page, TOKEN_SIDEBAR_ID); - const tokenName = await findImportableToken(panel); - const filterInput = panel.getByPlaceholder('Filter token strings'); + const panel = await openSidebarPanel(page); + const section = panel.locator(`#${TOKEN_SECTION_ID}`); + await expect(section).toBeVisible(); + + const tokenName = await findImportableToken(section); + const filterInput = section.getByPlaceholder('Filter token strings'); await filterInput.fill(tokenName); - const tokenListItem = panel.locator('.jp-PluginPlayground-listItem'); + const tokenListItem = section.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItem).toHaveCount(1); const importButton = tokenListItem.locator( From ca04e92dfbe808c95f2366a37424f257e98a89c8 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sat, 7 Mar 2026 14:30:47 +0530 Subject: [PATCH 07/23] Use submodule-only setup for extension-examples in docs build --- docs/conf.py | 54 ++++++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 5d01cdd..ce3d05f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,6 @@ def _ensure_extension_examples(root): - import shutil import subprocess examples = root / "extension-examples" @@ -30,38 +29,35 @@ def _ensure_extension_examples(root): return examples if (root / ".git").exists(): - subprocess.call( - [ - "git", - "submodule", - "update", - "--init", - "--recursive", - "extension-examples", - ], - cwd=str(root), + 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." ) - if (examples / "README.md").exists(): - return examples - - if examples.exists(): - shutil.rmtree(examples) - - subprocess.check_call( - [ - "git", - "clone", - "--depth", - "1", - "https://github.com/jupyterlab/extension-examples.git", - str(examples), - ], - cwd=str(root), + raise RuntimeError( + "Missing 'extension-examples'. Build from a git checkout with submodules " + "initialized." ) - return examples - def _sync_examples_to_lite_contents(root): import shutil From 6513b5df2b3909fddccf4433aee679bd60452f23 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sat, 7 Mar 2026 14:36:34 +0530 Subject: [PATCH 08/23] remove unused icon --- src/icons.ts | 6 ------ src/index.ts | 3 +-- style/icons/examples-sidebar.svg | 34 -------------------------------- 3 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 style/icons/examples-sidebar.svg diff --git a/src/icons.ts b/src/icons.ts index 40ceb79..b81d92f 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -1,14 +1,8 @@ import { LabIcon } from '@jupyterlab/ui-components'; import tokenSidebarIconSvgstr from '!!raw-loader!../style/icons/token-sidebar.svg'; -import examplesSidebarIconSvgstr from '!!raw-loader!../style/icons/examples-sidebar.svg'; export const tokenSidebarIcon = new LabIcon({ name: 'plugin-playground:token-sidebar', svgstr: tokenSidebarIconSvgstr }); - -export const examplesSidebarIcon = new LabIcon({ - name: 'plugin-playground:examples-sidebar', - svgstr: examplesSidebarIconSvgstr -}); diff --git a/src/index.ts b/src/index.ts index e8cebd6..be4af55 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,7 +44,7 @@ import { TokenSidebar } from './token-sidebar'; import { ExampleSidebar } from './example-sidebar'; -import { tokenSidebarIcon, examplesSidebarIcon } from './icons'; +import { tokenSidebarIcon } from './icons'; import { Token } from '@lumino/coreutils'; @@ -210,7 +210,6 @@ class PluginPlayground { exampleSidebar.title.label = 'Extension Examples'; exampleSidebar.title.caption = 'jupyterlab/extension-examples plugin entrypoints'; - exampleSidebar.title.icon = examplesSidebarIcon; const playgroundSidebar = new SidePanel(); playgroundSidebar.id = 'jp-plugin-playground-sidebar'; diff --git a/style/icons/examples-sidebar.svg b/style/icons/examples-sidebar.svg deleted file mode 100644 index cfbe036..0000000 --- a/style/icons/examples-sidebar.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From c0b4a686e5dade6de167888eef8eec2fad899578 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sat, 7 Mar 2026 21:00:02 +0530 Subject: [PATCH 09/23] Fix extension example descriptions on Lite content API --- src/example-sidebar.tsx | 2 +- src/index.ts | 105 ++++++++++++++++++++++++++-------------- 2 files changed, 71 insertions(+), 36 deletions(-) diff --git a/src/example-sidebar.tsx b/src/example-sidebar.tsx index 6030636..4108054 100644 --- a/src/example-sidebar.tsx +++ b/src/example-sidebar.tsx @@ -69,7 +69,7 @@ export class ExampleSidebar extends ReactWidget {

        {this._isLoading ? (

        - Loading extension examples... + Loading extension examples…

        ) : null} {this._errorMessage ? ( diff --git a/src/index.ts b/src/index.ts index be4af55..0c77139 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,9 +106,9 @@ type IDirectoryModel = Contents.IModel & { content: Contents.IModel[]; }; -type ITextFileModel = Contents.IModel & { +type IFileModel = Contents.IModel & { type: 'file'; - content: string; + content: unknown; }; const EXTENSION_EXAMPLES_ROOT = 'extension-examples'; @@ -486,17 +486,23 @@ class PluginPlayground { private async _getDirectoryModel( path: string ): Promise { - try { - const model = await this.app.serviceManager.contents.get(path, { - content: true - }); - if (model.type !== 'directory' || !Array.isArray(model.content)) { - return null; + for (const candidatePath of this._pathCandidates(path)) { + try { + const model = await this.app.serviceManager.contents.get( + candidatePath, + { + content: true + } + ); + if (model.type !== 'directory' || !Array.isArray(model.content)) { + continue; + } + return model as IDirectoryModel; + } catch { + continue; } - return model as IDirectoryModel; - } catch { - return null; } + return null; } private async _findExampleEntrypoint( @@ -523,23 +529,36 @@ class PluginPlayground { directoryPath: string ): Promise { const packageJsonPath = this._joinPath(directoryPath, 'package.json'); - const packageJson = await this._getTextFileModel(packageJsonPath); + const packageJson = await this._getFileModel(packageJsonPath); if (!packageJson) { return this._fallbackExampleDescription; } - try { - const packageData = JSON.parse(packageJson.content) as { - description?: unknown; - }; - if (typeof packageData.description === 'string') { - const description = packageData.description.trim(); - if (description.length > 0) { - return description; + + let packageData: { description?: unknown } | null = null; + if ( + packageJson.content !== null && + typeof packageJson.content === 'object' && + !Array.isArray(packageJson.content) + ) { + packageData = packageJson.content as { description?: unknown }; + } else if (typeof packageJson.content === 'string') { + try { + const parsed = JSON.parse(packageJson.content) as unknown; + if (parsed !== null && typeof parsed === 'object') { + packageData = parsed as { description?: unknown }; } + } catch { + return this._fallbackExampleDescription; + } + } + + if (packageData && typeof packageData.description === 'string') { + const description = packageData.description.trim(); + if (description.length > 0) { + return description; } - } catch { - // fall back to a default description } + return this._fallbackExampleDescription; } @@ -552,21 +571,37 @@ class PluginPlayground { return `${normalizedBase}/${normalizedChild}`; } - private async _getTextFileModel( - path: string - ): Promise { - try { - const model = await this.app.serviceManager.contents.get(path, { - content: true, - format: 'text' - }); - if (model.type !== 'file' || typeof model.content !== 'string') { - return null; + private async _getFileModel(path: string): Promise { + for (const candidatePath of this._pathCandidates(path)) { + try { + const model = await this.app.serviceManager.contents.get( + candidatePath, + { + content: true + } + ); + if (model.type !== 'file') { + continue; + } + return model as IFileModel; + } catch { + continue; } - return model as ITextFileModel; - } catch { - return null; } + return null; + } + + private _pathCandidates(path: string): string[] { + const trimmed = path.replace(/^\/+/g, ''); + const candidates = new Set(); + if (path.length > 0) { + candidates.add(path); + } + if (trimmed.length > 0) { + candidates.add(trimmed); + candidates.add(`/${trimmed}`); + } + return Array.from(candidates); } private _populateTokenMap(): void { From de95536517890467d9ce254925e70d16211df271 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sat, 7 Mar 2026 21:08:16 +0530 Subject: [PATCH 10/23] try fix --- src/index.ts | 103 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0c77139..deff5ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,6 +109,7 @@ type IDirectoryModel = Contents.IModel & { type IFileModel = Contents.IModel & { type: 'file'; content: unknown; + format?: string | null; }; const EXTENSION_EXAMPLES_ROOT = 'extension-examples'; @@ -462,10 +463,10 @@ class PluginPlayground { if (item.type !== 'directory' || item.name.startsWith('.')) { continue; } - const exampleDirectory = this._joinPath( - EXTENSION_EXAMPLES_ROOT, - item.name - ); + const exampleDirectory = + typeof item.path === 'string' && item.path.length > 0 + ? item.path + : this._joinPath(EXTENSION_EXAMPLES_ROOT, item.name); const entrypoint = await this._findExampleEntrypoint(exampleDirectory); if (!entrypoint) { continue; @@ -533,24 +534,7 @@ class PluginPlayground { if (!packageJson) { return this._fallbackExampleDescription; } - - let packageData: { description?: unknown } | null = null; - if ( - packageJson.content !== null && - typeof packageJson.content === 'object' && - !Array.isArray(packageJson.content) - ) { - packageData = packageJson.content as { description?: unknown }; - } else if (typeof packageJson.content === 'string') { - try { - const parsed = JSON.parse(packageJson.content) as unknown; - if (parsed !== null && typeof parsed === 'object') { - packageData = parsed as { description?: unknown }; - } - } catch { - return this._fallbackExampleDescription; - } - } + const packageData = this._parseJsonObject(packageJson); if (packageData && typeof packageData.description === 'string') { const description = packageData.description.trim(); @@ -573,21 +557,78 @@ class PluginPlayground { private async _getFileModel(path: string): Promise { for (const candidatePath of this._pathCandidates(path)) { - try { - const model = await this.app.serviceManager.contents.get( - candidatePath, - { - content: true + for (const format of ['text', 'json', null] as const) { + try { + const model = await this.app.serviceManager.contents.get( + candidatePath, + format + ? { + content: true, + format + } + : { + content: true + } + ); + if (model.type !== 'file') { + continue; } - ); - if (model.type !== 'file') { + if (model.content === null) { + continue; + } + return model as IFileModel; + } catch { continue; } - return model as IFileModel; + } + } + return null; + } + + private _parseJsonObject( + fileModel: IFileModel + ): { description?: unknown } | null { + if ( + fileModel.content !== null && + typeof fileModel.content === 'object' && + !Array.isArray(fileModel.content) + ) { + return fileModel.content as { description?: unknown }; + } + if (typeof fileModel.content !== 'string') { + return null; + } + + const tryParse = (raw: string): { description?: unknown } | null => { + try { + const parsed = JSON.parse(raw) as unknown; + if ( + parsed !== null && + typeof parsed === 'object' && + !Array.isArray(parsed) + ) { + return parsed as { description?: unknown }; + } } catch { - continue; + return null; } + return null; + }; + + const parsedText = tryParse(fileModel.content); + if (parsedText) { + return parsedText; } + + if (fileModel.format === 'base64') { + try { + const decoded = atob(fileModel.content); + return tryParse(decoded); + } catch { + return null; + } + } + return null; } From 0745ec1b0d83f1eb9f7ad45b86cfe96652abf583 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sun, 8 Mar 2026 00:15:42 +0530 Subject: [PATCH 11/23] Improve path compatibility for example discovery --- src/index.ts | 107 +++++++++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 46 deletions(-) diff --git a/src/index.ts b/src/index.ts index deff5ef..dfa1dec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -443,7 +443,7 @@ class PluginPlayground { private async _openExtensionExample(examplePath: string): Promise { await this.app.commands.execute('docmanager:open', { - path: examplePath, + path: this._normalizeContentsPath(examplePath), factory: 'Editor' }); } @@ -457,16 +457,16 @@ class PluginPlayground { if (!rootDirectory) { return []; } + const rootPath = this._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 = - typeof item.path === 'string' && item.path.length > 0 - ? item.path - : this._joinPath(EXTENSION_EXAMPLES_ROOT, item.name); + const exampleDirectory = this._joinPath(rootPath, item.name); const entrypoint = await this._findExampleEntrypoint(exampleDirectory); if (!entrypoint) { continue; @@ -488,20 +488,17 @@ class PluginPlayground { path: string ): Promise { for (const candidatePath of this._pathCandidates(path)) { - try { - const model = await this.app.serviceManager.contents.get( - candidatePath, - { - content: true - } - ); - if (model.type !== 'directory' || !Array.isArray(model.content)) { - continue; - } - return model as IDirectoryModel; - } catch { + const model = await this._getContentsModel(candidatePath, { + content: true + }); + if ( + !model || + model.type !== 'directory' || + !Array.isArray(model.content) + ) { continue; } + return model as IDirectoryModel; } return null; } @@ -523,7 +520,9 @@ class PluginPlayground { if (!entrypoint) { return null; } - return this._joinPath(srcDirectory.path, entrypoint.name); + return this._normalizeContentsPath( + this._joinPath(srcDirectory.path, entrypoint.name) + ); } private async _readExampleDescription( @@ -536,9 +535,9 @@ class PluginPlayground { } const packageData = this._parseJsonObject(packageJson); - if (packageData && typeof packageData.description === 'string') { - const description = packageData.description.trim(); - if (description.length > 0) { + if (packageData) { + const description = this._stringValue(packageData.description); + if (description) { return description; } } @@ -548,7 +547,7 @@ class PluginPlayground { private _joinPath(base: string, child: string): string { const normalizedBase = base.replace(/\/+$/g, ''); - const normalizedChild = child.replace(/^\/+/g, ''); + const normalizedChild = this._normalizeContentsPath(child); if (!normalizedBase) { return normalizedChild; } @@ -557,29 +556,27 @@ class PluginPlayground { private async _getFileModel(path: string): Promise { for (const candidatePath of this._pathCandidates(path)) { - for (const format of ['text', 'json', null] as const) { - try { - const model = await this.app.serviceManager.contents.get( - candidatePath, - format - ? { - content: true, - format - } - : { - content: true - } - ); - if (model.type !== 'file') { - continue; - } - if (model.content === null) { - continue; - } - return model as IFileModel; - } catch { - continue; - } + const model = await this._getContentsModel(candidatePath, { + content: true + }); + if (!model || model.type !== 'file') { + continue; + } + if (model.content !== null) { + return model as IFileModel; + } + + // Some jupyterlite setups need an explicit text request for file content. + const textModel = await this._getContentsModel(candidatePath, { + content: true, + format: 'text' + }); + if ( + textModel && + textModel.type === 'file' && + textModel.content !== null + ) { + return textModel as IFileModel; } } return null; @@ -633,7 +630,10 @@ class PluginPlayground { } private _pathCandidates(path: string): string[] { - const trimmed = path.replace(/^\/+/g, ''); + // Jupyter Server vs JupyterLite can expose contents paths with different + // leading-slash conventions; try both forms to keep example discovery and + // file reads working in both environments. + const trimmed = this._normalizeContentsPath(path); const candidates = new Set(); if (path.length > 0) { candidates.add(path); @@ -645,6 +645,21 @@ class PluginPlayground { return Array.from(candidates); } + private _normalizeContentsPath(path: string): string { + return path.replace(/^\/+/g, ''); + } + + private async _getContentsModel( + path: string, + options: Contents.IFetchOptions + ): Promise { + try { + return await this.app.serviceManager.contents.get(path, options); + } catch { + return null; + } + } + private _populateTokenMap(): void { const app = this.app as unknown as IPrivateServiceStore; this._tokenMap.clear(); From 729ea3010f89fcc1b1b467bd8a8670356036e570 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sun, 8 Mar 2026 03:07:48 +0530 Subject: [PATCH 12/23] add suggestions --- docs/conf.py | 2 -- src/example-sidebar.tsx | 22 +++++++++++----------- src/index.ts | 23 +++++++++-------------- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ce3d05f..bc7fed6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -82,8 +82,6 @@ def _sync_examples_to_lite_contents(root): continue src_dir = example_dir / "src" - if not src_dir.is_dir(): - continue if not ((src_dir / "index.ts").exists() or (src_dir / "index.js").exists()): continue diff --git a/src/example-sidebar.tsx b/src/example-sidebar.tsx index 4108054..4968216 100644 --- a/src/example-sidebar.tsx +++ b/src/example-sidebar.tsx @@ -18,17 +18,6 @@ export namespace ExampleSidebar { } export class ExampleSidebar extends ReactWidget { - private readonly _fetchExamples: () => Promise< - ReadonlyArray - >; - private readonly _onOpenExample: ( - examplePath: string - ) => Promise | void; - private _query = ''; - private _examples: ReadonlyArray = []; - private _isLoading = false; - private _errorMessage = ''; - constructor(options: ExampleSidebar.IOptions) { super(); this._fetchExamples = options.fetchExamples; @@ -169,4 +158,15 @@ export class ExampleSidebar extends ReactWidget { }); } } + + 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 dfa1dec..a91707f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -210,7 +210,7 @@ class PluginPlayground { exampleSidebar.id = 'jp-plugin-example-sidebar'; exampleSidebar.title.label = 'Extension Examples'; exampleSidebar.title.caption = - 'jupyterlab/extension-examples plugin entrypoints'; + 'Browse plugin examples from jupyterlab/extension-examples'; const playgroundSidebar = new SidePanel(); playgroundSidebar.id = 'jp-plugin-playground-sidebar'; @@ -457,9 +457,9 @@ class PluginPlayground { if (!rootDirectory) { return []; } - const rootPath = this._normalizeContentsPath( - rootDirectory.path || EXTENSION_EXAMPLES_ROOT - ); + const rootPath = + this._normalizeContentsPath(rootDirectory.path) || + EXTENSION_EXAMPLES_ROOT; const discovered: ExampleSidebar.IExampleRecord[] = []; for (const item of rootDirectory.content) { @@ -634,19 +634,14 @@ class PluginPlayground { // leading-slash conventions; try both forms to keep example discovery and // file reads working in both environments. const trimmed = this._normalizeContentsPath(path); - const candidates = new Set(); - if (path.length > 0) { - candidates.add(path); - } - if (trimmed.length > 0) { - candidates.add(trimmed); - candidates.add(`/${trimmed}`); + if (trimmed.length === 0) { + return []; } - return Array.from(candidates); + return [trimmed, `/${trimmed}`]; } - private _normalizeContentsPath(path: string): string { - return path.replace(/^\/+/g, ''); + private _normalizeContentsPath(path: string | null | undefined): string { + return (path ?? '').replace(/^\/+/g, ''); } private async _getContentsModel( From e97157ad0abdcdf7d5b4ab4768a4bd1edf99971b Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sun, 8 Mar 2026 03:17:59 +0530 Subject: [PATCH 13/23] remove default expansion --- src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index a91707f..19122c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,8 +48,6 @@ import { tokenSidebarIcon } from './icons'; import { Token } from '@lumino/coreutils'; -import { AccordionPanel } from '@lumino/widgets'; - import { IPlugin } from '@lumino/application'; namespace CommandIDs { @@ -218,8 +216,6 @@ class PluginPlayground { 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(() => { From 717d7c8e5bef9c03d1c5b536f38b30ab3d35632a Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sun, 8 Mar 2026 03:27:42 +0530 Subject: [PATCH 14/23] use collapse --- src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.ts b/src/index.ts index 19122c2..4e45c33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,8 @@ import { tokenSidebarIcon } from './icons'; import { Token } from '@lumino/coreutils'; +import { AccordionPanel } from '@lumino/widgets'; + import { IPlugin } from '@lumino/application'; namespace CommandIDs { @@ -216,6 +218,9 @@ class PluginPlayground { playgroundSidebar.title.icon = tokenSidebarIcon; playgroundSidebar.addWidget(tokenSidebar); playgroundSidebar.addWidget(exampleSidebar); + const accordion = playgroundSidebar.content as AccordionPanel; + accordion.collapse(0); + accordion.collapse(1); this.app.shell.add(playgroundSidebar, 'right', { rank: 650 }); app.shell.currentChanged?.connect(() => { From c93c74350894561ead84166e8f0b2296295b57fe Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sun, 8 Mar 2026 03:44:22 +0530 Subject: [PATCH 15/23] update tests --- ui-tests/tests/plugin-playground.spec.ts | 58 ++++++++++++++++++------ 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/ui-tests/tests/plugin-playground.spec.ts b/ui-tests/tests/plugin-playground.spec.ts index a604ce3..5ef69bd 100644 --- a/ui-tests/tests/plugin-playground.spec.ts +++ b/ui-tests/tests/plugin-playground.spec.ts @@ -11,6 +11,8 @@ const TEST_FILE = 'playground-integration-test.ts'; const PLAYGROUND_SIDEBAR_ID = 'jp-plugin-playground-sidebar'; const TOKEN_SECTION_ID = 'jp-plugin-token-sidebar'; const EXAMPLE_SECTION_ID = 'jp-plugin-example-sidebar'; +const TOKEN_SECTION_LABEL = 'Service Tokens'; +const EXAMPLE_SECTION_LABEL = 'Extension Examples'; test.use({ autoGoto: false }); @@ -34,7 +36,9 @@ export default plugin; `; async function openSidebarPanel( - page: IJupyterLabPageFixture + page: IJupyterLabPageFixture, + sectionId?: string, + sectionLabel?: string ): Promise { const sidebarTab = page.sidebar.getTabLocator(PLAYGROUND_SIDEBAR_ID); await expect(sidebarTab).toBeVisible(); @@ -44,7 +48,25 @@ async function openSidebarPanel( const panel = page.sidebar.getContentPanelLocator(sidebarSide ?? 'right'); await expect(panel).toBeVisible(); await expect(panel).toHaveAttribute('id', PLAYGROUND_SIDEBAR_ID); - return panel; + if (!sectionId) { + return panel; + } + + const section = panel.locator(`#${sectionId}`); + if (!(await section.isVisible())) { + if (!sectionLabel) { + throw new Error( + `Missing section label for sidebar section "${sectionId}"` + ); + } + const sectionTitle = panel + .locator('.lm-AccordionPanel-title') + .filter({ hasText: sectionLabel }); + await expect(sectionTitle).toBeVisible(); + await sectionTitle.click(); + } + await expect(section).toBeVisible(); + return section; } async function findImportableToken(panel: Locator): Promise { @@ -120,9 +142,11 @@ test('opens a dummy extension example from the sidebar', async ({ page }) => { ); await page.goto(); - const panel = await openSidebarPanel(page); - const section = panel.locator(`#${EXAMPLE_SECTION_ID}`); - await expect(section).toBeVisible(); + const section = await openSidebarPanel( + page, + EXAMPLE_SECTION_ID, + EXAMPLE_SECTION_LABEL + ); const filterInput = section.getByPlaceholder('Filter extension examples'); await expect(filterInput).toBeVisible(); @@ -202,9 +226,11 @@ test('opens token sidebar, shows tokens, and filters by exact token', async ({ page }) => { await page.goto(); - const panel = await openSidebarPanel(page); - const section = panel.locator(`#${TOKEN_SECTION_ID}`); - await expect(section).toBeVisible(); + const section = await openSidebarPanel( + page, + TOKEN_SECTION_ID, + TOKEN_SECTION_LABEL + ); const tokenListItems = section.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItems.first()).toBeVisible(); @@ -225,9 +251,11 @@ test('opens token sidebar, shows tokens, and filters by exact token', async ({ test('token sidebar copy button shows copied state', async ({ page }) => { await page.goto(); - const panel = await openSidebarPanel(page); - const section = panel.locator(`#${TOKEN_SECTION_ID}`); - await expect(section).toBeVisible(); + const section = await openSidebarPanel( + page, + TOKEN_SECTION_ID, + TOKEN_SECTION_LABEL + ); const tokenListItem = section.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItem.first()).toBeVisible(); @@ -255,9 +283,11 @@ 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 openSidebarPanel(page); - const section = panel.locator(`#${TOKEN_SECTION_ID}`); - await expect(section).toBeVisible(); + const section = await openSidebarPanel( + page, + TOKEN_SECTION_ID, + TOKEN_SECTION_LABEL + ); const tokenName = await findImportableToken(section); const filterInput = section.getByPlaceholder('Filter token strings'); From 49b2d44fc38b74b03364f67b65f7fd075b993730 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 13 Mar 2026 16:39:48 +0530 Subject: [PATCH 16/23] fix example import and schema resolution + add submodule to ignore list (eslint,prettier) --- scripts/check-extension-examples.js | 228 ++++++++++++++++++++++++++++ src/contents.ts | 115 ++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 scripts/check-extension-examples.js create mode 100644 src/contents.ts 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/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); +} From 910f1c0fc296075950bfc9dcb887e954e197b384 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 13 Mar 2026 16:41:14 +0530 Subject: [PATCH 17/23] fix example import and schema resolution + add submodule to ignore list (eslint,prettier) --- .github/workflows/build.yml | 7 ++ .prettierignore | 3 +- eslint.config.js | 1 + package.json | 1 + scripts/modules.py | 8 +- src/index.ts | 162 ++++++++---------------------------- src/loader.ts | 11 +-- src/modules.ts | 6 ++ src/resolver.ts | 63 +++++++++----- 9 files changed, 103 insertions(+), 159 deletions(-) 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/.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/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/package.json b/package.json index ead1546..ef239c2 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", 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/index.ts b/src/index.ts index 4e45c33..f0de5f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,6 +46,14 @@ 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'; @@ -101,17 +109,6 @@ interface IPrivatePluginData { }; } -type IDirectoryModel = Contents.IModel & { - type: 'directory'; - content: Contents.IModel[]; -}; - -type IFileModel = Contents.IModel & { - type: 'file'; - content: unknown; - format?: string | null; -}; - const EXTENSION_EXAMPLES_ROOT = 'extension-examples'; class PluginPlayground { @@ -132,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 && @@ -154,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', { @@ -444,7 +443,7 @@ class PluginPlayground { private async _openExtensionExample(examplePath: string): Promise { await this.app.commands.execute('docmanager:open', { - path: this._normalizeContentsPath(examplePath), + path: normalizeContentsPath(examplePath), factory: 'Editor' }); } @@ -452,15 +451,15 @@ class PluginPlayground { private async _discoverExtensionExamples(): Promise< ReadonlyArray > { - const rootDirectory = await this._getDirectoryModel( + const rootDirectory = await getDirectoryModel( + this.app.serviceManager, EXTENSION_EXAMPLES_ROOT ); if (!rootDirectory) { return []; } const rootPath = - this._normalizeContentsPath(rootDirectory.path) || - EXTENSION_EXAMPLES_ROOT; + normalizeContentsPath(rootDirectory.path) || EXTENSION_EXAMPLES_ROOT; const discovered: ExampleSidebar.IExampleRecord[] = []; for (const item of rootDirectory.content) { @@ -485,29 +484,11 @@ class PluginPlayground { ); } - private async _getDirectoryModel( - path: string - ): Promise { - for (const candidatePath of this._pathCandidates(path)) { - const model = await this._getContentsModel(candidatePath, { - content: true - }); - if ( - !model || - model.type !== 'directory' || - !Array.isArray(model.content) - ) { - continue; - } - return model as IDirectoryModel; - } - return null; - } - private async _findExampleEntrypoint( directoryPath: string ): Promise { - const srcDirectory = await this._getDirectoryModel( + const srcDirectory = await getDirectoryModel( + this.app.serviceManager, this._joinPath(directoryPath, 'src') ); if (!srcDirectory) { @@ -521,7 +502,7 @@ class PluginPlayground { if (!entrypoint) { return null; } - return this._normalizeContentsPath( + return normalizeContentsPath( this._joinPath(srcDirectory.path, entrypoint.name) ); } @@ -530,7 +511,10 @@ class PluginPlayground { directoryPath: string ): Promise { const packageJsonPath = this._joinPath(directoryPath, 'package.json'); - const packageJson = await this._getFileModel(packageJsonPath); + const packageJson = await getFileModel( + this.app.serviceManager, + packageJsonPath + ); if (!packageJson) { return this._fallbackExampleDescription; } @@ -548,112 +532,34 @@ class PluginPlayground { private _joinPath(base: string, child: string): string { const normalizedBase = base.replace(/\/+$/g, ''); - const normalizedChild = this._normalizeContentsPath(child); + const normalizedChild = normalizeContentsPath(child); if (!normalizedBase) { return normalizedChild; } return `${normalizedBase}/${normalizedChild}`; } - private async _getFileModel(path: string): Promise { - for (const candidatePath of this._pathCandidates(path)) { - const model = await this._getContentsModel(candidatePath, { - content: true - }); - if (!model || model.type !== 'file') { - continue; - } - if (model.content !== null) { - return model as IFileModel; - } - - // Some jupyterlite setups need an explicit text request for file content. - const textModel = await this._getContentsModel(candidatePath, { - content: true, - format: 'text' - }); - if ( - textModel && - textModel.type === 'file' && - textModel.content !== null - ) { - return textModel as IFileModel; - } - } - return null; - } - private _parseJsonObject( fileModel: IFileModel ): { description?: unknown } | null { - if ( - fileModel.content !== null && - typeof fileModel.content === 'object' && - !Array.isArray(fileModel.content) - ) { - return fileModel.content as { description?: unknown }; - } - if (typeof fileModel.content !== 'string') { - return null; - } - - const tryParse = (raw: string): { description?: unknown } | 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; - } + const raw = fileModelToText(fileModel); + if (raw === null) { return null; - }; - - const parsedText = tryParse(fileModel.content); - if (parsedText) { - return parsedText; - } - - if (fileModel.format === 'base64') { - try { - const decoded = atob(fileModel.content); - return tryParse(decoded); - } catch { - return null; - } } - return null; - } - - private _pathCandidates(path: string): string[] { - // Jupyter Server vs JupyterLite can expose contents paths with different - // leading-slash conventions; try both forms to keep example discovery and - // file reads working in both environments. - const trimmed = this._normalizeContentsPath(path); - if (trimmed.length === 0) { - return []; - } - return [trimmed, `/${trimmed}`]; - } - - private _normalizeContentsPath(path: string | null | undefined): string { - return (path ?? '').replace(/^\/+/g, ''); - } - - private async _getContentsModel( - path: string, - options: Contents.IFetchOptions - ): Promise { try { - return await this.app.serviceManager.contents.get(path, options); + 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 { @@ -839,6 +745,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..c94d045 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -11,6 +11,8 @@ import { IRequireJS } from './requirejs'; import { IModule, IModuleMember } from './types'; +import { readContentsFileAsText } from './contents'; + export namespace PluginLoader { export interface IOptions { transpiler: PluginTranspiler; @@ -72,13 +74,12 @@ export class PluginLoader { ]; for (const path of candidatePaths) { console.log(`Looking for schema in ${path}`); - try { - const file = await serviceManager.contents.get(path); + const schema = await readContentsFileAsText(serviceManager, path); + if (schema !== null) { console.log(`Found schema in ${path}`); - return file.content; - } catch (e) { - console.log(`Did not find schema in ${path}`); + return schema; } + console.log(`Did not find schema in ${path}`); } return null; } 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); + } } From 2b3e8047beb4660370fa86d4f7a68c91b63c429a Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 13 Mar 2026 16:49:15 +0530 Subject: [PATCH 18/23] add new devdependencies --- package.json | 5 +- yarn.lock | 308 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 311 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ef239c2..a2f5f3b 100644 --- a/package.json +++ b/package.json @@ -66,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", @@ -98,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", @@ -109,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/yarn.lock b/yarn.lock index 8bb27d8..18073f8 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" @@ -725,6 +744,34 @@ __metadata: languageName: node linkType: hard +"@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.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 + "@lumino/coreutils": ^2.2.2 + "@lumino/disposable": ^2.1.5 + "@lumino/messaging": ^2.0.4 + "@lumino/polling": ^2.1.5 + "@lumino/properties": ^2.0.4 + "@lumino/signaling": ^2.1.5 + "@lumino/widgets": ^2.7.5 + 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" @@ -754,6 +801,35 @@ __metadata: languageName: node linkType: hard +"@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 + "@lumino/disposable": ^2.1.5 + "@lumino/domutils": ^2.0.4 + "@lumino/messaging": ^2.0.4 + "@lumino/signaling": ^2.1.5 + "@lumino/virtualdom": ^2.0.4 + "@lumino/widgets": ^2.7.5 + "@types/react": ^18.0.26 + react: ^18.2.0 + sanitize-html: ~2.12.1 + checksum: 5c72ae37b8f4671786b7ca94a019fc8bdf743afe76f33bc471767fa54b861bda564b50dd2b2eae5b57ee862b169e0155f190f76e4bdcc7d33a1cd0ee2842bed3 + languageName: node + linkType: hard + "@jupyterlab/attachments@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/attachments@npm:4.5.5" @@ -890,6 +966,30 @@ __metadata: languageName: node linkType: hard +"@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.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 + "@lumino/messaging": ^2.0.4 + "@lumino/signaling": ^2.1.5 + "@lumino/widgets": ^2.7.5 + react: ^18.2.0 + checksum: 298cab335372ba900dcf1f811f31272f2fb214b42411eca44b39c66c1910b865a223015642b3f8ddc0a24e1a5eba7b424f771ded735f74e602d21491ad3c945f + languageName: node + linkType: hard + "@jupyterlab/codemirror@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/codemirror@npm:4.5.5" @@ -1000,6 +1100,20 @@ __metadata: languageName: node linkType: hard +"@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 + "@lumino/signaling": ^2.1.5 + minimist: ~1.2.0 + path-browserify: ^1.0.0 + url-parse: ~1.5.4 + checksum: e693f47d86dcff9762340d8a7df504b9b258a03785243b8aac8606321216efac9c965bbf4197abbcb1f175dade44d0f53f0a9ff70f14a16e5243fae3a5ef16cf + languageName: node + linkType: hard + "@jupyterlab/csvviewer@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/csvviewer@npm:4.5.5" @@ -1109,6 +1223,32 @@ __metadata: languageName: node linkType: hard +"@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.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 + "@lumino/messaging": ^2.0.4 + "@lumino/properties": ^2.0.4 + "@lumino/signaling": ^2.1.5 + "@lumino/widgets": ^2.7.5 + react: ^18.2.0 + checksum: e57126380d3abca32165c659fc3dd63c4642abc2dfe35017c38fd4ca91ae9cfcb95427c98fd47aa0f546c8030eb1af42be60c2a40bc03f4411fa9f92b77a4fe3 + languageName: node + linkType: hard + "@jupyterlab/documentsearch@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/documentsearch@npm:4.5.5" @@ -1424,6 +1564,15 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/nbformat@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/nbformat@npm:4.5.6" + dependencies: + "@lumino/coreutils": ^2.2.2 + checksum: 136f00cf215a7a4a9061d8874ac35191f5f65c62cdb7687578d77772d8d6fb866905710511986ef533a383826d1cc2807cfc58e5f7076b853703cae15b3c8bc8 + languageName: node + linkType: hard + "@jupyterlab/notebook@npm:^4.5.0, @jupyterlab/notebook@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/notebook@npm:4.5.5" @@ -1476,6 +1625,19 @@ __metadata: languageName: node linkType: hard +"@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: 1c1e04644929f2f8535126244c71bc916e1759b256d6442cfeef2fb68ebeaa872aa4cabc7dadce1c14ab419a742be9ef30b6e940b09d559705d47858a55b4174 + languageName: node + linkType: hard + "@jupyterlab/outputarea@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/outputarea@npm:4.5.5" @@ -1503,6 +1665,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 +1703,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 +1718,7 @@ __metadata: requirejs: ^2.3.6 rimraf: ^3.0.2 typescript: ~5.5.4 + yjs: ^13.5.40 languageName: unknown linkType: soft @@ -1601,6 +1766,16 @@ __metadata: languageName: node linkType: hard +"@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: 23bbf8f18712d6f14444ef20ed851fa8d787f4cdfaf4e43348365a3d6042ff1fe43e738b99a0e6a921d8205f7b97cc8d4016eb0863950266e105699fcd13a8d6 + languageName: node + linkType: hard + "@jupyterlab/rendermime@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/rendermime@npm:4.5.5" @@ -1621,6 +1796,26 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/rendermime@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/rendermime@npm:4.5.6" + dependencies: + "@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: 8c944ea0358cdabc5f8c7a4b3df1f8da48898dd0c3f3da0afe97bb36155b3172ce8061c87cdf98f71970893b05199d7e420eefdd85c7c89fd355639a493370c0 + languageName: node + linkType: hard + "@jupyterlab/running@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/running@npm:4.5.5" @@ -1660,6 +1855,25 @@ __metadata: languageName: node linkType: hard +"@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.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: c5b0d9ba7f2ccc245eb7bbf6f32c174116ff70f7d173168b66649b6d35cbca118e2e1be6fa7f305979eac3c8478dc228990b1c5537aec96b51cd637b2ab792a3 + languageName: node + linkType: hard + "@jupyterlab/settingeditor@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/settingeditor@npm:4.5.5" @@ -1709,6 +1923,25 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/settingregistry@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/settingregistry@npm:4.5.6" + dependencies: + "@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 + "@lumino/signaling": ^2.1.5 + "@rjsf/utils": ^5.13.4 + ajv: ^8.12.0 + json5: ^2.2.3 + peerDependencies: + react: ">=16" + checksum: fd8490e0a2e7a8359fc6c87e1988bda6ce168be202fba3888b77a9bbd4b0967a2486d39c73464f2295a17615cc715ab48733e6fa4d52b4565bb003e3d586c99b + languageName: node + linkType: hard + "@jupyterlab/statedb@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/statedb@npm:4.5.5" @@ -1722,6 +1955,19 @@ __metadata: languageName: node linkType: hard +"@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: 5a9a237ed1cd215ddb672bc080da5966320716b7ea6a018eeca2c5cc3689b0b9709843e1ef2452657976b751ce00cf64c82375f3dc796d844f94dcf856c50884 + languageName: node + linkType: hard + "@jupyterlab/statusbar@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/statusbar@npm:4.5.5" @@ -1738,6 +1984,22 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statusbar@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/statusbar@npm:4.5.6" + dependencies: + "@jupyterlab/ui-components": ^4.5.6 + "@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 + "@lumino/widgets": ^2.7.5 + react: ^18.2.0 + checksum: 80a5bc008827f32f8fb99e913262eaeba27e5ec92ca8e194d856e071efa1f62189a369b2c9276808377c7740ea27bbd5f8430b51e3c7a9ea1d8017f166594425 + languageName: node + linkType: hard + "@jupyterlab/terminal@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/terminal@npm:4.5.5" @@ -1812,6 +2074,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/translation@npm:^4.5.6": + version: 4.5.6 + resolution: "@jupyterlab/translation@npm:4.5.6" + dependencies: + "@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: 05db816acc6b908a02c80ff67c2239a54f1b43260b57ffc0038224ec5d082ed6167d27d4e3cccfd871f7408c10e6c36c8ab9fcd419b69e7a9ded4307bf3e16ad + languageName: node + linkType: hard + "@jupyterlab/ui-components@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/ui-components@npm:4.5.5" @@ -1843,6 +2118,37 @@ __metadata: languageName: node linkType: hard +"@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.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 + "@lumino/disposable": ^2.1.5 + "@lumino/messaging": ^2.0.4 + "@lumino/polling": ^2.1.5 + "@lumino/properties": ^2.0.4 + "@lumino/signaling": ^2.1.5 + "@lumino/virtualdom": ^2.0.4 + "@lumino/widgets": ^2.7.5 + "@rjsf/core": ^5.13.4 + "@rjsf/utils": ^5.13.4 + react: ^18.2.0 + react-dom: ^18.2.0 + typestyle: ^2.0.4 + peerDependencies: + react: ^18.2.0 + checksum: 23749d9ba442a49676c95c371c04440b55843d18b121723e170bcb086c4e447d7484745e873cdf47f256e1497304d07370bef875a8c80a5d15bbd5f54c2662a5 + languageName: node + linkType: hard + "@jupyterlab/workspaces@npm:^4.5.5": version: 4.5.5 resolution: "@jupyterlab/workspaces@npm:4.5.5" @@ -2161,7 +2467,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: From f130ae787f4faae4e4590834b236a45dc388b0e0 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 13 Mar 2026 17:02:53 +0530 Subject: [PATCH 19/23] revert collapse on startup --- src/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index f0de5f4..bb7010c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -217,9 +217,8 @@ class PluginPlayground { playgroundSidebar.title.icon = tokenSidebarIcon; playgroundSidebar.addWidget(tokenSidebar); playgroundSidebar.addWidget(exampleSidebar); - const accordion = playgroundSidebar.content as AccordionPanel; - accordion.collapse(0); - accordion.collapse(1); + (playgroundSidebar.content as AccordionPanel).expand(0); + (playgroundSidebar.content as AccordionPanel).expand(1); this.app.shell.add(playgroundSidebar, 'right', { rank: 650 }); app.shell.currentChanged?.connect(() => { From 207e35d7a64aeecd9ef70a87cd534c60b528ad45 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 13 Mar 2026 17:03:47 +0530 Subject: [PATCH 20/23] reset irrelevant test file changes --- ui-tests/tests/plugin-playground.spec.ts | 41 +++--------------------- 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/ui-tests/tests/plugin-playground.spec.ts b/ui-tests/tests/plugin-playground.spec.ts index 5ef69bd..f166d44 100644 --- a/ui-tests/tests/plugin-playground.spec.ts +++ b/ui-tests/tests/plugin-playground.spec.ts @@ -11,8 +11,6 @@ const TEST_FILE = 'playground-integration-test.ts'; const PLAYGROUND_SIDEBAR_ID = 'jp-plugin-playground-sidebar'; const TOKEN_SECTION_ID = 'jp-plugin-token-sidebar'; const EXAMPLE_SECTION_ID = 'jp-plugin-example-sidebar'; -const TOKEN_SECTION_LABEL = 'Service Tokens'; -const EXAMPLE_SECTION_LABEL = 'Extension Examples'; test.use({ autoGoto: false }); @@ -37,8 +35,7 @@ export default plugin; async function openSidebarPanel( page: IJupyterLabPageFixture, - sectionId?: string, - sectionLabel?: string + sectionId?: string ): Promise { const sidebarTab = page.sidebar.getTabLocator(PLAYGROUND_SIDEBAR_ID); await expect(sidebarTab).toBeVisible(); @@ -53,18 +50,6 @@ async function openSidebarPanel( } const section = panel.locator(`#${sectionId}`); - if (!(await section.isVisible())) { - if (!sectionLabel) { - throw new Error( - `Missing section label for sidebar section "${sectionId}"` - ); - } - const sectionTitle = panel - .locator('.lm-AccordionPanel-title') - .filter({ hasText: sectionLabel }); - await expect(sectionTitle).toBeVisible(); - await sectionTitle.click(); - } await expect(section).toBeVisible(); return section; } @@ -142,11 +127,7 @@ test('opens a dummy extension example from the sidebar', async ({ page }) => { ); await page.goto(); - const section = await openSidebarPanel( - page, - EXAMPLE_SECTION_ID, - EXAMPLE_SECTION_LABEL - ); + const section = await openSidebarPanel(page, EXAMPLE_SECTION_ID); const filterInput = section.getByPlaceholder('Filter extension examples'); await expect(filterInput).toBeVisible(); @@ -226,11 +207,7 @@ test('opens token sidebar, shows tokens, and filters by exact token', async ({ page }) => { await page.goto(); - const section = await openSidebarPanel( - page, - TOKEN_SECTION_ID, - TOKEN_SECTION_LABEL - ); + const section = await openSidebarPanel(page, TOKEN_SECTION_ID); const tokenListItems = section.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItems.first()).toBeVisible(); @@ -251,11 +228,7 @@ test('opens token sidebar, shows tokens, and filters by exact token', async ({ test('token sidebar copy button shows copied state', async ({ page }) => { await page.goto(); - const section = await openSidebarPanel( - page, - TOKEN_SECTION_ID, - TOKEN_SECTION_LABEL - ); + const section = await openSidebarPanel(page, TOKEN_SECTION_ID); const tokenListItem = section.locator('.jp-PluginPlayground-listItem'); await expect(tokenListItem.first()).toBeVisible(); @@ -283,11 +256,7 @@ 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 section = await openSidebarPanel( - page, - TOKEN_SECTION_ID, - TOKEN_SECTION_LABEL - ); + const section = await openSidebarPanel(page, TOKEN_SECTION_ID); const tokenName = await findImportableToken(section); const filterInput = section.getByPlaceholder('Filter token strings'); From d62803ebfb92471962247783695aa3ba29c36a13 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 13 Mar 2026 19:44:41 +0530 Subject: [PATCH 21/23] support multi-schema example loading --- src/errors.tsx | 2 +- src/index.ts | 26 +++-- src/loader.ts | 272 ++++++++++++++++++++++++++++++++++++------------- 3 files changed, 219 insertions(+), 81 deletions(-) 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/index.ts b/src/index.ts index bb7010c..cf100f5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -310,17 +310,23 @@ class PluginPlayground { } return; } - const plugin = this._ensureDeactivateSupport(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: {} @@ -332,9 +338,15 @@ class PluginPlayground { ).emit(plugin.id); } - await this._deactivateAndDeregisterPlugin(plugin.id); - this.app.registerPlugin(plugin); - if (plugin.autoStart) { + for (const plugin of plugins) { + await this._deactivateAndDeregisterPlugin(plugin.id); + this.app.registerPlugin(plugin); + } + + for (const plugin of plugins) { + if (!plugin.autoStart) { + continue; + } try { await this.app.activatePlugin(plugin.id); } catch (e) { diff --git a/src/loader.ts b/src/loader.ts index c94d045..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,7 +11,7 @@ import { IRequireJS } from './requirejs'; import { IModule, IModuleMember } from './types'; -import { readContentsFileAsText } from './contents'; +import { getDirectoryModel, readContentsFileAsText } from './contents'; export namespace PluginLoader { export interface IOptions { @@ -27,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; } } @@ -46,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 @@ -55,51 +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}`); - const schema = await readContentsFileAsText(serviceManager, path); + + for (const packageJsonPath of packageJsonPaths) { + const packageSchemas = await this._discoverPackageSchemas( + packageJsonPath, + plugins + ); + if (Object.keys(packageSchemas).length > 0) { + return packageSchemas; + } + } + + 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) { - console.log(`Found schema in ${path}`); - return schema; + schemas[plugin.id] = schema; } - console.log(`Did not find schema in ${path}`); } - return null; + + 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' ); @@ -110,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'; } } From 3ffdf16e77f04ed63c9057da037b7835b35cdfaa Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Fri, 13 Mar 2026 20:26:50 +0530 Subject: [PATCH 22/23] skip unsupported example plugins --- src/index.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/index.ts b/src/index.ts index cf100f5..5e1d54b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -347,6 +347,15 @@ class PluginPlayground { 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) { @@ -359,6 +368,20 @@ 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 { From 36212f4150e9b4d049a733e0073d2f094550f0c6 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sat, 14 Mar 2026 00:18:04 +0530 Subject: [PATCH 23/23] updated yarn.lock --- yarn.lock | 365 +++--------------------------------------------------- 1 file changed, 20 insertions(+), 345 deletions(-) diff --git a/yarn.lock b/yarn.lock index 18073f8..9b1eb6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -716,35 +716,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/application@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/application@npm:4.5.5" - 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 - "@lumino/algorithm": ^2.0.4 - "@lumino/application": ^2.4.8 - "@lumino/commands": ^2.3.3 - "@lumino/coreutils": ^2.2.2 - "@lumino/disposable": ^2.1.5 - "@lumino/messaging": ^2.0.4 - "@lumino/polling": ^2.1.5 - "@lumino/properties": ^2.0.4 - "@lumino/signaling": ^2.1.5 - "@lumino/widgets": ^2.7.5 - checksum: 70df5afff4f0c84f1bdaa521711ae0b6b82a009b7c7b7f60180142e633c3c5397e9ceb324a74d797edd104d221d8fd6bf10306faded96a360e508780ca2a3d61 - languageName: node - linkType: hard - -"@jupyterlab/application@npm:~4.5.6": +"@jupyterlab/application@npm:^4.5.5, @jupyterlab/application@npm:~4.5.6": version: 4.5.6 resolution: "@jupyterlab/application@npm:4.5.6" dependencies: @@ -772,36 +744,7 @@ __metadata: 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 - "@lumino/algorithm": ^2.0.4 - "@lumino/commands": ^2.3.3 - "@lumino/coreutils": ^2.2.2 - "@lumino/disposable": ^2.1.5 - "@lumino/domutils": ^2.0.4 - "@lumino/messaging": ^2.0.4 - "@lumino/signaling": ^2.1.5 - "@lumino/virtualdom": ^2.0.4 - "@lumino/widgets": ^2.7.5 - "@types/react": ^18.0.26 - react: ^18.2.0 - sanitize-html: ~2.12.1 - checksum: f1459a948bded9ec1bf40193f6a2c7cefc2483c619eb6e6008a78fd0f1bfffc76053afbd3b844ef5e3d94130b41218f20f5782541c37a70c475d87d16b04e745 - languageName: node - linkType: hard - -"@jupyterlab/apputils@npm:^4.6.6": +"@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: @@ -942,31 +885,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/codeeditor@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/codeeditor@npm:4.5.5" - 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 - "@lumino/coreutils": ^2.2.2 - "@lumino/disposable": ^2.1.5 - "@lumino/dragdrop": ^2.1.8 - "@lumino/messaging": ^2.0.4 - "@lumino/signaling": ^2.1.5 - "@lumino/widgets": ^2.7.5 - react: ^18.2.0 - checksum: b74ba6c6b24924f2fd63d35e25ebbc7ddbfa32e8ae77763b01b3dea647d141ff796b8639b9e1b1221c8dd0646ea9837e0d32f3936e41918213173fb613f887aa - languageName: node - linkType: hard - -"@jupyterlab/codeeditor@npm:^4.5.6": +"@jupyterlab/codeeditor@npm:^4.5.5, @jupyterlab/codeeditor@npm:^4.5.6": version: 4.5.6 resolution: "@jupyterlab/codeeditor@npm:4.5.6" dependencies: @@ -1086,21 +1005,7 @@ __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" - dependencies: - "@lumino/coreutils": ^2.2.2 - "@lumino/disposable": ^2.1.5 - "@lumino/signaling": ^2.1.5 - minimist: ~1.2.0 - path-browserify: ^1.0.0 - url-parse: ~1.5.4 - checksum: 044e7639afacb53cfcb75ab5e9020a9ee042b2ef13ff2906531a774f31177bdcec3380a7e60313c8ee632f035e825e6900b8e0f3a54c88a6efa05560a6f55275 - languageName: node - linkType: hard - -"@jupyterlab/coreutils@npm:^6.5.6, @jupyterlab/coreutils@npm:~6.5.6": +"@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: @@ -1197,33 +1102,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/docregistry@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/docregistry@npm:4.5.5" - 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 - "@lumino/algorithm": ^2.0.4 - "@lumino/coreutils": ^2.2.2 - "@lumino/disposable": ^2.1.5 - "@lumino/messaging": ^2.0.4 - "@lumino/properties": ^2.0.4 - "@lumino/signaling": ^2.1.5 - "@lumino/widgets": ^2.7.5 - react: ^18.2.0 - checksum: fc07e1dbaabdea83638e1d737b1f33d77c016b07cb1bb6c7358d5eb83e0ec9cae7ab44057d9ec391ebfc5590e37807f4ff94e62524cf06f23b798578ac4c0194 - languageName: node - linkType: hard - -"@jupyterlab/docregistry@npm:^4.5.6, @jupyterlab/docregistry@npm:~4.5.6": +"@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: @@ -1555,16 +1434,7 @@ __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" - dependencies: - "@lumino/coreutils": ^2.2.2 - checksum: 3db7d9fa500161bd1d0a5abcd7c05f7a45abc6180ccab0023bc3f80cfdc6a354de61970f67c6835de87d2ad40c520d59e6cc3bf27834dc8f18d0dc867f130b3e - languageName: node - linkType: hard - -"@jupyterlab/nbformat@npm:^4.5.6": +"@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: @@ -1612,20 +1482,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/observables@npm:^5.5.5": - version: 5.5.5 - resolution: "@jupyterlab/observables@npm:5.5.5" - 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 - languageName: node - linkType: hard - -"@jupyterlab/observables@npm:^5.5.6": +"@jupyterlab/observables@npm:^5.5.5, @jupyterlab/observables@npm:^5.5.6": version: 5.5.6 resolution: "@jupyterlab/observables@npm:5.5.6" dependencies: @@ -1756,17 +1613,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/rendermime-interfaces@npm:^3.13.5": - version: 3.13.5 - resolution: "@jupyterlab/rendermime-interfaces@npm:3.13.5" - dependencies: - "@lumino/coreutils": ^1.11.0 || ^2.2.2 - "@lumino/widgets": ^1.37.2 || ^2.7.5 - checksum: b128c5babd0728383f8e35af16aa76cd63e8a4e922f74643298baf647419885f1c7cacf1f7bcd6f6094ba5e7d7d0b20900d1a127c297c1c8df7d6a78f9ee6463 - languageName: node - linkType: hard - -"@jupyterlab/rendermime-interfaces@npm:^3.13.6, @jupyterlab/rendermime-interfaces@npm:~3.13.6": +"@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: @@ -1776,27 +1623,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/rendermime@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/rendermime@npm:4.5.5" - 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 - "@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 - languageName: node - linkType: hard - -"@jupyterlab/rendermime@npm:^4.5.6": +"@jupyterlab/rendermime@npm:^4.5.5, @jupyterlab/rendermime@npm:^4.5.6": version: 4.5.6 resolution: "@jupyterlab/rendermime@npm:4.5.6" dependencies: @@ -1836,26 +1663,7 @@ __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" - 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 - "@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 - languageName: node - linkType: hard - -"@jupyterlab/services@npm:^7.5.6": +"@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: @@ -1904,26 +1712,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/settingregistry@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/settingregistry@npm:4.5.5" - dependencies: - "@jupyterlab/nbformat": ^4.5.5 - "@jupyterlab/statedb": ^4.5.5 - "@lumino/commands": ^2.3.3 - "@lumino/coreutils": ^2.2.2 - "@lumino/disposable": ^2.1.5 - "@lumino/signaling": ^2.1.5 - "@rjsf/utils": ^5.13.4 - ajv: ^8.12.0 - json5: ^2.2.3 - peerDependencies: - react: ">=16" - checksum: 722f0d404cb56167e49bda3d4ad269a880d790adc25708d74287ab2980865e0d9c8f98bac45e4c58af7df14dfcc486916c18951d9b8f9c978d07643257be5a3a - languageName: node - linkType: hard - -"@jupyterlab/settingregistry@npm:^4.5.6": +"@jupyterlab/settingregistry@npm:^4.5.5, @jupyterlab/settingregistry@npm:^4.5.6": version: 4.5.6 resolution: "@jupyterlab/settingregistry@npm:4.5.6" dependencies: @@ -1942,20 +1731,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/statedb@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/statedb@npm:4.5.5" - 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 - languageName: node - linkType: hard - -"@jupyterlab/statedb@npm:^4.5.6": +"@jupyterlab/statedb@npm:^4.5.5, @jupyterlab/statedb@npm:^4.5.6": version: 4.5.6 resolution: "@jupyterlab/statedb@npm:4.5.6" dependencies: @@ -1968,23 +1744,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/statusbar@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/statusbar@npm:4.5.5" - dependencies: - "@jupyterlab/ui-components": ^4.5.5 - "@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 - "@lumino/widgets": ^2.7.5 - react: ^18.2.0 - checksum: dfe4da0b2c373e8e2c3458c720b1940e574cf2e26869319cb6018b8eddebfa4bf21b7022ba65fe1cd33049c9c139045052bbadf61a1ae749fec7490d115cec9f - languageName: node - linkType: hard - -"@jupyterlab/statusbar@npm:^4.5.6": +"@jupyterlab/statusbar@npm:^4.5.5, @jupyterlab/statusbar@npm:^4.5.6": version: 4.5.6 resolution: "@jupyterlab/statusbar@npm:4.5.6" dependencies: @@ -2061,20 +1821,7 @@ __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" - dependencies: - "@jupyterlab/coreutils": ^6.5.5 - "@jupyterlab/rendermime-interfaces": ^3.13.5 - "@jupyterlab/services": ^7.5.5 - "@jupyterlab/statedb": ^4.5.5 - "@lumino/coreutils": ^2.2.2 - checksum: a8965bae1806361470c0799180df930f9d9be030d74424753c4e43effd3f2db2032be56b35d0cf77eb37663d58f71d988e14fd6d9def2fbb53571fbab981b110 - languageName: node - linkType: hard - -"@jupyterlab/translation@npm:^4.5.6": +"@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: @@ -2087,38 +1834,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/ui-components@npm:^4.5.5": - version: 4.5.5 - resolution: "@jupyterlab/ui-components@npm:4.5.5" - 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 - "@lumino/algorithm": ^2.0.4 - "@lumino/commands": ^2.3.3 - "@lumino/coreutils": ^2.2.2 - "@lumino/disposable": ^2.1.5 - "@lumino/messaging": ^2.0.4 - "@lumino/polling": ^2.1.5 - "@lumino/properties": ^2.0.4 - "@lumino/signaling": ^2.1.5 - "@lumino/virtualdom": ^2.0.4 - "@lumino/widgets": ^2.7.5 - "@rjsf/core": ^5.13.4 - "@rjsf/utils": ^5.13.4 - react: ^18.2.0 - react-dom: ^18.2.0 - typestyle: ^2.0.4 - peerDependencies: - react: ^18.2.0 - checksum: 51dd8aecaf5ced1e82418daf13e9c1f8196146bad95f4e7b59bc9810544fbecffc31354a70d15a2344eaa686a981db35f6c5e5ba341084700c57fa86b7c8ae8b - languageName: node - linkType: hard - -"@jupyterlab/ui-components@npm:^4.5.6, @jupyterlab/ui-components@npm:~4.5.6": +"@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: @@ -3548,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: @@ -5649,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: @@ -5969,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 @@ -7020,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: @@ -8945,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 @@ -8959,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"