mirror of
https://github.com/rmccorkl/TubeSage.git
synced 2026-07-22 06:45:31 +00:00
Collapsed history snapshot — represents the cumulative state of TubeSage through 1.2.21. Earlier commit history (covering versions 1.2.0 through 1.2.20) was rewritten into this single root commit to remove tracking of a personal deploy script that referenced local filesystem paths. The current release content is unchanged. See manifest.json for the plugin version. Older tags (1.2.0 through 1.2.20) have been retired. Features in this release line: - YouTube transcript extraction (ScrapeCreators API + local fallbacks) - LLM-driven summarisation (OpenAI, Anthropic, Google, Ollama) - Per-provider always-visible API key rows - Selected-provider model controls with registry-aware overrides - Custom-model parameter overrides with synchronous panel refresh - Max tokens field with blur-commit and reset-on-blank - Settings de-pollution migration on plugin load - Summary-callout split for multi-section model output - Security patches: fast-xml-parser >=5.7.0, uuid >=14.0.0
13 KiB
13 KiB
Comprehensive Obsidian Plugin Development Guidelines
Retrieved from Obsidian Developer Documentation via Context7
Core Development Principles
1. App Instance Management
NEVER use the global app object:
// ❌ Bad - Avoid global app
const globalApp = app;
// ✅ Good - Use plugin instance
class MyPlugin extends Plugin {
onload() {
this.app.vault.read("path/to/file.md");
}
}
2. Resource Management
Always clean up resources on unload:
export default class MyPlugin extends Plugin {
onload() {
// Use registerEvent for automatic cleanup
this.registerEvent(this.app.vault.on('create', this.onCreate));
}
onCreate: (file: TAbstractFile) => {
// Handle file creation
}
}
3. Security Best Practices
Never use innerHTML for user input:
// ❌ Vulnerable to XSS
function showNameUnsafe(name: string) {
let containerElement = document.querySelector('.my-container');
containerElement.innerHTML = `<div class="my-class"><b>Your name is: </b>${name}</div>`;
}
// ✅ Safe DOM construction
function showNameSecure(name: string) {
let containerElement = document.querySelector('.my-container');
if (containerElement) {
containerElement.empty();
const div = containerElement.createDiv({ cls: "my-class" });
div.createEl("b", { text: "Your name is: " });
div.createSpan({ text: name });
}
}
File System Operations
Prefer Vault API Over Adapter API
// ✅ Good - Use Vault API
this.app.vault.create('path/to/new-file.md', 'content');
this.app.vault.read(file);
// ❌ Bad - Direct Adapter API
this.app.vault.adapter.write('path/to/file', 'content');
Optimize File Access
// ❌ Inefficient - Don't iterate all files
this.app.vault.getFiles().find(file => file.path === filePath);
// ✅ Efficient - Direct access
const file = this.app.vault.getFileByPath(filePath);
const folder = this.app.vault.getFolderByPath(folderPath);
const abstractFile = this.app.vault.getAbstractFileByPath(filePath);
// Type checking
if (file instanceof TFile) {
// it's a file
}
if (file instanceof TFolder) {
// it's a folder
}
Handle Frontmatter Properly
// ✅ Use FileManager.processFrontMatter
// Atomic operation, avoids conflicts with other plugins
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
frontmatter.title = 'New Title';
});
File Editing Best Practices
// ✅ For active files - Use Editor API (preserves cursor position)
const editor = this.app.workspace.activeEditor?.editor;
if (editor) {
editor.replaceRange('new text', { line: 0, ch: 0 });
}
// ✅ For background files - Use Vault.process (atomic)
await this.app.vault.process(file, (content) => {
return content.replace(/old/g, 'new');
});
Network Operations
Use Obsidian's requestUrl
// ✅ Good - Cross-platform compatible
import { requestUrl } from 'obsidian';
async function fetchData(url: string) {
try {
const response = await requestUrl(url);
console.log(response.json);
} catch (error) {
console.error('Request failed:', error);
}
}
// ❌ Bad - Won't work on mobile
// fetch(url).then(...)
// axios.get(url).then(...)
Path Handling
Always Normalize User Paths
import { normalizePath } from 'obsidian';
// ✅ Safe path handling
const pathToPlugin = normalizePath('//my-folder\\file');
// Result: "my-folder/file" not "//my-folder\"
Async Operations
Use async/await Over Promises
// ❌ Hard to read Promise chains
function test(): Promise<string | null> {
return requestUrl('https://example.com')
.then(res => res.text)
.catch(e => {
console.log(e);
return null;
});
}
// ✅ Clean async/await
async function AsyncTest(): Promise<string | null> {
try {
let res = await requestUrl('https://example.com');
let text = await res.text;
return text;
} catch (e) {
console.log(e);
return null;
}
}
Workspace and View Management
Access Views Safely
// ✅ Safe view access
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
// Work with view
}
const editor = this.app.workspace.activeEditor?.editor;
if (editor) {
// Work with editor
}
Handle Custom Views Properly
// ❌ Bad - Storing view references
this.registerView(MY_VIEW_TYPE, () => this.view = new MyCustomView());
// ✅ Good - Factory function
this.registerView(MY_VIEW_TYPE, () => new MyCustomView());
// ✅ Access views when needed
for (let leaf of app.workspace.getActiveLeavesOfType(MY_VIEW_TYPE)) {
let view = leaf.view;
if (view instanceof MyCustomView) {
// Work with view
}
}
Handle Deferred Views (Obsidian 1.7.2+)
// ✅ Good - Safe instanceof check
workspace.iterateAllLeaves(leaf => {
if (leaf.view instanceof MyCustomView) {
// View is fully loaded
}
});
// ❌ Bad - Unsafe type assertion
workspace.iterateAllLeaves(leaf => {
if (leaf.view.getViewType() === 'my-view') {
let view = leaf.view as MyCustomView; // Dangerous!
}
});
Plugin Data Management
Use Plugin Data Methods
// ✅ Good - Use built-in methods
export default class MyPlugin extends Plugin {
async onload() {
const myData = await this.loadData();
if (myData) {
console.log('Loaded data:', myData);
}
}
async saveMyData(data: any) {
await this.saveData(data);
}
}
// ❌ Bad - Manual file management
// fs.writeFileSync(this.manifest.dir + '/data.json', JSON.stringify(data));
Settings and UI
Use Proper Setting Creation
// ✅ Good - Use setHeading() API
new Setting(containerEl).setName('Your Heading Title').setHeading();
// ❌ Bad - Direct HTML
// containerEl.innerHTML = '<h1>Your Heading Title</h1>';
Command Registration Best Practices
// ❌ Bad - Redundant plugin name
this.addCommand({
id: 'my-plugin-do-something',
name: 'My Plugin: Do Something',
callback: () => { /* ... */ }
});
// ✅ Good - Clean naming
this.addCommand({
id: 'do-something',
name: 'Do Something',
callback: () => { /* ... */ }
});
Avoid Default Hotkeys
// ❌ Bad - Setting default hotkeys causes conflicts
this.addCommand({
id: 'my-command',
name: 'My Command',
hotkeys: [{ modifiers: ['Ctrl'], key: 'k' }], // Don't do this
callback: () => {}
});
// ✅ Good - Let users set their own hotkeys
this.addCommand({
id: 'my-command',
name: 'My Command',
callback: () => {}
});
Editor Extensions
Update Extensions Dynamically
class MyPlugin extends Plugin {
private editorExtension: Extension[] = [];
onload() {
this.registerEditorExtension(this.editorExtension);
}
updateEditorExtension() {
// Empty array while keeping same reference
this.editorExtension.length = 0;
// Add new extension
let myNewExtension = this.createEditorExtension();
this.editorExtension.push(myNewExtension);
// Flush changes to all editors
this.app.workspace.updateOptions();
}
}
Platform Detection
Use Obsidian's Platform API
import { Platform } from 'obsidian';
// ✅ Good - Cross-platform compatible
if (Platform.isMobile) {
console.log('Running on mobile');
}
if (Platform.isIosApp) {
// iOS-specific code
}
if (Platform.isAndroidApp) {
// Android-specific code
}
// ❌ Bad - Node.js specific
// if (process.platform === 'darwin') {
// console.log('Running on macOS');
// }
Performance Optimization
Optimize Plugin Load Time
// ✅ Defer non-critical setup
class MyPlugin extends Plugin {
onload() {
// Critical setup only
this.app.workspace.onLayoutReady(() => {
// Defer heavy operations
this.registerEvent(this.app.vault.on('create', this.onCreate));
});
}
onCreate() {
if (!this.app.workspace.layoutReady) {
return; // Skip during initial load
}
// Handle file creation
}
}
Modern JavaScript Practices
Use Modern Variable Declarations
// ✅ Good - Modern JavaScript
let count = 0;
const MAX_COUNT = 10;
// ❌ Bad - Avoid var
// var oldVar = 'value';
Avoid Global Variables
// ✅ Good - Encapsulated scope
class MyClass {
private myVariable: string = 'local';
constructor() {
console.log(this.myVariable);
}
}
// ❌ Bad - Global variables
// let globalVar = 'global';
Strong TypeScript Typing
// ✅ Good - Proper typing
interface MyData { id: number; name: string; }
const data: MyData = { id: 1, name: 'Example' };
// ❌ Bad - Using 'as any'
// const data: any = { id: 1, name: 'Example' };
// const id = (data as any).id;
Dependencies
Import from Obsidian When Available
// ✅ Good - Use Obsidian's bundled libraries
import { moment } from 'obsidian';
// ❌ Bad - Bundling your own copy
// import moment from 'moment';
CSS Styling Guidelines
Use CSS Classes, Not Inline Styles
// ❌ Bad - Inline styles via JavaScript
const el = containerEl.createDiv();
el.style.color = 'white';
el.style.backgroundColor = 'red';
el.style.display = 'none'; // Avoid this pattern
// ✅ Good - CSS classes for styling and visibility
const el = containerEl.createDiv({cls: 'warning-container hidden'});
// ✅ Good - Dynamic class toggling instead of style assignments
if (shouldHide) {
el.addClass('hidden');
} else {
el.removeClass('hidden');
}
/* styles.css */
.warning-container {
color: var(--text-normal);
background-color: var(--background-modifier-error);
}
Scope Your CSS
/* ❌ Bad - Overriding core styles */
.some-obsidian-core-class {
color: red;
}
/* ✅ Good - Scoped to plugin */
.my-plugin-container .some-element {
color: blue;
}
Plugin Release Guidelines
Required Files
Your repository must contain:
README.md- Plugin description and usageLICENSE- Usage terms and conditionsmanifest.json- Plugin metadata
Manifest.json Structure
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "A description of my plugin.",
"author": "Your Name",
"authorUrl": "https://yourwebsite.com",
"fundingUrl": "https://ko-fi.com/yourname",
"isDesktopOnly": false
}
Version Management
// manifest.json
{
"version": "1.0.0",
"minAppVersion": "1.2.0"
}
// versions.json (optional fallbacks)
{
"0.1.0": "1.0.0",
"0.12.0": "1.1.0"
}
GitHub Actions Release
name: Release Obsidian plugin
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Build plugin
run: |
npm install
npm run build
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css
Common Anti-Patterns to Avoid
❌ Things to Avoid
- Don't use global app object
- Don't include 'Obsidian' in plugin name (unless essential)
- Don't use innerHTML with user input (XSS vulnerability)
- Don't set default hotkeys (causes conflicts)
- Don't use deprecated methods (check for strikethrough in IDE)
- Don't detach leaves in onunload (breaks user layout)
- Don't manage view references (causes memory leaks)
- Don't use Node.js APIs on mobile
- Don't include main.js in repository (only in releases)
- Don't leave console.log statements (unless necessary)
- Don't use placeholder names (MyPlugin, SampleSettingTab)
- Don't override core CSS classes
- Don't use
!importantin CSS - Don't use
:has()CSS selector (performance issues) - Don't manually manage plugin data files
Plugin Development Checklist
Before Release
- Replace all placeholder names
- Remove unnecessary console.log statements
- Scan for deprecated methods
- Optimize plugin load time
- Test on mobile (if not desktop-only)
- Ensure CSS is scoped to plugin
- Use strong TypeScript typing
- Add funding URL to manifest
- Minimize main.js file
- Test with DeferredViews (Obsidian 1.7.2+)
- Use proper heading methods in settings
- Avoid setting default hotkeys
- Clean up resources on unload
- Use Vault API over Adapter API
- Handle paths with normalizePath()
- Use requestUrl for network requests
Repository Structure
my-plugin/
├── README.md
├── LICENSE
├── manifest.json
├── main.ts
├── styles.css
├── package.json
├── tsconfig.json
├── esbuild.config.mjs
└── .gitignore (exclude main.js)
This comprehensive guide covers all essential aspects of Obsidian plugin development, from basic setup to advanced performance optimization and security considerations. Following these guidelines ensures your plugin is robust, secure, and compatible with Obsidian's ecosystem.