dev: initial implementation of notebook-snapshot extension
Manual-commit notebook snapshot extension (JupyterLab 4):
Backend
- Storage adapter layer (Local + S3 via boto3, pip extra)
- Dumb put/get/list/delete contract; key validation
- S3Storage: lazy boto3 import, S3ConnectionError with friendly
messages + connectivity check at startup
- SnapshotStore
- Strip code outputs/execution_count, MD5 on cleaned JSON
- Gzipped envelope (id/timestamp/name/description/hash/size/notebook)
- Append-only manifest.json (rebuildable from version files)
- SnapshotUnchangedError when new hash matches most recent version
- REST API (commit / list / content) under /snapshot/ namespace
- traitlets config (storage_type, local_root, s3_* + S3_* env fallback)
Frontend
- snapshot:commit command (toolbar button + command palette)
- Dialog for name + description
- SnapshotPanel (left sidebar timeline) + DiffWidget (main area)
- Cell diff: id-match first, LCS fallback; jsdiff for line diff
- Restore via model.fromJSON() + context.save() (no refresh)
- All AGENTS.md hard conventions enforced
Tests: 33 backend pytest passing (storage + store + routes)
Docs: AGENTS.md, design.md, README.md synced with implementation.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,815 @@
|
||||
# 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<void> = {
|
||||
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<string> {
|
||||
try {
|
||||
const response = await requestAPI<DataResponse>('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('<!DOCTYPE') || detail.includes('<html'))
|
||||
) {
|
||||
detail = `HTML error page (${detail.substring(0, 100)}...)`;
|
||||
}
|
||||
|
||||
throw new Error(`API request failed (${status}): ${detail}`);
|
||||
}
|
||||
|
||||
const msg = err instanceof Error ? err.message : 'Unknown error';
|
||||
throw new Error(`API request failed: ${msg}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Do**:
|
||||
|
||||
- Always wrap API calls in try-catch blocks with proper error handling
|
||||
- Check for `ServerConnection.ResponseError` to extract HTTP status codes and response details
|
||||
- Handle HTML error responses gracefully by truncating them (they're often unhelpful error pages)
|
||||
- Include response status codes in error messages for better debugging
|
||||
- Use matching response types between Python and TypeScript
|
||||
- Create typed API wrapper functions in `src/api.ts` instead of calling `requestAPI()` directly from widgets
|
||||
|
||||
### API Sync and Naming
|
||||
|
||||
**✅ Do**: Keep backend and frontend in sync
|
||||
|
||||
- Match JSON keys: `{"result": ...}` in Python → `response.result` in TypeScript
|
||||
- Update TypeScript interfaces when changing Python responses
|
||||
- Define matching endpoint path strings (e.g., `"hello"`, `"get-data"`) in both `snapshot/routes.py` and `src/api.ts` to ensure routes sync between backend and frontend
|
||||
|
||||
**❌ Don't**:
|
||||
|
||||
- Create unused routes or orphaned API calls
|
||||
- Use inconsistent field naming across languages
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Environment Activation (CRITICAL)
|
||||
|
||||
**Before ANY command**, ensure you're in the correct environment. This repository uses a uv-managed venv at `.venv/` (see [Environment](#environment) above):
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate # macOS/Linux (this repo)
|
||||
```
|
||||
|
||||
**All `jlpm`, `pip`, and `jupyter` commands MUST run within the activated environment.**
|
||||
|
||||
**Symptoms of running outside the environment:**
|
||||
|
||||
- `jlpm: command not found`
|
||||
- Extension not appearing after build
|
||||
- `jupyter: command not found`
|
||||
|
||||
**✅ Do**: Always activate your environment first
|
||||
**❌ Don't**: Run commands in your base/system environment
|
||||
|
||||
---
|
||||
|
||||
### Complete Development Workflow Checklist
|
||||
|
||||
**When implementing a new feature from scratch, follow this complete sequence:**
|
||||
|
||||
1. **Activate environment** (see above — required first!)
|
||||
2. **Write the code** (TypeScript in `src/`, styles in `style/`, Python in `snapshot/`)
|
||||
3. **Install dependencies** (if you added any to `package.json`):
|
||||
```bash
|
||||
jlpm install
|
||||
```
|
||||
4. **Build the extension**:
|
||||
```bash
|
||||
jlpm build
|
||||
```
|
||||
5. **Install the extension** (REQUIRED for JupyterLab to recognize it):
|
||||
```bash
|
||||
pip install -e .
|
||||
jupyter-builder develop . --overwrite
|
||||
jupyter server extension enable snapshot
|
||||
```
|
||||
6. **Verify installation**:
|
||||
```bash
|
||||
jupyter labextension list # Should show your extension as "enabled" and "OK"
|
||||
jupyter server extension list # Should show backend extension
|
||||
```
|
||||
7. **Start JupyterLab**:
|
||||
```bash
|
||||
jupyter lab
|
||||
```
|
||||
8. **Test the feature** in your browser
|
||||
|
||||
**Critical: Steps 5-7 are REQUIRED after building. Building alone is not enough!**
|
||||
|
||||
---
|
||||
|
||||
### Understanding Build vs Install
|
||||
|
||||
Many issues arise from confusing these two steps:
|
||||
|
||||
#### `jlpm build` — Compiles the Extension. Do this every time you change TypeScript code.
|
||||
|
||||
- **What it does**: Compiles TypeScript → JavaScript, bundles the extension
|
||||
- **Output**: Creates files in `lib/` and `snapshot/labextension/`
|
||||
- **What it does NOT do**: Register the extension with JupyterLab
|
||||
|
||||
#### `pip install -e .` + `jupyter-builder develop .` — Registers the Extension. Do this once as a setup step.
|
||||
|
||||
- **What it does**: Tells JupyterLab where to find your extension
|
||||
- **Output**: Creates symlinks so changes are reflected
|
||||
- **Note**: Also installs the Python package in editable mode
|
||||
- **Result**: Extension appears in JupyterLab
|
||||
|
||||
**You need BOTH steps!** Building prepares the code; installing registers it with JupyterLab.
|
||||
|
||||
**Common mistake**: Running only `jlpm build` and expecting the extension to appear. It won't show up until you also run the installation commands.
|
||||
|
||||
---
|
||||
|
||||
### Initial Setup (run once)
|
||||
|
||||
```bash
|
||||
pip install -e ".[dev,test]"
|
||||
jupyter-builder develop . --overwrite
|
||||
jupyter server extension enable snapshot
|
||||
```
|
||||
|
||||
### Iterative Development
|
||||
|
||||
**Development with auto-rebuild** (recommended):
|
||||
|
||||
```bash
|
||||
jlpm run watch # Auto-rebuild on file changes (keep running)
|
||||
# In another terminal:
|
||||
jupyter lab
|
||||
```
|
||||
|
||||
**After editing TypeScript** (files in `src/`):
|
||||
|
||||
- If using `jlpm run watch`: Just **refresh your browser** (Cmd+R / Ctrl+R)
|
||||
- If not using watch: Run `jlpm build`, then **refresh your browser**
|
||||
|
||||
**Quick TypeScript validation** (optional, for fast feedback):
|
||||
|
||||
```bash
|
||||
npx tsc --noEmit src/index.ts # Check single file
|
||||
```
|
||||
|
||||
**After editing Python** (files in `snapshot/`):
|
||||
|
||||
- **Restart the JupyterLab server** (Ctrl+C in terminal, then `jupyter lab` again)
|
||||
- No rebuild needed!
|
||||
- Only run `pip install -e .` if you changed package structure (renamed package directory, or modified entry points in `pyproject.toml`)
|
||||
|
||||
**Memory aid**: "What did you change? Restart that!"
|
||||
|
||||
- Changed **JavaScript** → Build (or auto-builds with watch) → **Refresh browser**
|
||||
- Changed **Python** → **Restart JupyterLab server** (no build needed)
|
||||
|
||||
### Debugging and Diagnostics
|
||||
|
||||
```bash
|
||||
jupyter labextension list # Check if extension is installed
|
||||
jupyter server extension list # Check backend extension
|
||||
jlpm run lint # Lint frontend code
|
||||
```
|
||||
|
||||
**Browser console** (ask user to check):
|
||||
|
||||
- Request user to open browser console (F12 or Cmd+Option+I)
|
||||
- Ask user to report any JavaScript errors
|
||||
- Ask user to check for failed network requests to backend endpoints
|
||||
- Ask user if the extension appears to be loaded
|
||||
|
||||
**Server logs** (terminal running `jupyter lab`):
|
||||
|
||||
- Check for Python errors or exceptions
|
||||
- Verify backend routes are registered
|
||||
- Look for HTTP request logs
|
||||
|
||||
---
|
||||
|
||||
### Troubleshooting: Extension Not Appearing
|
||||
|
||||
If your extension doesn't appear in JupyterLab after building:
|
||||
|
||||
**1. Check if the extension is installed:**
|
||||
|
||||
```bash
|
||||
jupyter labextension list
|
||||
```
|
||||
|
||||
Your extension should appear as **"enabled"** and **"OK"**.
|
||||
|
||||
**2. If NOT in the list**, run the installation commands:
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
jupyter-builder develop . --overwrite
|
||||
jupyter server extension enable snapshot
|
||||
```
|
||||
|
||||
**3. Did you restart JupyterLab?**
|
||||
|
||||
- Changes require a full restart (Ctrl+C in terminal, then `jupyter lab` again)
|
||||
- Simply refreshing the browser is NOT enough for new extensions
|
||||
|
||||
**4. Ask user to check the browser console** (F12 or Cmd+Option+I):
|
||||
|
||||
- Request user to look for JavaScript errors that might prevent extension activation
|
||||
- Ask user to search for the extension ID (`snapshot`) to see if it loaded
|
||||
- Ask user to report any error messages or warnings
|
||||
|
||||
**5. Verify the build output:**
|
||||
|
||||
```bash
|
||||
ls -la lib/ # Should contain compiled .js files
|
||||
ls -la snapshot/labextension/ # Should contain bundled extension
|
||||
```
|
||||
|
||||
**6. If still not working**, try a clean rebuild following the reset instructions below
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- ❌ Only ran `jlpm build` without installation commands
|
||||
- ❌ Forgot to restart JupyterLab after installation
|
||||
- ❌ Running commands outside the activated environment
|
||||
- ❌ Build errors that were missed (check terminal output)
|
||||
|
||||
### Reset (if build state is broken)
|
||||
|
||||
```bash
|
||||
jlpm clean:all # Clean build artifacts
|
||||
# git clean -fdX # (Optional) Remove all ignored files including node_modules
|
||||
jlpm install # Only needed if you used 'git clean -fdX'
|
||||
jlpm build
|
||||
pip install -e ".[dev,test]"
|
||||
jupyter-builder develop . --overwrite
|
||||
jupyter server extension enable snapshot
|
||||
```
|
||||
|
||||
### Environment Notes
|
||||
|
||||
**✅ Do**: Use a virtual environment (conda/mamba/micromamba/venv)
|
||||
**✅ Do**: Use `jlpm` exclusively
|
||||
**❌ Don't**: Mix package managers (`npm`, `yarn`) with `jlpm`
|
||||
**❌ Don't**: Mix lockfiles — keep only `yarn.lock`, not `package-lock.json`
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Project Structure Alignment
|
||||
|
||||
**✅ Do**: Follow the template structure
|
||||
|
||||
- Keep configuration files in project root: `package.json`, `pyproject.toml`, `tsconfig.json`
|
||||
- Backend routes: `snapshot/routes.py`
|
||||
- Server extension config: `jupyter-config/server-config/snapshot.json`
|
||||
- Frontend code: `src/index.ts` and other `src/` files
|
||||
- Styles: `style/index.css`
|
||||
- Settings schema: `schema/plugin.json`
|
||||
|
||||
**❌ Don't**: Rename or move core files without updating all references in configuration
|
||||
|
||||
### Version Management
|
||||
|
||||
**✅ Do**: Update version in `package.json` only
|
||||
|
||||
- The `package.json` version is the source of truth
|
||||
- `pyproject.toml` automatically syncs from `package.json` via `hatch-nodejs-version`
|
||||
- Follow semantic versioning: MAJOR.MINOR.PATCH
|
||||
|
||||
**❌ Don't**: Manually edit version in `pyproject.toml` — it's dynamically sourced from `package.json`
|
||||
|
||||
**Note**: Releases are handled by GitHub Actions, not manually. AI agents should only update versions when explicitly requested by the user.
|
||||
|
||||
### Development Approach
|
||||
|
||||
**✅ Do**: Start simple and iterate
|
||||
|
||||
- Begin with minimal functionality (e.g., a single command or widget)
|
||||
- **When integrating backend/frontend**: See [Integration Workflow](#integration-workflow-critical) for the correct order
|
||||
- Add backend routes or verbs only when frontend needs them
|
||||
- Test in running JupyterLab frequently
|
||||
- Ask user to check browser console and review terminal logs for errors
|
||||
|
||||
**❌ Don't**: Build complex features without incremental testing
|
||||
|
||||
**❌ Don't**: Write frontend interfaces without first checking the backend API contract in `snapshot/routes.py`
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Package Management
|
||||
|
||||
**✅ Do**: Use `jlpm` consistently
|
||||
|
||||
```bash
|
||||
jlpm install
|
||||
jlpm build
|
||||
```
|
||||
|
||||
**❌ Don't**: Mix package managers or lockfiles
|
||||
|
||||
- Don't use `package-lock.json` (this project uses `yarn.lock`)
|
||||
- Don't run `npm install`
|
||||
|
||||
### Path Handling
|
||||
|
||||
**✅ Do**: Use relative imports in TypeScript (`src/` files)
|
||||
|
||||
```typescript
|
||||
import { MyWidget } from './widgets/MyWidget';
|
||||
```
|
||||
|
||||
**❌ Don't**: Use absolute paths or assume specific directory structures
|
||||
|
||||
### Error Handling
|
||||
|
||||
**✅ Do**: Wrap async operations in try-catch (in `src/api.ts`, widget code)
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const data = await fetchData();
|
||||
} catch (err) {
|
||||
showErrorMessage('Failed to fetch data');
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Don't**: Let errors propagate silently or crash the extension
|
||||
|
||||
### CSS and Styling
|
||||
|
||||
**✅ Do**: Namespace all CSS in `style/index.css`
|
||||
|
||||
```css
|
||||
.jp-snapshot-widget {
|
||||
padding: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Don't**: Use generic class names like `.widget` or `.button`
|
||||
|
||||
### Resource Cleanup
|
||||
|
||||
**✅ Do**: Dispose resources in widget `dispose()` methods
|
||||
|
||||
```typescript
|
||||
dispose(): void {
|
||||
this._signal.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Don't**: Leave event listeners or signal connections active after disposal
|
||||
|
||||
### Backend Integration
|
||||
|
||||
**✅ Do**: Use relative imports within your package
|
||||
|
||||
```python
|
||||
from .routes import setup_route_handlers
|
||||
```
|
||||
|
||||
**❌ Don't**: Use absolute imports like `from snapshot.routes import ...`
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Key Identifiers
|
||||
|
||||
Use these patterns consistently throughout your code:
|
||||
|
||||
- **Plugin ID** (in `src/index.ts`): `'snapshot:plugin'`
|
||||
- **Command IDs** (in `src/commands.ts` or `src/index.ts`): `'snapshot:command-name'`
|
||||
- For multiple commands, create `src/commands.ts` with a centralized `COMMANDS` mapping
|
||||
- For 1-2 commands, define directly in `src/index.ts`
|
||||
- **CSS classes** (in `style/index.css`): `.jp-snapshot-ClassName`
|
||||
- **API routes** (in `snapshot/routes.py`): `url_path_join(base_url, "snapshot", "endpoint")`
|
||||
|
||||
### Essential Commands
|
||||
|
||||
See [Development Workflow](#development-workflow) section for full command reference.
|
||||
Reference in New Issue
Block a user