6.2 KiB
Project Review: zeroq-qol-modules
Overview
Obsidian plugin with a modular architecture. Each feature is a separate module that can be toggled on/off in settings.
Plugin ID: zeroq-qol-modules
Entry point: src/main.ts → compiles to main.js (esbuild)
Version source: package.json → auto-synced to manifest.json on build
Current modules (2): attachment-clean-paste, preserve-link-aliases
Stack
- Language: TypeScript (5.x)
- Bundler: esbuild (0.17.x)
- Obsidian API:
obsidianlatest (type defs only, excluded from bundle) - Format: CommonJS (
format: 'cjs'), target ES2018
Directory Structure
src/
├── main.ts # Plugin class — loads settings, bootstraps modules
├── settings.ts # ZeroQSettingTab — single settings pane, renders all modules
├── types.ts # QoLModule interface, ZeroQSettings, ModuleSettings
├── locales/
│ ├── en.ts # Locale type definition + English dictionary
│ ├── ru.ts # Russian dictionary (same Locale shape)
│ └── index.ts # Auto-detects language from <html lang>, exports locale
└── modules/
├── index.ts # Static registry: array of all module instances
├── base-module.ts # BaseModule abstract class
├── attachment-clean-paste/
│ └── index.ts # Handles paste/drop — replaces ![[embed]] → [[path|name]]
└── preserve-link-aliases/
└── index.ts # Preserves aliases on vault rename events
Architecture
Plugin Lifecycle (main.ts)
onload()
→ loadSettings() # merge DEFAULT_SETTINGS + saved data
→ addSettingTab() # register ZeroQSettingTab
→ for each module:
if enabled → loadModule(module)
→ module.onload(this, moduleSettings)
onunload()
→ for each module:
unloadModule(module)
→ module.onunload(this)
Module Lifecycle
Each module is a singleton implementing QoLModule (or extending BaseModule):
| Method | Purpose |
|---|---|
onload(plugin, moduleSettings) |
Initialize: register events, commands, setting tab |
onunload(plugin) |
Cleanup (auto-runs registerCleanup callbacks) |
renderInlineSettings(containerEl, settings, saveSettings) |
Render module-specific settings inline (no separate tab) |
defaultSettings |
Default module settings object |
Cleanup pattern
Modules use this.registerCleanup(fn) to queue cleanup callbacks. They auto-run in onunload(). Example:
const ref = plugin.app.workspace.on('editor-paste', handler);
this.registerCleanup(() => plugin.app.workspace.offref(ref));
Settings architecture
- ZeroQSettingTab (
settings.ts) — single Obsidian settings pane - Iterates
MODULES, renders each with:- Toggle (enable/disable) — toggling calls
loadModule/unloadModuleat runtime module.renderInlineSettings()— module-specific controls rendered directly below the toggle
- Toggle (enable/disable) — toggling calls
- No separate PluginSettingTab per module — all in one block
Settings data shape
interface ZeroQSettings {
modules: Record<string, {
enabled: boolean;
settings: Record<string, unknown>; // module-specific, typed per module
}>;
}
Localization (locales/)
- Locale type defined in
en.tsas the interfaceLocale - Language detection: reads
<html lang>attribute (set by Obsidian per its own language setting) - Fallback to
'en'if language is not in the dictionary - Usage:
import { locale } from '../../locales'→locale.settings.title,locale.modules['xxx'].name
To add a language:
- Create
src/locales/de.tswithLocaleshape - Import it in
src/locales/index.tsand add tolocalesrecord
Adding a New Module
Step 1: Create the module class
Extend BaseModule in src/modules/my-module/index.ts:
import { Plugin } from 'obsidian';
import { BaseModule } from '../base-module';
import { locale } from '../../locales';
export class MyModule extends BaseModule {
id = 'my-module';
name = locale.modules['my-module'].name;
description = locale.modules['my-module'].description;
get defaultSettings(): Record<string, unknown> {
return { myOption: 'default' };
}
onload(plugin: Plugin, moduleSettings: Record<string, unknown>): void {
// register events/commands via plugin.registerEvent / plugin.addCommand
// use this.registerCleanup(fn) for teardown
}
renderInlineSettings(
containerEl: HTMLElement,
settings: Record<string, unknown>,
saveSettings: () => Promise<void>,
): void {
// render controls into containerEl
// mutate settings object directly, then call saveSettings()
new Setting(containerEl)
.setName('My option')
.addText(text => text
.setValue(String(settings.myOption ?? ''))
.onChange(async v => {
settings.myOption = v;
await saveSettings();
}));
}
}
Step 2: Add translations
In src/locales/en.ts, add to the Locale interface and the en object:
'modules': {
'my-module': {
name: string;
description: string;
// any other strings this module needs
};
}
Do the same in src/locales/ru.ts.
Step 3: Register in the registry
In src/modules/index.ts:
import { MyModule } from './my-module';
export const MODULES: QoLModule[] = [
new AttachmentCleanPasteModule(),
new MyModule(),
];
Build
npm run build # production build (minified, no sourcemaps)
npm run dev # dev build (inline sourcemaps)
esbuild.config.mjs also syncs version from package.json to manifest.json before building.
Conventions
- No comments in source code
- No emojis in code or UI
- Indentation: tabs (Obsidian convention)
- Imports:
import typefor type-only imports to avoid circular dependencies - File naming:
kebab-casefor files,PascalCasefor classes - Russian labels for Russian locale, English labels for all other locales
- Module
idiskebab-caseand matches the key in the localesmodulesobject