# JupyterLab Extension Development This guide provides coding standards and best practices for developing JupyterLab extensions. Follow these rules to align with community standards and keep your extension maintainable. **Extension type**: frontend-and-server --- # Project: notebook-snapshot ## What This Project Is Notebook 版本快照扩展:自动保存 `.ipynb` 历史版本(**无 Git 依赖**),支持侧边栏查看版本时间轴、Cell 级 diff、无刷新恢复。 **`design.md` 是目标架构文档**;当前代码仍处于模板初始状态(`src/index.ts` 调用 `/snapshot/hello`,`snapshot/routes.py` 只有 `HelloRouteHandler`)。实现新功能一律以 `design.md` 为准,并把模板代码替换为真实实现。 ## Architecture (design.md 摘要) - **前端**:手动提交(notebook 工具栏按钮 / 命令面板 `snapshot:commit`)→ 对话框填 name + description → POST commit(**无防抖自动提交,不绑定 Ctrl+S**);左侧栏版本时间轴(list API);Cell 级 diff;restore 用 `model.fromJSON()` + `context.save()` 无刷新覆盖 - **后端**:路由挂在 `/snapshot/` 命名空间;存储 adapter(Local 默认;S3 / MinIO 走 boto3 做成 pip extra,**基础安装保持零依赖**);MD5 去重(hash 命中最近版本则弹 warning 跳过);gzip 压缩持久化;`manifest.json` 记录版本索引 - **API 契约**: - `POST /snapshot/notebook-version/commit` — body `{path, content, name, description}`,content 为前端显式提交的 notebook JSON 本体(后端**不**从磁盘读);若 hash 与最近版本相同,响应 `{skipped: true, reason: "unchanged", message}`(不视为错误),前端弹 warning - `GET /snapshot/notebook-version/list?path=...` — 返回 manifest 版本列表(name / description / 时间戳 / hash / 大小) - `GET /snapshot/notebook-version/content?path=...&id=...` — 解压并返回旧版本 notebook JSON(已剥离 outputs) - `POST /snapshot/notebook-version/restore` — **暂缓**,前端 restore 走 `fromJSON()` + `save()` 闭环 ## Hard Conventions (从 design.md 评审钉死,实现时不得违背) 1. **Hash 在清洗之后计算 + 去重**:快照存的是剥离全部 `outputs` / `execution_count` 后的内容,MD5 基于清洗后 JSON 计算;若新内容 hash 与**最近版本**相同,**跳过保存**(不写文件、不更新 manifest),通过 `SnapshotUnchangedError` 通知前端弹 warning 2. **禁用 Monaco**:JupyterLab 4 使用 CodeMirror 6,行级 diff 用 `diff`(jsdiff)库或 CodeMirror merge 视图,不得引入第二个编辑器 3. **历史 append-only**:永不改写 / 删除历史版本;restore 走确认对话框(警告未保存修改将丢失,提示先建快照)→ `fromJSON()` → `context.save()`——注意手动模式下 restore **不再**自动生成备份快照 4. **快照走命令系统**:注册 `snapshot:commit` 命令(工具栏 + 命令面板);**不绑定 Ctrl+S**(与 `docmanager:save` 冲突,Lumino 后注册者会吃掉保存),不拦截 DOM 键盘事件 5. **manifest 只是索引**:真相在快照文件的 envelope(`{id, timestamp, name, description, hash, size, notebook}`)内;manifest 写入原子化(tmp + rename),损坏 / 丢失时扫描版本文件重建 6. **Cell diff 匹配顺序**:先按 nbformat 4.5 的 cell id 匹配,LCS 仅作对齐兜底——纯 LCS 在 cell 重排时会产生噪声 diff ## Environment 本仓库使用 **uv/venv**(非 conda): ```bash source .venv/bin/activate ``` ## Commands - **构建**:`jlpm build`(改 TS 后必跑)/ `jlpm watch`(自动重建) - **前端测试**:`jlpm test`(jest);单文件:`jlpm jest src/__tests__/snapshot.spec.ts` - **后端测试**:`pytest snapshot/tests/ -vv`;单用例:`pytest snapshot/tests/test_routes.py::test_hello` - **Lint**:`jlpm lint:check`(stylelint + prettier + eslint) - **安装注册**:`pip install -e ".[dev,test]" && jupyter-builder develop . --overwrite && jupyter server extension enable snapshot` - **集成测试**:`ui-tests/`(Playwright + Galata,见 `ui-tests/README.md`) --- ## External Documentation and Resources ### PRIORITY RESOURCE USAGE **When you encounter uncertainty, incomplete information, or need implementation examples, you MUST consult these external resources FIRST before attempting to implement features.** Use your available tools (web search, documentation search) to access and retrieve content from these resources when: - You're unsure about API usage, method signatures, or interface requirements - You need to verify the correct approach for a feature or pattern - You're looking for existing implementation examples or best practices - You're debugging unexpected behavior and need official documentation - You're implementing a feature that likely exists in core JupyterLab or other extensions ### Required External Resources **These resources are PRIORITY references. Always check them when you need external information:** 1. **JupyterLab Extension Developer Guide** - URL: https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html - Use for: Extension patterns, architecture overview, development workflow, and best practices - **Action**: Use web search or documentation tools to retrieve specific sections when needed 2. **JupyterLab API Reference (Frontend)** - URL: https://jupyterlab.readthedocs.io/en/latest/api/index.html - Use for: Complete API reference for all JupyterLab frontend packages, interfaces, classes, and methods - **Action**: Search for specific APIs when you need method signatures, interface definitions, or class documentation. For example, search "JupyterLab IRenderMime.IRenderer" or "JupyterLab ICommandPalette" 3. **JupyterLab Extension Examples Repository** - URL: https://github.com/jupyterlab/extension-examples - Use for: Working code examples, implementation patterns, complete working extensions - **Action**: Search this repository for similar features before implementing from scratch 4. **JupyterLab Core Repository** - URL: https://github.com/jupyterlab/jupyterlab - Use for: Reference implementations in `packages/` directory - all core packages are extensions themselves - **Action**: When implementing complex features, search this repo for how core extensions solve similar problems 5. **Jupyter Server API Documentation** - URL: https://jupyter-server.readthedocs.io/ - Use for: Server-side API handlers, route setup, backend integration patterns - **Action**: Consult when working on backend routes or server extension configuration 6. **Project-Specific Documentation** - Locations: `README.md`, `RELEASE.md` in project root; check for `docs/` directory - Use for: Project requirements, specific configuration, custom conventions - **Action**: Read these files at the start of work and reference when making architectural decisions ### When to Use These Resources **ALWAYS consult external documentation when:** - ❗ You're about to implement a feature without knowing if there's an established pattern - ❗ An API call or method isn't working as expected - ❗ You need to understand the correct lifecycle methods or hooks - ❗ You're uncertain about type definitions or interfaces - ❗ You're implementing something that seems like it should be a common pattern **HOW to access these resources:** - 🔍 Use web search tools with specific queries like: "JupyterLab IRenderMime.IRenderer interface documentation" - 🔍 Search GitHub repositories for code examples: "JupyterLab extension examples widget" - 🔍 Retrieve documentation pages to read API specifications and usage guidelines - 🔍 Look for working code in the extension-examples repository before writing custom implementations **Remember:** These resources contain the authoritative information. Don't guess at API usage - look it up! ## Code Quality Rules ### Logging and Debugging **❌ Don't**: Use `console.log()` **✅ Do**: Use structured logging or user-facing notifications ```typescript // In TypeScript files like src/index.ts import { INotification } from '@jupyterlab/apputils'; app.commands.notifyCommandChanged(); ``` **✅ Do**: Use `console.error()` to log low-level error details that should not be presented to users in the UI **✅ Do**: Use `console.warn()` to log non-optimal conditions, e.g. an unexpected response from an external API that's been successfully handled. ### Type Safety **✅ Do**: Define explicit interfaces (see example patterns in `src/index.ts`) ```typescript interface PluginConfig { enabled: boolean; apiEndpoint: string; } ``` **❌ Don't**: Use the `any` type in TypeScript files **✅ Do**: Prefer typeguards over type casts ### File-Scoped Validation After editing TypeScript files, run: ```bash npx tsc --noEmit src/index.ts # Check single file npx tsc --noEmit # Check all files ``` After editing Python files (like `snapshot/routes.py`): ```bash python -m py_compile snapshot/__init__.py # Check single file for syntax errors ``` ## Coding Standards ### Naming Conventions **Python** (in `snapshot/*.py` files): - **✅ Do**: Use PEP 8 style with 4-space indentation - Classes: `DataProcessor`, `UserDataRouteHandler` - Functions/methods: `setup_route_handlers()`, `process_request()` - Private: `_internal_method()` - **❌ Don't**: Use camelCase for Python or mix styles **TypeScript/JavaScript** (in `src/*.ts` files): - **✅ Do**: Use consistent casing - Classes/interfaces: `MyPanelWidget`, `PluginConfig` - Functions/variables: `activatePlugin()`, `buttonCount` - Constants: `PLUGIN_ID`, `COMMAND_ID` - **✅ Do**: Use 2-space indentation (Prettier default) - **❌ Don't**: Use lowercase_snake_case or inconsistent formatting ### Documentation **✅ Do**: Add JSDoc for TypeScript and docstrings for Python ```typescript /** * Activates the extension plugin. * @param app - JupyterLab application instance */ function activate(app: JupyterFrontEnd): void {} ``` **❌ Don't**: Leave complex logic undocumented or use vague names like `MyRouteHandler` — prefer `DataUploadRouteHandler` ### Code Organization **✅ Do**: Keep backend and frontend logic separate - Backend processing in `snapshot/routes.py` - Frontend calls in `src/request.ts` using `requestAPI()` **❌ Don't**: Duplicate business logic across TypeScript and Python **✅ Do**: Implement features completely or not at all. Notify the prompter if you're unable to completely implement a feature. **❌ Don't**: Leave TODO comments or dead code in committed files ## Project Structure and Naming ### Package Naming **Python package** (directory name and imports): - **✅ Do**: `snapshot/` with underscores, all lowercase - **❌ Don't**: Use dashes in any Python file or directory names **PyPI distribution name** (in `pyproject.toml`): - **✅ Do**: Use dashes instead of underscores, like `jupyterlab-myext` - **✅ Do**: Match it to the npm package name for consistency **NPM package** (in `package.json`): - **✅ Do**: Use lowercase with dashes: `"jupyterlab-myext"` or scoped `"@org/myext"` - **❌ Don't**: Mix naming styles between package.json and pyproject.toml ### Plugin and Command IDs **✅ Do**: Define plugin ID in `src/index.ts`: ```typescript const PLUGIN_ID = 'snapshot:plugin'; ``` **✅ Do**: For extensions with multiple commands, create a `src/commands.ts` module to centralize command definitions: ```typescript // src/commands.ts import { JupyterFrontEnd } from '@jupyterlab/application'; import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; // Command IDs export namespace CommandIDs { export const openPanel = 'snapshot:open-panel'; export const refreshData = 'snapshot:refresh-data'; } // Command argument types export namespace CommandArguments { export interface IOpenPanel { filePath?: string; } export interface IRefreshData { force?: boolean; } } /** * Register all commands with the application command registry. * Call this function in your plugin's activate function. */ export function registerCommands(app: JupyterFrontEnd): void { // Register the openPanel command app.commands.addCommand(CommandIDs.openPanel, { label: 'Open Panel', caption: 'Open the extension panel', execute: (args: ReadonlyPartialJSONObject) => { const typedArgs = args as CommandArguments.IOpenPanel; // Implementation using typedArgs.filePath } }); // Register the refreshData command app.commands.addCommand(CommandIDs.refreshData, { label: 'Refresh Data', execute: (args: ReadonlyPartialJSONObject) => { const typedArgs = args as CommandArguments.IRefreshData; // Implementation using typedArgs.force } }); } ``` Then in `src/index.ts`: ```typescript import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { registerCommands, CommandIDs, CommandArguments } from './commands'; const plugin: JupyterFrontEndPlugin = { id: 'snapshot:plugin', autoStart: true, activate: (app: JupyterFrontEnd) => { // Register all commands with JupyterLab's command registry registerCommands(app); // Commands are now registered and can be executed anywhere: // - From the command palette // - From menus // - Programmatically via app.commands.execute() // ... rest of activation (e.g., add to palette, create widgets, etc.) } }; export default plugin; ``` **Executing commands with typed arguments:** ```typescript import { CommandIDs, CommandArguments } from './commands'; // Execute with typed arguments await app.commands.execute(CommandIDs.openPanel, { filePath: '/path/to/file' } as CommandArguments.IOpenPanel); // Execute without arguments await app.commands.execute(CommandIDs.refreshData); ``` **Notes:** - Accept `ReadonlyPartialJSONObject` in the execute function signature (required by Lumino) - Cast to your typed interface inside the function for type safety - Use namespaces (`CommandIDs`, `CommandArguments`) to organize related constants and types - This pattern matches how popular extensions like `jupyterlab-git` handle commands **✅ Do**: For simple extensions with 1-2 commands, you can define them directly in `src/index.ts` **❌ Don't**: Use generic IDs like `'mycommand'` or mix casing styles ### File Organization **✅ Do**: Organize related files into directories and name by their purpose - Widget components: `src/widgets/DataPanel.tsx` (class `DataPanel`) - Command definitions (for multiple commands): `src/commands.ts` with `COMMANDS` mapping - API utilities: `src/api.ts` (not `src/utils.ts`) - Backend routes: `snapshot/routes.py` (class `DataRouteHandler`) - Frontend logic: `src/` directory - Python package: `snapshot/` directory **❌ Don't**: Create catch-all files or directories like `utils.ts` or `helpers.py` or `handlers.py` — partition by feature instead ## Backend–Frontend Integration ### Integration Workflow (Critical!) When connecting frontend and backend, **ALWAYS follow this order**: 1. **Read the backend first** — Check `snapshot/routes.py` to understand the existing API contract 2. **Write frontend to match** — Create TypeScript interfaces in `src/api.ts` that match backend responses exactly 3. **Or modify backend intentionally** — If changing the backend, update it first, then write matching frontend code **Why this matters**: Writing frontend code based on assumptions leads to field name mismatches (e.g., expecting `message` when backend returns `data`), causing empty widgets and debugging cycles. Always verify the actual backend response format first. ### Backend Routes Create RESTful endpoints in `snapshot/routes.py`: **✅ Do**: Extend `APIHandler` from `jupyter_server.base.handlers` ```python from jupyter_server.base.handlers import APIHandler from jupyter_server.utils import url_path_join class DataRouteHandler(APIHandler): def get(self): """Handle GET requests.""" result = {"status": "success", "data": "Hello"} self.finish(result) def post(self): """Handle POST requests.""" body = self.get_json_body() # Process body... self.finish({"status": "success"}) def setup_route_handlers(web_app): base_url = web_app.settings.get("base_url", "/") data_route = url_path_join(base_url, "snapshot", "data") web_app.add_handlers(r".*$", [(data_route, DataRouteHandler)]) ``` **✅ Do**: Include error handling in route handlers **❌ Don't**: - Hardcode URL paths — always use `url_path_join()` - Use plain `tornado.web.RequestHandler` — instead, use `APIHandler` from `jupyter_server.base.handlers` ### Frontend API Calls **✅ Do**: Call backend endpoints from typed API functions in `src/api.ts` (not directly in widgets): ```ts import { ServerConnection } from '@jupyterlab/services'; import { requestAPI } from './request'; interface DataResponse { status: 'success' | 'error'; data: string; } export async function fetchData( serverSettings: ServerConnection.ISettings ): Promise { try { const response = await requestAPI('data', serverSettings, { method: 'GET' }); if (response.status === 'error') { throw new Error('Server returned error'); } return response.data; } catch (err) { // Extract detailed error information from ResponseError if (err instanceof ServerConnection.ResponseError) { const status = err.response.status; let detail = err.message; // Truncate HTML responses for cleaner error messages if ( typeof detail === 'string' && (detail.includes('