diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0f6d58..146472b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,11 +29,15 @@ jobs: set -eux jlpm jlpm run lint:check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Test the extension run: | set -eux jlpm run test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build the extension run: | @@ -47,6 +51,8 @@ jobs: jupyter labextension list jupyter labextension list 2>&1 | grep -ie "jupyterlab-deepnote.*OK" python -m jupyterlab.browser_check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Package the extension run: | @@ -55,6 +61,8 @@ jobs: pip install build python -m build pip uninstall -y "jupyterlab_deepnote" jupyterlab + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload extension packages uses: actions/upload-artifact@v4 diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml index 8308798..6527f73 100644 --- a/.github/workflows/check-release.yml +++ b/.github/workflows/check-release.yml @@ -1,9 +1,12 @@ name: Check Release on: push: - branches: ["main"] + branches: ['main'] pull_request: - branches: ["*"] + branches: ['*'] + +env: + NODE_VERSION: 22.x concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -15,13 +18,25 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + cache: 'npm' + node-version: ${{ env.NODE_VERSION }} + registry-url: 'https://npm.pkg.github.com' + scope: '@deepnote' + always-auth: true - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Check Release uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 with: - token: ${{ secrets.GITHUB_TOKEN }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Distributions uses: actions/upload-artifact@v4 diff --git a/.yarnrc.yml b/.yarnrc.yml index 3186f3f..37de770 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1,6 @@ nodeLinker: node-modules +npmScopes: + deepnote: + npmRegistryServer: 'https://npm.pkg.github.com' + npmAlwaysAuth: true + npmAuthToken: '${GITHUB_TOKEN}' diff --git a/README.md b/README.md index a799e60..38ede3d 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,22 @@ Install `jupyterlab`. The extension package itself doesn’t depend on `jupyterl uv pip install jupyterlab ``` +**Configure Access to @deepnote/blocks Package** + +The `@deepnote/blocks` package is published on GitHub Packages. To install it, you'll need to authenticate with GitHub: + +1. Create a GitHub Personal Access Token (classic) with `read:packages` scope: + - Go to https://github.com/settings/tokens + - Click "Generate new token (classic)" + - Select the `read:packages` scope + - Generate and copy the token + +2. Set the `GITHUB_TOKEN` environment variable to ensure `jlpm` (which is a wrapper around Yarn) can download the `@deepnote/blocks` package from the GitHub package registry. You can set the variable in `.zshrc` or manually like: + ```shell + export GITHUB_TOKEN=your_token_here + ``` + Replace `YOUR_TOKEN_HERE` with your actual token. + Install the extension package in editable mode. It installs the package’s dependencies, too: ```shell diff --git a/jupyterlab_deepnote/contents.py b/jupyterlab_deepnote/contents.py index 679d01f..fc9aec0 100644 --- a/jupyterlab_deepnote/contents.py +++ b/jupyterlab_deepnote/contents.py @@ -2,63 +2,7 @@ from jupyter_server.services.contents.filemanager import FileContentsManager from typing import cast -import yaml -from nbformat.v4 import new_notebook, new_code_cell, new_markdown_cell - - -def yaml_to_ipynb(yaml_text: str): - """Convert Deepnote YAML into a minimal Jupyter nbformat v4 notebook.""" - try: - data = yaml.safe_load(yaml_text) - except Exception: - return new_notebook(cells=[]) - - notebooks = ( - data.get("project", {}).get("notebooks", []) if isinstance(data, dict) else [] - ) - - if not notebooks: - return new_notebook(cells=[]) - - # Collect notebook names - notebook_names = [nb.get("name", "") for nb in notebooks] - - # Build all_notebooks dict: name -> full nbformat notebook JSON - all_notebooks = {} - for nb in notebooks: - nb_blocks = nb.get("blocks", []) - nb_cells = [] - for block in sorted(nb_blocks, key=lambda b: b.get("sortingKey", "")): - btype = block.get("type", "code") - content = block.get("content", "") - if btype == "code": - nb_cells.append(new_code_cell(content)) - else: - nb_cells.append(new_markdown_cell(content)) - # Use the notebook name as key - nb_name = nb.get("name", "") - all_notebooks[nb_name] = new_notebook(cells=nb_cells) - - # Use first notebook's cells to render initially - nb0 = notebooks[0] - blocks = nb0.get("blocks", []) - cells = [] - for block in sorted(blocks, key=lambda b: b.get("sortingKey", "")): - btype = block.get("type", "code") - content = block.get("content", "") - if btype == "code": - cells.append(new_code_cell(content)) - else: - cells.append(new_markdown_cell(content)) - - metadata = { - "deepnote": {"notebook_names": notebook_names, "notebooks": all_notebooks} - } - return new_notebook(cells=cells, metadata=metadata) - - -def yaml_to_ipynb_dummy(yaml_text: str) -> dict: - return {"nbformat": 4, "nbformat_minor": 5, "metadata": {}, "cells": []} +from nbformat.v4 import new_notebook class DeepnoteContentsManager(FileContentsManager): @@ -74,15 +18,13 @@ def get(self, path, content=True, type=None, format=None, require_hash=False): else: yaml_text = cast(str, _content) - nb_node = yaml_to_ipynb(yaml_text) - model = self._base_model(path) model["type"] = "notebook" model["format"] = "json" - model["content"] = nb_node + model["content"] = new_notebook( + cells=[], metadata={"deepnote": {"rawYamlString": yaml_text}} + ) model["writable"] = False - self.mark_trusted_cells(nb_node, path) - self.validate_notebook_model(model, validation_error={}) if require_hash: # Accept 2- or 3-tuple; we only need the bytes diff --git a/package.json b/package.json index 964568c..bb47bba 100644 --- a/package.json +++ b/package.json @@ -58,18 +58,23 @@ "watch:labextension": "jupyter labextension watch ." }, "dependencies": { + "@deepnote/blocks": "^1.1.0", "@jupyterlab/application": "^4.0.0", "@jupyterlab/coreutils": "^6.0.0", "@jupyterlab/notebook": "^4.4.7", "@jupyterlab/services": "^7.0.0", "@jupyterlab/settingregistry": "^4.0.0", - "@lumino/widgets": "^2.7.1" + "@lumino/widgets": "^2.7.1", + "lodash": "^4.17.21", + "yaml": "^2.8.1", + "zod": "^4.1.11" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", "@jupyterlab/testutils": "^4.0.0", "@types/jest": "^29.2.0", "@types/json-schema": "^7.0.11", + "@types/lodash": "^4.17.20", "@types/react": "^18.0.26", "@types/react-addons-linked-state-mixin": "^0.14.22", "@typescript-eslint/eslint-plugin": "^6.1.0", @@ -141,19 +146,6 @@ "@typescript-eslint" ], "rules": { - "@typescript-eslint/naming-convention": [ - "error", - { - "selector": "interface", - "format": [ - "PascalCase" - ], - "custom": { - "regex": "^I[A-Z]", - "match": true - } - } - ], "@typescript-eslint/no-unused-vars": [ "warn", { diff --git a/src/index.tsx b/src/components/NotebookPicker.tsx similarity index 60% rename from src/index.tsx rename to src/components/NotebookPicker.tsx index 6382c0f..a7f15f6 100644 --- a/src/index.tsx +++ b/src/components/NotebookPicker.tsx @@ -1,59 +1,9 @@ -import { - JupyterFrontEnd, - JupyterFrontEndPlugin -} from '@jupyterlab/application'; import React from 'react'; -import { IToolbarWidgetRegistry, ReactWidget } from '@jupyterlab/apputils'; -import { - INotebookWidgetFactory, - NotebookPanel, - NotebookWidgetFactory -} from '@jupyterlab/notebook'; -import { Widget } from '@lumino/widgets'; +import { ReactWidget } from '@jupyterlab/apputils'; +import { NotebookPanel } from '@jupyterlab/notebook'; import { HTMLSelect } from '@jupyterlab/ui-components'; -const plugin: JupyterFrontEndPlugin = { - id: 'jupyterlab-deepnote:plugin', - description: 'Open .deepnote files as notebooks.', - autoStart: true, - requires: [INotebookWidgetFactory, IToolbarWidgetRegistry], - activate: ( - app: JupyterFrontEnd, - notebookWidgetFactory: NotebookWidgetFactory, - toolbarRegistry: IToolbarWidgetRegistry - ) => { - app.docRegistry.addFileType( - { - name: 'deepnote', - displayName: 'Deepnote Notebook', - extensions: ['.deepnote'], - mimeTypes: ['text/yaml', 'application/x-yaml'], - fileFormat: 'text', - contentType: 'file' - }, - [notebookWidgetFactory.name] - ); - - app.docRegistry.setDefaultWidgetFactory( - 'deepnote', - notebookWidgetFactory.name - ); - - toolbarRegistry.addFactory( - notebookWidgetFactory.name, - 'deepnote:switch-notebook', - panel => { - if (!panel.context.path.endsWith('.deepnote')) { - return new Widget(); // don’t render for .ipynb or others - } - - return new NotebookPicker(panel); - } - ); - } -}; - -class NotebookPicker extends ReactWidget { +export class NotebookPicker extends ReactWidget { private selected: string | null = null; constructor(private panel: NotebookPanel) { @@ -68,7 +18,7 @@ class NotebookPicker extends ReactWidget { ? metadataNames : []; - this.selected = names.length > 0 ? names[0] : null; + this.selected = names.length === 0 ? null : (names[0] ?? null); this.update(); }); } @@ -115,7 +65,6 @@ class NotebookPicker extends ReactWidget { return ( {}} @@ -141,5 +90,3 @@ class NotebookPicker extends ReactWidget { ); } } - -export default plugin; diff --git a/src/convert-deepnote-block-to-jupyter-cell.ts b/src/convert-deepnote-block-to-jupyter-cell.ts new file mode 100644 index 0000000..5799004 --- /dev/null +++ b/src/convert-deepnote-block-to-jupyter-cell.ts @@ -0,0 +1,44 @@ +import { + createMarkdown, + createPythonCode, + DeepnoteBlock +} from '@deepnote/blocks'; +import _cloneDeep from 'lodash/cloneDeep'; +import { ICodeCell, IMarkdownCell } from '@jupyterlab/nbformat'; +import { convertDeepnoteBlockTypeToJupyter } from './convert-deepnote-block-type-to-jupyter'; + +export function convertDeepnoteBlockToJupyterCell(block: DeepnoteBlock) { + const blockCopy = _cloneDeep(block); + const jupyterCellMetadata = { ...blockCopy.metadata, cell_id: blockCopy.id }; + const jupyterCellType = convertDeepnoteBlockTypeToJupyter(blockCopy.type); + + if (jupyterCellType === 'code') { + const blockOutputs = blockCopy.outputs ?? []; + + if (Array.isArray(blockOutputs)) { + blockOutputs.forEach(output => { + delete output.truncated; + }); + } + + const source = createPythonCode(blockCopy); + + const jupyterCell: ICodeCell = { + cell_type: 'code', + metadata: jupyterCellMetadata, + execution_count: blockCopy.executionCount ?? null, + outputs: blockOutputs, + source + }; + return jupyterCell; + } else { + // Markdown cell + const source = createMarkdown(blockCopy); + const jupyterCell: IMarkdownCell = { + cell_type: 'markdown', + metadata: {}, + source + }; + return jupyterCell; + } +} diff --git a/src/convert-deepnote-block-type-to-jupyter.ts b/src/convert-deepnote-block-type-to-jupyter.ts new file mode 100644 index 0000000..8543e9d --- /dev/null +++ b/src/convert-deepnote-block-type-to-jupyter.ts @@ -0,0 +1,32 @@ +export function convertDeepnoteBlockTypeToJupyter(blockType: string) { + switch (blockType) { + case 'big-number': + case 'code': + case 'sql': + case 'notebook-function': + case 'input-text': + case 'input-checkbox': + case 'input-textarea': + case 'input-file': + case 'input-select': + case 'input-date-range': + case 'input-date': + case 'input-slider': + case 'visualization': + return 'code'; + + case 'markdown': + case 'text-cell-h1': + case 'text-cell-h3': + case 'text-cell-h2': + case 'text-cell-p': + case 'text-cell-bullet': + case 'text-cell-todo': + case 'text-cell-callout': + case 'image': + case 'button': + case 'separator': + default: + return 'markdown'; + } +} diff --git a/src/deepnote-content-provider.ts b/src/deepnote-content-provider.ts new file mode 100644 index 0000000..86ec0e0 --- /dev/null +++ b/src/deepnote-content-provider.ts @@ -0,0 +1,58 @@ +import { Contents, RestContentProvider } from '@jupyterlab/services'; +import { z } from 'zod'; +import { transformDeepnoteYamlToNotebookContent } from './transform-deepnote-yaml-to-notebook-content'; + +export const deepnoteContentProviderName = 'deepnote-content-provider'; + +const deepnoteNotebookSchema = z.object({ + cells: z.array(z.any()), // or refine further with nbformat + metadata: z.object({ + deepnote: z.object({ + rawYamlString: z.string() + }) + }), + nbformat: z.number(), + nbformat_minor: z.number() +}); + +export class DeepnoteContentProvider extends RestContentProvider { + async get( + localPath: string, + options?: Contents.IFetchOptions + ): Promise { + const model = await super.get(localPath, options); + const isDeepnoteFile = + localPath.endsWith('.deepnote') && model.type === 'notebook'; + + if (!isDeepnoteFile) { + // Not a .deepnote file, return as-is + return model; + } + + const validatedModelContent = deepnoteNotebookSchema.safeParse( + model.content + ); + + if (!validatedModelContent.success) { + console.error( + 'Invalid .deepnote file content:', + validatedModelContent.error + ); + // Return an empty notebook instead of throwing an error + model.content.cells = []; + return model; + } + + const transformedModelContent = + await transformDeepnoteYamlToNotebookContent( + validatedModelContent.data.metadata.deepnote.rawYamlString + ); + + const transformedModel = { + ...model, + content: transformedModelContent + }; + + return transformedModel; + } +} diff --git a/src/fallback-data.ts b/src/fallback-data.ts new file mode 100644 index 0000000..7d3f45c --- /dev/null +++ b/src/fallback-data.ts @@ -0,0 +1,30 @@ +import { ICodeCell } from '@jupyterlab/nbformat'; +import { IDeepnoteNotebookContent } from './types'; + +export const blankCodeCell: ICodeCell = { + cell_type: 'code', + source: '', + metadata: {}, + outputs: [], + execution_count: null +}; + +export const blankDeepnoteNotebookContent: IDeepnoteNotebookContent = { + cells: [ + { + cell_type: 'code', + source: '# Transformed from Deepnote YAML\n', + metadata: {}, + outputs: [], + execution_count: null + } + ], + metadata: { + deepnote: { + rawYamlString: null, + deepnoteFile: null + } + }, + nbformat: 4, + nbformat_minor: 0 +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c54e61a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,79 @@ +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; +import { IToolbarWidgetRegistry } from '@jupyterlab/apputils'; +import { + INotebookWidgetFactory, + NotebookPanel, + NotebookWidgetFactory +} from '@jupyterlab/notebook'; +import { Widget } from '@lumino/widgets'; +import { ContentsManager } from '@jupyterlab/services'; +import { + DeepnoteContentProvider, + deepnoteContentProviderName +} from './deepnote-content-provider'; +import { NotebookPicker } from './components/NotebookPicker'; + +const plugin: JupyterFrontEndPlugin = { + id: 'jupyterlab-deepnote:plugin', + description: 'Open .deepnote files as notebooks.', + autoStart: true, + requires: [INotebookWidgetFactory, IToolbarWidgetRegistry], + activate: ( + app: JupyterFrontEnd, + notebookWidgetFactory: NotebookWidgetFactory, + toolbarRegistry: IToolbarWidgetRegistry + ) => { + // Register a custom contents provider for the default notebook widget factory. + const drive = (app.serviceManager.contents as ContentsManager).defaultDrive; + const registry = drive?.contentProviderRegistry; + if (!registry) { + // If content provider is a non-essential feature and support for JupyterLab <4.4 is desired: + console.error( + 'Cannot initialize content provider: no content provider registry.' + ); + return; + } + const deepnoteContentProvider = new DeepnoteContentProvider({ + // These options are only required if extending the `RestContentProvider`. + apiEndpoint: '/api/contents', + serverSettings: app.serviceManager.serverSettings + }); + registry.register(deepnoteContentProviderName, deepnoteContentProvider); + notebookWidgetFactory.contentProviderId = deepnoteContentProviderName; + + // Register .deepnote file type and set the notebook widget factory as the default. + app.docRegistry.addFileType( + { + name: 'deepnote', + displayName: 'Deepnote Notebook', + extensions: ['.deepnote'], + mimeTypes: ['text/yaml', 'application/x-yaml'], + fileFormat: 'text', + contentType: 'file' + }, + [notebookWidgetFactory.name] + ); + app.docRegistry.setDefaultWidgetFactory( + 'deepnote', + notebookWidgetFactory.name + ); + + // Add a toolbar item to switch between notebooks in a .deepnote file. + toolbarRegistry.addFactory( + notebookWidgetFactory.name, + 'deepnote:switch-notebook', + panel => { + if (!panel.context.path.endsWith('.deepnote')) { + return new Widget(); // don’t render for .ipynb or others + } + + return new NotebookPicker(panel); + } + ); + } +}; + +export default plugin; diff --git a/src/transform-deepnote-yaml-to-notebook-content.ts b/src/transform-deepnote-yaml-to-notebook-content.ts new file mode 100644 index 0000000..83e7fdc --- /dev/null +++ b/src/transform-deepnote-yaml-to-notebook-content.ts @@ -0,0 +1,38 @@ +import { IDeepnoteNotebookContent } from './types'; +import { blankCodeCell, blankDeepnoteNotebookContent } from './fallback-data'; +import { deserializeDeepnoteFile } from '@deepnote/blocks'; +import { convertDeepnoteBlockToJupyterCell } from './convert-deepnote-block-to-jupyter-cell'; + +export async function transformDeepnoteYamlToNotebookContent( + yamlString: string +): Promise { + try { + const deepnoteFile = await deserializeDeepnoteFile(yamlString); + + const selectedNotebook = deepnoteFile.project.notebooks[0]; + + if (!selectedNotebook) { + return { + ...blankDeepnoteNotebookContent, + cells: [ + { + ...blankCodeCell, + source: '# No notebooks found in the Deepnote file.\n' + } + ] + }; + } + + const cells = selectedNotebook.blocks.map( + convertDeepnoteBlockToJupyterCell + ); + + return { + ...blankDeepnoteNotebookContent, + cells + }; + } catch (error) { + console.error('Failed to deserialize Deepnote file:', error); + throw new Error('Failed to transform Deepnote YAML to notebook content.'); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..73f8435 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,14 @@ +import { INotebookContent, INotebookMetadata } from '@jupyterlab/nbformat'; +import type { DeepnoteFile } from '@deepnote/blocks'; + +export interface IDeepnoteNotebookMetadata extends INotebookMetadata { + deepnote: { + rawYamlString: string | null; + deepnoteFile: DeepnoteFile | null; + }; +} + +export interface IDeepnoteNotebookContent + extends Omit { + metadata: IDeepnoteNotebookMetadata; +} diff --git a/tsconfig.json b/tsconfig.json index 25af040..81a7a75 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "esModuleInterop": true, "incremental": true, "jsx": "react", - "lib": ["DOM", "ES2018", "ES2020.Intl"], + "lib": ["ES2022", "DOM"], "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, @@ -18,8 +18,9 @@ "rootDir": "src", "strict": true, "strictNullChecks": true, + "noUncheckedIndexedAccess": true, "target": "ES2018", "types": ["jest"] }, - "include": ["src/*"] + "include": ["src"] } diff --git a/yarn.lock b/yarn.lock index dd8960e..22a4354 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1577,6 +1577,17 @@ __metadata: languageName: node linkType: hard +"@deepnote/blocks@npm:^1.1.0": + version: 1.1.0 + resolution: "@deepnote/blocks@npm:1.1.0::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40deepnote%2Fblocks%2F1.1.0%2Feaed628f944e73fe64a6a9fd5f93b2bcfe130b44" + dependencies: + ts-dedent: ^2.2.0 + yaml: ^2.8.1 + zod: ^4.1.12 + checksum: 00ceb43c10c9c50e921bb83e2cd4b2cb8316f61edc1ebc89e3588d03ebe15bd5d2f9b2af4402c15e2b9e49a7a0b5603509103b86fc0814b19f6d83a40d82ce87 + languageName: node + linkType: hard + "@discoveryjs/json-ext@npm:^0.5.0": version: 0.5.7 resolution: "@discoveryjs/json-ext@npm:0.5.7" @@ -3266,6 +3277,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4.17.20": + version: 4.17.20 + resolution: "@types/lodash@npm:4.17.20" + checksum: dc7bb4653514dd91117a4c4cec2c37e2b5a163d7643445e4757d76a360fabe064422ec7a42dde7450c5e7e0e7e678d5e6eae6d2a919abcddf581d81e63e63839 + languageName: node + linkType: hard + "@types/minimist@npm:^1.2.2": version: 1.2.5 resolution: "@types/minimist@npm:1.2.5" @@ -6794,6 +6812,7 @@ __metadata: version: 0.0.0-use.local resolution: "jupyterlab-deepnote@workspace:." dependencies: + "@deepnote/blocks": ^1.1.0 "@jupyterlab/application": ^4.0.0 "@jupyterlab/builder": ^4.0.0 "@jupyterlab/coreutils": ^6.0.0 @@ -6804,6 +6823,7 @@ __metadata: "@lumino/widgets": ^2.7.1 "@types/jest": ^29.2.0 "@types/json-schema": ^7.0.11 + "@types/lodash": ^4.17.20 "@types/react": ^18.0.26 "@types/react-addons-linked-state-mixin": ^0.14.22 "@typescript-eslint/eslint-plugin": ^6.1.0 @@ -6813,6 +6833,7 @@ __metadata: eslint-config-prettier: ^8.8.0 eslint-plugin-prettier: ^5.0.0 jest: ^29.2.0 + lodash: ^4.17.21 mkdirp: ^1.0.3 npm-run-all2: ^7.0.1 prettier: ^3.0.0 @@ -6825,7 +6846,9 @@ __metadata: stylelint-csstree-validator: ^3.0.0 stylelint-prettier: ^4.0.0 typescript: ~5.8.0 + yaml: ^2.8.1 yjs: ^13.5.0 + zod: ^4.1.11 languageName: unknown linkType: soft @@ -9016,6 +9039,13 @@ __metadata: languageName: node linkType: hard +"ts-dedent@npm:^2.2.0": + version: 2.2.0 + resolution: "ts-dedent@npm:2.2.0" + checksum: 93ed8f7878b6d5ed3c08d99b740010eede6bccfe64bce61c5a4da06a2c17d6ddbb80a8c49c2d15251de7594a4f93ffa21dd10e7be75ef66a4dc9951b4a94e2af + languageName: node + linkType: hard + "ts-jest@npm:^29.1.0": version: 29.4.1 resolution: "ts-jest@npm:29.4.1" @@ -9761,6 +9791,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.8.1": + version: 2.8.1 + resolution: "yaml@npm:2.8.1" + bin: + yaml: bin.mjs + checksum: 35b46150d48bc1da2fd5b1521a48a4fa36d68deaabe496f3c3fa9646d5796b6b974f3930a02c4b5aee6c85c860d7d7f79009416724465e835f40b87898c36de4 + languageName: node + linkType: hard + "yargs-parser@npm:^20.2.9": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" @@ -9805,3 +9844,17 @@ __metadata: checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard + +"zod@npm:^4.1.11": + version: 4.1.11 + resolution: "zod@npm:4.1.11" + checksum: 022d59f85ebe054835fbcdc96a93c01479a64321104f846cb5644812c91e00d17ae3479f823956ec9b04e4351dd32841e1f12c567e81bc43f6e21ef5cc02ce3c + languageName: node + linkType: hard + +"zod@npm:^4.1.12": + version: 4.1.12 + resolution: "zod@npm:4.1.12" + checksum: 91174acc7d2ca5572ad522643474ddd60640cf6877b5d76e5d583eb25e3c4072c6f5eb92ab94f231ec5ce61c6acdfc3e0166de45fb1005b1ea54986b026b765f + languageName: node + linkType: hard