9.6 KiB
9.6 KiB
Obsidian community plugin
Project overview
- Target: Obsidian Community Plugin (TypeScript → bundled JavaScript).
- Entry point:
main.tscompiled tomain.jsand loaded by Obsidian. - Required release artifacts:
main.js,manifest.json, and optionalstyles.css.
Environment & tooling
- Node.js: use current LTS (Node 18+ recommended).
- Package manager: npm (required for this sample -
package.jsondefines npm scripts and dependencies). - Bundler: esbuild (required for this sample -
esbuild.config.mjsand build scripts depend on it). Alternative bundlers like Rollup or webpack are acceptable for other projects if they bundle all external dependencies intomain.js. - Types:
obsidiantype 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
npm install
Dev (watch)
npm run dev
Production build
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/. Keepmain.tssmall 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)nameversion(Semantic Versioningx.y.z)minAppVersiondescriptionisDesktopOnly(boolean)- Optional:
author,authorUrl,fundingUrl(string or map)
- Never change
idafter release. Treat it as stable API. - Keep
minAppVersionaccurate 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:<Vault>/.obsidian/plugins/<plugin-id>/ - 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
versioninmanifest.json(SemVer) and updateversions.jsonto map plugin version → minimum app version. - Create a GitHub release whose tag exactly matches
manifest.json'sversion. Do not use a leadingv. - Attach
manifest.json,main.js, andstyles.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.mdand 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": truepreferred. - Keep
main.tsminimal: 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
isDesktopOnlyaccordingly. - Prefer
async/awaitover promise chains; handle errors gracefully.
Mobile
- Where feasible, test on iOS and Android.
- Don't assume desktop-only behavior unless
isDesktopOnlyistrue. - 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):
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:
export interface MySettings {
enabled: boolean;
apiKey: string;
}
export const DEFAULT_SETTINGS: MySettings = {
enabled: true,
apiKey: "",
};
commands/index.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
this.addCommand({
id: "your-command-id",
name: "Do the thing",
callback: () => this.doTheThing(),
});
Persist settings
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
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.jsandmanifest.jsonare at the top level of the plugin folder under<Vault>/.obsidian/plugins/<plugin-id>/. - Build issues: if
main.jsis missing, runnpm run buildornpm run devto compile your TypeScript source code. - Commands not appearing: verify
addCommandruns afteronloadand IDs are unique. - Settings not persisting: ensure
loadData/saveDataare awaited and you re-render the UI after changes. - Mobile-only issues: confirm you're not using desktop-only APIs; check
isDesktopOnlyand 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