---
description: Obsidian plugin API conventions
globs: ["src/**/*.ts", "main.ts", "manifest.json", "styles.css"]
alwaysApply: false
---

# Obsidian plugin

## Commands
`npm run dev` (esbuild watch into the test vault's `.obsidian/plugins/<id>/`) · `npm run build` (typecheck + production bundle) · `npm run lint` if present.

## Lifecycle
- The plugin class extends `Plugin`. Register everything in `onload()` through the helpers that clean up automatically: `this.addCommand`, `this.addRibbonIcon`, `this.addSettingTab`, `this.registerEvent(this.app.vault.on(...))`, `this.registerDomEvent`, `this.registerInterval(window.setInterval(...))`, `this.registerView`.
- Anything not registered that way must be undone in `onunload()`. Disabling the plugin must leave no listeners, intervals or DOM behind.
- Defer heavy startup work with `this.app.workspace.onLayoutReady(...)` so vault load is not slowed.

## APIs
- Files: use `this.app.vault` (`read`, `cachedRead`, `process` for atomic edits, `create`) and `this.app.fileManager` (`renameFile`, `processFrontMatter`). Don't use Node `fs` or the adapter directly; plugins must work on mobile.
- Frontmatter and links come from `this.app.metadataCache`; don't regex-parse YAML.
- Paths through `normalizePath()`. Network requests with `requestUrl` (no CORS issues), not `fetch` to arbitrary hosts.
- Never use `innerHTML`/`outerHTML` with user or note content. Build DOM with `createEl`/`createDiv`/`setText`, or render Markdown with `MarkdownRenderer.render(...)` and a component.
- Get the active editor via `this.app.workspace.getActiveViewOfType(MarkdownView)`; don't store references to leaves or views across events.

## Settings
`loadSettings()` merges `Object.assign({}, DEFAULT_SETTINGS, await this.loadData())`; `saveSettings()` calls `this.saveData`. Settings UI uses `new Setting(containerEl)`.

## Release
Keep `manifest.json` (`id`, `version`, `minAppVersion`) and `versions.json` in sync; a release ships `main.js`, `manifest.json` and `styles.css`. Don't add the word "Obsidian" to the plugin ID, and don't bump `minAppVersion` without reason.
