fix(compat): support Obsidian down to 1.11.0

Release 1.2.0 raised minAppVersion to 1.13.0, pushing users still on
Obsidian 1.11/1.12 back to the older 1.1.2 build via versions.json. Only
three 1.13-era APIs were in use; two were already runtime-guarded, but
ButtonComponent.setDestructive() would throw on older Obsidian.

- Guard setDestructive() behind applyDestructiveStyle() (no-ops on < 1.13).
- Declare SettingDefinitionItem locally and read update() via a cast so the
  forward-compatible settings code type-checks against 1.11 typings.
- Ship 1.2.1 with minAppVersion 1.11.0 (manifest.json + versions.json).
- Add tsconfig.compat.json and `npm run typecheck:compat` to type-check src
  against the 1.11 API, plus regression tests for applyDestructiveStyle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSikT9ZAfQF8K4SE3DatD3
This commit is contained in:
Claude 2026-07-06 16:31:46 +00:00
parent 0a4cff5a46
commit d896015765
8 changed files with 106 additions and 43 deletions

View file

@ -1,8 +1,8 @@
{
"id": "git-file-sync",
"name": "Git File Sync",
"version": "1.2.0",
"minAppVersion": "1.13.0",
"version": "1.2.1",
"minAppVersion": "1.11.0",
"description": "Selectively sync individual notes with GitLab or GitHub. Push, pull, diff, and resolve conflicts — file by file, on mobile and desktop.",
"author": "ClaudiaFang",
"authorUrl": "https://firstsun.heavenfortress.com/en/",

51
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "git-file-sync",
"version": "1.2.0",
"version": "1.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "git-file-sync",
"version": "1.2.0",
"version": "1.2.1",
"license": "MIT",
"dependencies": {
"ignore": "^7.0.5",
@ -31,6 +31,7 @@
"husky": "^9.1.7",
"jiti": "2.6.1",
"jsdom": "^29.1.1",
"obsidian-1_11": "npm:obsidian@^1.11.0",
"semantic-release": "^25.0.5",
"tslib": "2.4.0",
"typescript": "^5.8.3",
@ -1602,9 +1603,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -1622,9 +1620,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -1642,9 +1637,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -1662,9 +1654,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -1682,9 +1671,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -1702,9 +1688,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -7172,9 +7155,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -7196,9 +7176,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -7220,9 +7197,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -7244,9 +7218,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -9887,6 +9858,22 @@
"@codemirror/view": "6.38.6"
}
},
"node_modules/obsidian-1_11": {
"name": "obsidian",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.11.0.tgz",
"integrity": "sha512-lVqN9AmDWHzhNATi2tDnjqVgI6WUYKeT+lIsAycAyLt4XCC6zRsWzb+tFCiB7Rn3PpttefjoovilhYwvS4Iqxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/codemirror": "5.60.8",
"moment": "2.29.4"
},
"peerDependencies": {
"@codemirror/state": "6.5.0",
"@codemirror/view": "6.38.6"
}
},
"node_modules/obug": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "git-file-sync",
"version": "1.2.0",
"version": "1.2.1",
"description": "Selectively sync individual notes with GitLab or GitHub. Push, pull, diff, and resolve conflicts — file by file, on mobile and desktop.",
"main": "main.js",
"type": "module",
@ -12,6 +12,7 @@
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"typecheck:compat": "tsc -p tsconfig.compat.json",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"lint": "eslint .",
"test": "vitest run",
@ -40,6 +41,7 @@
"husky": "^9.1.7",
"jiti": "2.6.1",
"jsdom": "^29.1.1",
"obsidian-1_11": "npm:obsidian@^1.11.0",
"semantic-release": "^25.0.5",
"tslib": "2.4.0",
"typescript": "^5.8.3",

View file

