Obsidian Upgrade Migration
Current State
!npm list 2>/dev/null | head -20
!cat manifest.json 2>/dev/null || echo 'No manifest.json in cwd'
Overview
Upgrade an Obsidian plugin between versions: migrate persisted settings with version checks, replace deprecated API calls, update manifest.json minAppVersion, and test across Obsidian releases.
Prerequisites
Instructions
Step 1: Audit Current Version Compatibility
Check what your plugin currently targets and what the user's Obsidian version requires:
echo "=== manifest.json ==="
cat manifest.json | python3 -c "
import json, sys
m = json.load(sys.stdin)
print(f\"Plugin: {m['id']} v{m['version']}\")
print(f\"minAppVersion: {m['minAppVersion']}\")
"
echo "=== obsidian package version ==="
npm ls obsidian 2>/dev/null || echo "Not found in node_modules"
echo "=== versions.json ==="
cat versions.json 2>/dev/null | python3 -m json.tool || echo "No versions.json"
Step 2: Update the Obsidian Type Definitions
npm install obsidian@latest --save-dev
npm diff obsidian 2>/dev/null | head -100
Then check for TypeScript errors against the new types:
npx tsc --noEmit 2>&1 | head -50
Every error here is a breaking change you need to address.
Step 3: Settings Migration with Version Tracking
Implement a version-aware loadData() pattern so existing users' settings survive upgrades:
interface PluginSettings {
_version: number;
enabled: boolean;
syncInterval: number;
theme: 'light' | 'dark' | 'system';
}
const CURRENT_SETTINGS_VERSION = 3;
const DEFAULT_SETTINGS: PluginSettings = {
_version: CURRENT_SETTINGS_VERSION,
enabled: true,
syncInterval: 300,
theme: 'system',
};
async loadSettings(): Promise<PluginSettings> {
const raw = await this.loadData();
if (!raw) return { ...DEFAULT_SETTINGS };
const version = raw._version ?? 1;
let settings = { ...raw };
if (version < 2) {
settings.syncInterval = DEFAULT_SETTINGS.;
.();
}
(version < ) {
settings. = .;
( settings) {
settings. = settings. ? : ;
settings.;
}
.();
}
settings. = ;
.(settings);
settings ;
}
Step 4: Replace Deprecated API Calls
Common deprecations and their replacements:
Vault API changes:
await this.app.vault.modify(filePath, content);
const file = this.app.vault.getAbstractFileByPath(filePath);
if (file instanceof TFile) {
await this.app.vault.modify(file, content);
}
this.app.vault.create(path, content);
const newFile = await this.app.vault.create(path, content);
Event registration changes:
this.app.workspace.on('file-open', (file) => { ... });
this.registerEvent(
this.app.workspace.on('file-open', (file) => { ... })
);
Editor API (CodeMirror 5 to 6 migration):
const cm = (editor as any).cm;
cm.getValue();
const content = editor.getValue();
const cursor = editor.getCursor();
editor.replaceRange(text, cursor);
import { EditorView, ViewPlugin } from '@codemirror/view';
this.registerEditorExtension(
ViewPlugin.fromClass(class {
constructor(view: EditorView) {
}
})
);
FileManager changes:
this.app.fileManager.processFrontMatter(file, (fm) => {
fm.tags = ['updated'];
});
await this.app.fileManager.processFrontMatter(file, (fm) => {
fm.tags = ['updated'];
});
Step 5: Update manifest.json
Bump minAppVersion to the lowest Obsidian version that supports all APIs you use:
{
"id": "your-plugin",
"name": "Your Plugin",
"version": "3.0.0",
"minAppVersion": "1.5.0",
"description": "...",
"author": "...",
"isDesktopOnly": false
}
Update versions.json to map your plugin version to the minimum Obsidian version:
{
"1.0.0": "0.15.0",
"2.0.0": "1.0.0",
"3.0.0": "1.5.0"
}
Step 6: Test Across Obsidian Versions
Build and verify:
rm -rf dist node_modules/.cache
npm install
npm run build
npx tsc --noEmit
grep -rn 'cm\.getValue\|processFrontMatter.*sync\|vault\.modify.*string' src/ || echo "No deprecated patterns found"
Manual testing checklist:
- Install plugin on the
minAppVersion you declared -- confirm it loads without errors
- Install on latest Obsidian -- confirm full functionality
- Test settings migration: copy a
data.json from an older version into the plugin directory, reload, verify settings are preserved and upgraded
- Open Developer Console (Ctrl+Shift+I) and check for deprecation warnings
Step 7: Handle the Release
npm version major
python3 -c "
import json
v = json.load(open('versions.json'))
m = json.load(open('manifest.json'))
v[m['version']] = m['minAppVersion']
json.dump(v, open('versions.json', 'w'), indent=2)
print(f\"Added {m['version']} -> {m['minAppVersion']}\")
"
npm run build
Output
- Updated
manifest.json with correct minAppVersion
- Updated
versions.json with new version mapping
- Settings migration code that handles all previous schema versions
- All deprecated API calls replaced with current equivalents
- Clean
tsc --noEmit with no type errors
- Tested on minimum and latest Obsidian versions
Error Handling
| Error | Cause | Solution |
|---|
Property does not exist on type 'Plugin' | API removed in newer obsidian types | Check changelog for replacement API |
Cannot find module 'obsidian' | Types not installed | npm install obsidian@latest --save-dev |
| Settings lost after upgrade | No migration logic for _version jump | Add migration step for each version gap |
TypeError: x is not a function at runtime | API exists in types but not in user's Obsidian | Lower minAppVersion or add runtime version check |
| Plugin loads but features missing | Feature flag not migrated | Check settings migration covers all paths |
Examples
Simple version bump: Plugin works fine on new Obsidian, just need to update minAppVersion. Run Step 1 to audit, Step 5 to update manifest, Step 6 to verify.
CodeMirror 5 to 6 migration: Plugin uses editor.cm for custom decorations. Replace CM5 Decoration with CM6 EditorView extensions per Step 4. This is the most common large migration.
Settings schema change: Plugin v2 renamed darkMode: boolean to theme: 'light' | 'dark' | 'system'. Add migration in Step 3 that maps the old boolean to the new enum, preserving user preference.
Resources
Next Steps
For CI/CD to automate release testing, see obsidian-ci-integration. For multi-environment testing, see obsidian-multi-env-setup.