Skip to content

Optional "AI rules" scaffolding in Copier #110

@ktaletsk

Description

@ktaletsk

Propose adding an optional Copier question that, when enabled, scaffolds a small, vendor-neutral set of AI assistant guidelines for extension development ("AI rules"). If the user opts in, the template also drops tool-specific helper files (e.g., .cursorrules, CLAUDE.md) to improve the out-of-the-box experience with popular AI pair-programmers without coupling the template to any vendor.

Problem

Many extension authors now use AI assistants when starting from this template. A light set of rules (naming, typing, no-any, no console.log, API patterns, IDs) nudges assistants toward community-aligned code and reduces review churn. Today, every team re-creates similar rules ad hoc.

Proposed Solution

  1. New Copier question (default = No):
    • Prompt: "Include AI assistant rules and helper files?"
    • Default: false to keep the current behavior and avoid vendor implications.
  2. When true, scaffold:
    • Vendor-neutral doc: AI_RULES.md (concise coding standards aligned with JupyterLab examples: typed TS, consistent plugin/command IDs, RESTful server handlers, no console.log, no any, error handling, CSS namespacing, etc.).
    • Tool-specific helpers (optional files generated by the same toggle):
      • .cursorrules — minimal JSON/YAML with enforceable patterns the Cursor ecosystem understands (e.g., disallow console.log, : any).
      • CLAUDE.md — prompt/guidance file with the same rules phrased for Claude (or any LLM that reads a project “guide” file).
      • README snippet: one short paragraph linking to AI_RULES.md and explaining the optional files; explicitly vendor-neutral.
  3. No runtime dependencies. These are docs/config files only; they do not alter build, packaging, or test behavior.

Additional context

Example rule

template/.cursor/rules/jupyterlab-extension.mdc.jinja

---
description: JupyterLab Extension Development – coding standards, naming, integration, and project alignment
globs: ["*.py", "*.ts", "*.tsx", "*.js", "*.jsx", "*.json", "*.md", "pyproject.toml", "package.json"]
alwaysApply: true
---

### JupyterLab Extension Development – Project Rules

These rules guide AI-generated code to align with JupyterLab community standards and keep the extension maintainable. They adapt based on the selected extension kind.

Kind selected: {{ kind }}

## Enforceable Rules

- **No `console.log`**: Avoid `console.log` in favor of structured logging or user-facing notifications.
- **No `any` type**: Do not use the `any` type in TypeScript; define explicit interfaces or types instead.

```json
{
  "rules": [
    {
      "name": "no-console-log",
      "description": "Disallow console.log statements in TypeScript/JavaScript files.",
      "globs": ["*.ts", "*.tsx", "*.js", "*.jsx"],
      "prohibitedPatterns": ["console.log"]
    },
    {
      "name": "no-any-type",
      "description": "Disallow the `any` type in TypeScript files.",
      "globs": ["*.ts", "*.tsx"],
      "prohibitedPatterns": [": any", " = any"]
    }
  ]
}
```

## Coding Standards

{% if kind == 'frontend-and-server' %}- **Python (PEP 8)**: Use 4-space indentation and meaningful names. Class names use CamelCase; functions, variables, and methods use lowercase_with_underscores. Private/internal names use a single leading underscore. Align with Jupyter’s coding style guidelines.
{% endif %}- **TypeScript/JavaScript**: Use PascalCase for class and interface names (e.g., `MyPanelWidget`) and camelCase for functions, methods, and variables. Avoid `any` whenever possible—prefer explicit types. Use Prettier/ESLint defaults (2-space indent, etc.) for clean, uniform formatting.
- **Descriptive naming and comments**: Choose clear, descriptive names for classes, commands, and modules (e.g., `DataUploadHandler` instead of `MyHandler`). Include JSDoc for TS and{% if kind == 'frontend-and-server' %} docstrings for Python{% endif %} to describe the purpose of modules, classes, and complex functions.
- **No unused or duplicate code**: {% if kind == 'frontend-and-server' %}Avoid duplicating logic across front and back ends. Centralize processing when possible and pass data across the boundary. {% endif %}Do not produce dead code or leave TODOs—implement features fully or not at all.

## Naming and Project Structure

{% if kind == 'frontend-and-server' %}- **Python package names**: Use short, all-lowercase names without dashes. Use underscores if needed (e.g., `jupyterlab_myext`). For the PyPI distribution name, using a dash is acceptable/common (e.g., `jupyterlab-myext`) as long as the importable Python module uses underscores.
{% endif %}- **Frontend package name**: Use the same base name as the project for the npm package (e.g., `"jupyterlab-myext"` or `@<organization>/myext`). Keep naming consistent to avoid confusion.
- **Plugin and command IDs**: Prefix with your extension name. Example plugin id: `'<your-ext-name>:plugin'`; command IDs like `'<your-ext-name>:do-action'`. Keep IDs lowercase; use hyphens or camelCase for multi-word actions (e.g., `myext:open-file`).
- **File and class names**: Name sources after their function. For a React widget, `MyWidget.tsx` containing class `MyWidget` is appropriate. Avoid catch-all files like `utils.ts`; partition logically (e.g., `api.ts` for API calls{% if kind == 'frontend-and-server' %}, `handlers.py` for Tornado handlers{% endif %}). Organize frontend under `src/`{% if kind == 'frontend-and-server' %} and Python code in the package directory{% endif %}.

