# Obsidian community plugin ## Project overview - Target: Obsidian Community Plugin (TypeScript → bundled JavaScript). - Entry point: `src/main.ts` compiled to `main.js` and loaded by Obsidian. - Required release artifacts: `main.js`, `manifest.json`, and optional `styles.css`. ## Environment & tooling - Node.js: use current LTS (Node 18+ recommended). - **Package manager: npm** (required for this sample - `package.json` defines npm scripts and dependencies). - **Bundler: esbuild** (required for this sample - `esbuild.config.mjs` and build scripts depend on it). Alternative bundlers like Rollup or webpack are acceptable for other projects if they bundle all external dependencies into `main.js`. - Types: `obsidian` type definitions. **Note**: This sample project has specific technical dependencies on npm and esbuild. If you're creating a plugin from scratch, you can choose different tools, but you'll need to replace the build configuration accordingly. ### Install ```bash npm install ``` ### Dev (watch) ```bash npm run dev ``` ### Production build ```bash npm run build ``` ## Linting - ESLint is preconfigured with `eslint-plugin-obsidianmd` for Obsidian-specific rules. - Run `npm run lint` to lint the project. - A GitHub Action automatically lints every commit on all branches. ## File & folder conventions - **Organize code into multiple files**: Split functionality across separate modules rather than putting everything in `main.ts`. - Source lives in `src/`. Keep `main.ts` small and focused on plugin lifecycle (loading, unloading, registering commands). - **Example file structure**: ``` src/ main.ts # Plugin entry point, lifecycle management settings.ts # Settings interface and defaults commands/ # Command implementations command1.ts command2.ts ui/ # UI components, modals, views modal.ts view.ts utils/ # Utility functions, helpers helpers.ts constants.ts types.ts # TypeScript interfaces and types ``` - **Do not commit build artifacts**: Never commit `node_modules/`, `main.js`, or other generated files to version control. - Keep the plugin small. Avoid large dependencies. Prefer browser-compatible packages. - Generated output should be placed at the plugin root or `dist/` depending on your build setup. Release artifacts must end up at the top level of the plugin folder in the vault (`main.js`, `manifest.json`, `styles.css`). ## Manifest rules (`manifest.json`) - Must include (non-exhaustive): - `id` (plugin ID; for local dev it should match the folder name) - `name` - `version` (Semantic Versioning `x.y.z`) - `minAppVersion` - `description` - `isDesktopOnly` (boolean) - Optional: `author`, `authorUrl`, `fundingUrl` (string or map) - Never change `id` after release. Treat it as stable API. - Keep `minAppVersion` accurate when using newer APIs. - Canonical requirements are coded here: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml ## Testing - Manual install for testing: copy `main.js`, `manifest.json`, `styles.css` (if any) to: ``` /.obsidian/plugins// ``` - Reload Obsidian and enable the plugin in **Settings → Community plugins**. ## Commands & settings - Any user-facing commands should be added via `this.addCommand(...)`. - If the plugin has configuration, provide a settings tab and sensible defaults. - Persist settings using `this.loadData()` / `this.saveData()`. - Use stable command IDs; avoid renaming once released. ## Versioning & releases - Bump `version` in `manifest.json` (SemVer) and update `versions.json` to map plugin version → minimum app version. - Create a GitHub release whose tag exactly matches `manifest.json`'s `version`. Do not use a leading `v`. - Attach `manifest.json`, `main.js`, and `styles.css` (if present) to the release as individual assets. - After the initial release, follow the process to add/update your plugin in the community catalog as required. ## Security, privacy, and compliance Follow Obsidian's **Developer Policies** and **Plugin Guidelines**. In particular: - Default to local/offline operation. Only make network requests when essential to the feature. - No hidden telemetry. If you collect optional analytics or call third-party services, require explicit opt-in and document clearly in `README.md` and in settings. - Never execute remote code, fetch and eval scripts, or auto-update plugin code outside of normal releases. - Minimize scope: read/write only what's necessary inside the vault. Do not access files outside the vault. - Clearly disclose any external services used, data sent, and risks. - Respect user privacy. Do not collect vault contents, filenames, or personal information unless absolutely necessary and explicitly consented. - Avoid deceptive patterns, ads, or spammy notifications. - Register and clean up all DOM, app, and interval listeners using the provided `register*` helpers so the plugin unloads safely. ## UX & copy guidelines (for UI text, commands, settings) - Prefer sentence case for headings, buttons, and titles. - Use clear, action-oriented imperatives in step-by-step copy. - Use **bold** to indicate literal UI labels. Prefer "select" for interactions. - Use arrow notation for navigation: **Settings → Community plugins**. - Keep in-app strings short, consistent, and free of jargon. ## Performance - Keep startup light. Defer heavy work until needed. - Avoid long-running tasks during `onload`; use lazy initialization. - Batch disk access and avoid excessive vault scans. - Debounce/throttle expensive operations in response to file system events. ## Coding conventions - TypeScript with `"strict": true` preferred. - **Keep `main.ts` minimal**: Focus only on plugin lifecycle (onload, onunload, addCommand calls). Delegate all feature logic to separate modules. - **Split large files**: If any file exceeds ~200-300 lines, consider breaking it into smaller, focused modules. - **Use clear module boundaries**: Each file should have a single, well-defined responsibility. - Bundle everything into `main.js` (no unbundled runtime deps). - Avoid Node/Electron APIs if you want mobile compatibility; set `isDesktopOnly` accordingly. - Prefer `async/await` over promise chains; handle errors gracefully. ## Mobile - Where feasible, test on iOS and Android. - Don't assume desktop-only behavior unless `isDesktopOnly` is `true`. - Avoid large in-memory structures; be mindful of memory and storage constraints. ## Agent do/don't **Do** - Add commands with stable IDs (don't rename once released). - Provide defaults and validation in settings. - Write idempotent code paths so reload/unload doesn't leak listeners or intervals. - Use `this.register*` helpers for everything that needs cleanup. **Don't** - Introduce network calls without an obvious user-facing reason and documentation. - Ship features that require cloud services without clear disclosure and explicit opt-in. - Store or transmit vault contents unless essential and consented. ## Common tasks ### Organize code across multiple files **main.ts** (minimal, lifecycle only): ```ts import { Plugin } from 'obsidian'; import { MySettings, DEFAULT_SETTINGS } from './settings'; import { registerCommands } from './commands'; export default class MyPlugin extends Plugin { settings!: MySettings; async onload() { this.settings = Object.assign( {}, DEFAULT_SETTINGS, (await this.loadData()) as Partial, ); registerCommands(this); } } ``` **settings.ts**: ```ts export interface MySettings { enabled: boolean; apiKey: string; } export const DEFAULT_SETTINGS: MySettings = { enabled: true, apiKey: '', }; ``` **commands/index.ts**: ```ts import { Plugin } from 'obsidian'; import { doSomething } from './my-command'; export function registerCommands(plugin: Plugin) { plugin.addCommand({ id: 'do-something', name: 'Do something', callback: () => doSomething(plugin), }); } ``` ### Add a command ```ts this.addCommand({ id: 'your-command-id', name: 'Do the thing', callback: () => this.doTheThing(), }); ``` ### Persist settings ```ts interface MySettings { enabled: boolean } const DEFAULT_SETTINGS: MySettings = { enabled: true }; async onload() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial); await this.saveData(this.settings); } ``` ### Register listeners safely ```ts this.registerEvent( this.app.workspace.on('file-open', (f) => { /* ... */ }), ); this.registerDomEvent(activeWindow, 'resize', () => { /* ... */ }); this.registerInterval( window.setInterval(() => { /* ... */ }, 1000), ); ``` ## Troubleshooting - Plugin doesn't load after build: ensure `main.js` and `manifest.json` are at the top level of the plugin folder under `/.obsidian/plugins//`. - Build issues: if `main.js` is missing, run `npm run build` or `npm run dev` to compile your TypeScript source code. - Commands not appearing: verify `addCommand` runs after `onload` and IDs are unique. - Settings not persisting: ensure `loadData`/`saveData` are awaited and you re-render the UI after changes. - Mobile-only issues: confirm you're not using desktop-only APIs; check `isDesktopOnly` and adjust. ## References - Obsidian sample plugin: https://github.com/obsidianmd/obsidian-sample-plugin - API documentation: https://docs.obsidian.md - Developer policies: https://docs.obsidian.md/Developer+policies - Plugin guidelines: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines - Style guide: https://help.obsidian.md/style-guide