@ -1,6 +1,15 @@
import {App, PluginSettingTab, Setting, Notice, TextComponent, SettingDefinitionItem} from 'obsidian';
import {App, PluginSettingTab, Setting, Notice, TextComponent} from 'obsidian';
import GitLabFilesPush from "./main";
// Minimal shape of Obsidian >= 1.13's SettingDefinitionItem. Declared locally so
// the plugin still type-checks against older Obsidian typings (minAppVersion
// 1.11.0), where this type does not exist. Obsidian only calls
// getSettingDefinitions() on versions that understand it.
interface SettingDefinitionItem {
name: string;
render: (setting: unknown, group: { listEl: HTMLElement }) => void;
}
export interface SyncMetadata {
lastSyncedSha: string;
lastSyncedAt: number;
@ -83,8 +92,9 @@ export class GitLabSyncSettingTab extends PluginSettingTab {
this.plugin = plugin;
}
// Kept as a fallback for Obsidian < 1.13.0 (this plugin's minAppVersion),
// which don't know about getSettingDefinitions() and always call display().
// Kept as a fallback for Obsidian < 1.13.0 (older than 1.13, down to
// minAppVersion 1.11.0), which don't know about getSettingDefinitions()
// and always call display().
display(): void {
this.renderSettings(this.containerEl);
}
@ -99,8 +109,12 @@ export class GitLabSyncSettingTab extends PluginSettingTab {
}
private refresh(): void {
if (typeof this.update === 'function') {
this.update();
// update() only exists on Obsidian >= 1.13. On older versions (down to
// minAppVersion 1.11.0) re-render manually instead. Accessed via a cast
// so this compiles against the 1.11 typings, which lack update().
const maybeUpdate = (this as { update?: () => void }).update;
if (typeof maybeUpdate === 'function') {
maybeUpdate.call(this);
} else {
this.renderSettings(this.containerEl);
}

View file

@ -1,5 +1,20 @@
import { App, Modal, Setting } from 'obsidian';
/**
* Apply the "destructive" button style, but only when the running Obsidian
* supports it. ButtonComponent.setDestructive() was added in Obsidian 1.13; on
* older versions (down to this plugin's minAppVersion, 1.11.0) the method is
* absent, so we skip it instead of throwing "setDestructive is not a function".
* Returns the same button so it can be chained.
*/
export function applyDestructiveStyle<T extends object>(btn: T): T {
const setDestructive = (btn as { setDestructive?: () => unknown }).setDestructive;
if (typeof setDestructive === 'function') {
setDestructive.call(btn);
}
return btn;
}
export class SyncConflictModal extends Modal {
private readonly fileName: string;
private readonly localContent: string;
@ -52,10 +67,9 @@ export class SyncConflictModal extends Modal {
this.onChoose('local');
this.close();
}))
.addButton(btn => btn
.addButton(btn => applyDestructiveStyle(btn)
.setButtonText('Keep remote')
.setTooltip('Overwrite local with remote content')
.setDestructive()
.onClick(() => {
this.onChoose('remote');
this.close();

View file

@ -0,0 +1,28 @@
import { describe, it, expect, vi } from 'vitest';
import { applyDestructiveStyle } from '../../src/ui/SyncConflictModal';
// Guards the backward-compatibility fix that lets the plugin run on Obsidian
// down to minAppVersion 1.11.0. ButtonComponent.setDestructive() only exists on
// Obsidian >= 1.13; calling it unconditionally would throw on older versions.
describe('applyDestructiveStyle (Obsidian version compatibility)', () => {
it('applies the destructive style on Obsidian >= 1.13 (method present)', () => {
const setDestructive = vi.fn();
const btn = { setDestructive };
expect(applyDestructiveStyle(btn)).toBe(btn);
expect(setDestructive).toHaveBeenCalledOnce();
});
it('degrades gracefully on Obsidian < 1.13 (method absent) without throwing', () => {
const btn = {}; // an older ButtonComponent has no setDestructive()
expect(() => applyDestructiveStyle(btn)).not.toThrow();
expect(applyDestructiveStyle(btn)).toBe(btn);
});
it('ignores a non-function setDestructive rather than crashing', () => {
const btn = { setDestructive: 'nope' as unknown as () => unknown };
expect(() => applyDestructiveStyle(btn)).not.toThrow();
});
});

17
tsconfig.compat.json Normal file
View file

@ -0,0 +1,17 @@
{
// Type-checks src against the Obsidian 1.11 API typings (installed as the
// "obsidian-1_11" alias) instead of the latest ones. Because the plugin
// declares minAppVersion 1.11.0, any use of an API newer than 1.11 that is
// not runtime-guarded becomes a compile error here. Run via `npm run
// typecheck:compat`. Keep this passing whenever minAppVersion stays at 1.11.
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"obsidian": ["node_modules/obsidian-1_11/obsidian"]
}
},
"include": ["src/**/*.ts"]
}

View file

@ -5,5 +5,6 @@
"1.0.6": "1.12.7",
"1.1.1": "1.12.7",
"1.1.2": "1.12.7",
"1.2.0": "1.13.0"
"1.2.0": "1.13.0",
"1.2.1": "1.11.0"
}