{% if kind == 'theme' %}## Theme Extensions

- **No backend**: Theme extensions are frontend-only. Keep all assets under `style/`.
- **CSS organization**: Use `style/variables.css` for custom properties and `style/index.css` for imports and global theme selectors. Avoid global resets; extend JupyterLab tokens.
- **Support light and dark**: Scope colors with attribute selectors like `[data-jp-theme-light='true']` and `[data-jp-theme-light='false']`.
- **Register the theme**: Register via `IThemeManager` and load CSS from `style/index.css`.

```ts
import { IThemeManager } from '@jupyterlab/apputils';
import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application';

const plugin: JupyterFrontEndPlugin<void> = {
  id: '<your-ext-name>:theme',
  autoStart: true,
  requires: [IThemeManager],
  activate: (app: JupyterFrontEnd, manager: IThemeManager) => {
    const style = 'style/index.css';
    manager.register({
      name: '<Human Theme Name>',
      isLight: true, // set appropriately
      load: () => manager.loadCSS(style),
      unload: () => Promise.resolve()
    });
  }
};

export default plugin;
```

- **Use CSS variables**: Prefer CSS custom properties to ensure compatibility with other extensions and JupyterLab updates.
{% endif %}

{% if kind == 'mimerender' %}## MIME Renderer Extensions

- **No backend**: MIME renderers are frontend-only. Implement an `IRenderMime.IExtension` to render a MIME type.
- **Renderer factory**: Provide `mimeTypes`, `dataType` (`'json' | 'string' | 'object'`), and `safe` indicating whether the renderer is safe for untrusted content.
- **Widget implementation**: Extend `Widget` and implement `renderModel`.

```ts
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
import { Widget } from '@lumino/widgets';

class MyRenderer extends Widget implements IRenderMime.IRenderer {
  constructor(options: IRenderMime.IRendererOptions) {
    super();
    this.addClass('myext-Renderer');
  }
  async renderModel(model: IRenderMime.IMimeModel): Promise<void> {
    const data = model.data['text/plain'] as string;
    this.node.textContent = data;
  }
}

const extension: IRenderMime.IExtension = {
  id: '<your-ext-name>:renderer',
  rendererFactory: {
    safe: true,
    mimeTypes: ['text/plain'],
    createRenderer: opts => new MyRenderer(opts)
  },
  rank: 50,
  dataType: 'string'
};

export default extension;
```

- **Security**: If using HTML, sanitize before injecting. Set `safe: false` only when necessary and handle untrusted content carefully.
- **Styling**: Namespace CSS classes (e.g., `myext-*`). Avoid mutating global styles.
- **Performance**: Keep `renderModel` fast; avoid blocking the main thread. Use async operations where appropriate.
{% endif %}

{% if kind == 'frontend-and-server' %}## Backend–Frontend Integration

- **RESTful endpoints with Tornado**: Create a handler extending Jupyter Server’s `APIHandler` and register routes on startup.

```python
from jupyter_server.base.handlers import APIHandler
from jupyter_server.utils import url_path_join

class HelloWorldHandler(APIHandler):
    def get(self):
        self.finish({"data": "Hello, world!"})

def setup_handlers(web_app):
    host_pattern = r".*$"
    base_url = web_app.settings.get("base_url", "/")
    route_pattern = url_path_join(base_url, "myext", "hello")
    web_app.add_handlers(host_pattern, [(route_pattern, HelloWorldHandler)])
```

- **HTTP methods**: `GET` for retrieval, `POST`/`PUT` for actions/updates. Use `self.get_json_body()` or `self.get_body_argument()` to read request data.
- **Frontend calls via `requestAPI`**: Call your server endpoints from TypeScript.

```ts
import { requestAPI } from './handler';

interface HelloResponse { 
  data: string;
  status?: 'success' | 'error';
}

export async function fetchHello(): Promise<string> {
  try {
    const response = await requestAPI<HelloResponse>('hello', { 
      method: 'GET' 
    });
    if (response.status === 'error') {
      throw new Error('Server returned error status');
    }
    return response.data;
  } catch (err) {
    // Re-throw the error to allow the caller to handle it,
    // optionally wrapping it in a custom error type.
    throw new Error(`API request failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
  }
}
```

- **Keep back and front in sync**: When server responses change, update TS types and UI accordingly. Avoid unused routes and calls to nonexistent endpoints.
- **Consistent JSON naming**: Use the same keys on both sides (e.g., `{"result": ...}` ↔ `response.result`). Consider shared constants for routes and command names.
{% endif %}

## Development builds

{% set is_full_stack = kind|lower == 'frontend-and-server' %}

### Setup (run once)
```bash
pip install -e ".{% if test and is_full_stack %}[test]{% endif %}" && jupyter labextension develop . --overwrite{% if is_full_stack %} && jupyter server extension enable {{ python_name }}{% endif %} && jlpm build
```

### Rebuild during development
```bash
# Frontend changes
jlpm build

