# Obsidian community plugin ## Project overview - Target: Obsidian Community Plugin (TypeScript → bundled JavaScript). - Entry point: `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 - To use eslint install eslint from terminal: `npm install -g eslint` - To use eslint to analyze this project use this command: `eslint main.ts` - eslint will then create a report with suggestions for code improvement by file and line number. - If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder: `eslint ./src/` ## 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()); 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()); await this.saveData(this.settings); } ``` ### Register listeners safely ```ts this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ })); this.registerDomEvent(window, "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