{% if is_full_stack %}# Backend (Python) changes
pip install -e .
{% endif %}
```

### Debug
```bash
jupyter labextension list && jupyter server extension list
```

### Clean / Reset (if things get out of sync)
```bash
jlpm clean && jlpm build
jupyter labextension develop . --overwrite
{% if is_full_stack %}pip install -e .{% endif %}
```

Notes & Tips:
- Run commands inside the same environment where JupyterLab is installed (conda/mamba/venv). If `jlpm` is not found, verify your environment.
- Prefer `jlpm` (JupyterLab’s pinned Yarn). Do not mix `yarn.lock` with `package-lock.json` to prevent resolution issues.
- Restart JupyterLab if changes don’t appear (`Ctrl+C` then `jupyter lab`).

## Best Practices and Template Alignment

- **Leverage the project structure**: Keep changes aligned with the project’s scaffold. Do not rename or move core files without updating configuration. {% if kind == 'frontend-and-server' %}Use `handlers.py` for API handlers and the provided server setup.{% else %}Place UI logic in `src/` and organize modules by feature.{% endif %}
- **Version consistency**: {% if kind == 'frontend-and-server' %}Keep Python package and npm package versions in sync for releases.{% else %}Maintain semantic versioning in `package.json` and update consistently across release artifacts.{% endif %}
- **Follow community examples**: Start minimal (e.g., “Server Hello World” for combined extensions or simple UI command for frontend-only) and iterate. Use JupyterLab APIs and patterns shown in official examples.
- **Thorough testing**: Exercise features in a running JupyterLab. Use browser console{% if kind == 'frontend-and-server' %} and server logs{% endif %} for debugging. Add unit/integration tests for non-trivial features.

## Common Pitfalls to Avoid

- **Mixing package managers**: Use only `jlpm`/`yarn` or only `npm`. Don't mix `yarn.lock` and `package-lock.json`.
- **Hardcoded paths**: Use `url_path_join()` for server routes and relative imports for frontend modules.
- **Missing error handling**: Always wrap API calls in try-catch blocks and provide user feedback.
- **CSS conflicts**: Namespace all CSS classes with your extension name (e.g., `.myext-widget`).
- **Memory leaks**: Dispose of widgets and disconnect signals properly in `dispose()` methods.{% if kind == 'frontend-and-server' %}
- **CORS issues**: Ensure server handlers inherit from `APIHandler` for proper CORS handling.
- **Python import errors**: Use relative imports within your package; absolute imports for external dependencies.{% endif %}

### Quick Reference

Refer to the commands in the "Development builds" section above.

**Key Patterns:**
- Plugin ID: `'{{ python_name }}:plugin'`
- Command ID: `'{{ python_name }}:command-name'`
- CSS classes: `.{{ python_name | replace('_', '-') }}-ClassName`

Backwards compatibility

  • No change for users who don’t opt in.
  • Purely additive files when enabled.

Vendor neutrality

  • The question and docs are vendor-neutral.
  • Tool-specific files are optional artifacts emitted by the same toggle to reduce friction for users who already rely on those assistants.
  • We do not maintain vendor integrations; we only ship small text files.

Alternatives considered

  • Host the rules in a separate “starter-rules” repo and link from the template (adds friction).
  • Add only AI_RULES.md and omit tool-specific files (lower immediate utility for common assistants).

Open questions for maintainers

  • Preferred filenames and placement (AI_RULES.md at root vs docs/).
  • Scope/length of the neutral rules (keep to one pager?).
  • Which tool files to include initially (Cursor and Claude?), and how to frame this in README to stay neutral.
  • Testing/documentation expectations (a small unit in the docs site, or README-only?).

Acceptance criteria (proposal)

  • New Copier question added (default false)
  • AI_RULES.md generated when true
  • Optional .cursorrules and CLAUDE.md generated when true
  • README gains a 2–3 sentence section linking to AI_RULES.md
  • No changes to build/test/publish flows when disabled or enabled

Prior art / Reference

  • Example fork implementing this approach (Cursor rules + neutral guidance): orbrx/extension-template — fork of this repo with AI rules added to the template

If the direction looks good, I can submit a PR that:

  • Adds the Copier question + conditional file templates
  • Contributes a concise AI_RULES.md
  • Wires a short README blurb

Thanks for considering!

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions