From d8960157655e57e61e897483c493edd6a852bd45 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 16:31:46 +0000 Subject: [PATCH 1/3] 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 Claude-Session: https://claude.ai/code/session_01SSikT9ZAfQF8K4SE3DatD3 --- manifest.json | 4 +-- package-lock.json | 51 +++++++++++------------------- package.json | 4 ++- src/settings.ts | 24 +++++++++++--- src/ui/SyncConflictModal.ts | 18 +++++++++-- tests/ui/SyncConflictModal.test.ts | 28 ++++++++++++++++ tsconfig.compat.json | 17 ++++++++++ versions.json | 3 +- 8 files changed, 106 insertions(+), 43 deletions(-) create mode 100644 tests/ui/SyncConflictModal.test.ts create mode 100644 tsconfig.compat.json diff --git a/manifest.json b/manifest.json index 2638073..1e2424b 100644 --- a/manifest.json +++ b/manifest.json @@ -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/", diff --git a/package-lock.json b/package-lock.json index 90629d4..e0764e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index de29e90..6175a63 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/settings.ts b/src/settings.ts index 5947bd5..288f9c5 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -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); } diff --git a/src/ui/SyncConflictModal.ts b/src/ui/SyncConflictModal.ts index 655de05..10c7e49 100644 --- a/src/ui/SyncConflictModal.ts +++ b/src/ui/SyncConflictModal.ts @@ -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(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(); diff --git a/tests/ui/SyncConflictModal.test.ts b/tests/ui/SyncConflictModal.test.ts new file mode 100644 index 0000000..73ae781 --- /dev/null +++ b/tests/ui/SyncConflictModal.test.ts @@ -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(); + }); +}); diff --git a/tsconfig.compat.json b/tsconfig.compat.json new file mode 100644 index 0000000..2874231 --- /dev/null +++ b/tsconfig.compat.json @@ -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"] +} diff --git a/versions.json b/versions.json index 66971d8..545bdf3 100644 --- a/versions.json +++ b/versions.json @@ -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" } \ No newline at end of file From 692fc0f293c6e7f8d02e01cd5d9c369433d9d3b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 16:41:28 +0000 Subject: [PATCH 2/3] build(compat): derive the compat typecheck's target version dynamically The 1.11 compatibility gate previously hardcoded the target Obsidian version in two places (an obsidian-1_11 alias devDependency and a static tsconfig.compat.json), so bumping minAppVersion would silently leave the check pinned to the old version. Replace both with scripts/typecheck-compat.mjs, which reads the target from versions.json (the entry for the current manifest version, falling back to manifest.minAppVersion), installs the matching Obsidian typings into a git-ignored cache on demand, and type-checks src against them. The gate now follows minAppVersion automatically with no code changes. Also fold `typecheck:compat` into `npm run build` so it runs wherever the build runs (the husky pre-commit hook and any CI step that builds). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01SSikT9ZAfQF8K4SE3DatD3 --- .gitignore | 3 ++ package-lock.json | 17 ---------- package.json | 5 ++- scripts/typecheck-compat.mjs | 62 ++++++++++++++++++++++++++++++++++++ tsconfig.compat.json | 17 ---------- 5 files changed, 67 insertions(+), 37 deletions(-) create mode 100644 scripts/typecheck-compat.mjs delete mode 100644 tsconfig.compat.json diff --git a/.gitignore b/.gitignore index 5e9bb59..c9de6f6 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ data.json # test coverage coverage/ +# generated by scripts/typecheck-compat.mjs +tsconfig.compat.generated.json + # Claude worktrees .claude/ diff --git a/package-lock.json b/package-lock.json index e0764e1..8b7e0cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,6 @@ "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", @@ -9858,22 +9857,6 @@ "@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", diff --git a/package.json b/package.json index 6175a63..1f10292 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ }, "scripts": { "dev": "node esbuild.config.mjs", - "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", - "typecheck:compat": "tsc -p tsconfig.compat.json", + "build": "tsc -noEmit -skipLibCheck && npm run typecheck:compat && node esbuild.config.mjs production", + "typecheck:compat": "node scripts/typecheck-compat.mjs", "version": "node version-bump.mjs && git add manifest.json versions.json", "lint": "eslint .", "test": "vitest run", @@ -41,7 +41,6 @@ "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", diff --git a/scripts/typecheck-compat.mjs b/scripts/typecheck-compat.mjs new file mode 100644 index 0000000..d6821db --- /dev/null +++ b/scripts/typecheck-compat.mjs @@ -0,0 +1,62 @@ +#!/usr/bin/env node +/* + * Type-checks src/ against the exact Obsidian API version this release targets. + * Any use of an API newer than our declared minAppVersion that is NOT + * runtime-guarded becomes a compile error here. + * + * The target version is derived dynamically — never hardcoded. versions.json + * maps each plugin version to its minAppVersion; we use the entry for the + * current manifest version, falling back to manifest.minAppVersion (which is the + * canonical floor, and the reliable source when version-bump.mjs omits a + * versions.json entry because the minAppVersion was unchanged). + * + * The matching Obsidian typings are installed on demand into node_modules/.cache + * (git-ignored) and reused across runs, so bumping minAppVersion automatically + * re-points this check with no code changes. + */ +import { execSync } from 'node:child_process'; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, relative } from 'node:path'; +import process from 'node:process'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const readJson = (p) => JSON.parse(readFileSync(join(root, p), 'utf8')); + +const manifest = readJson('manifest.json'); +const versions = readJson('versions.json'); +const target = versions[manifest.version] ?? manifest.minAppVersion; + +if (!target) { + console.error('compat: could not resolve target Obsidian version from versions.json / manifest.json'); + process.exit(1); +} + +// Install the target typings into a per-version cache (reused on later runs). +const cacheDir = join(root, 'node_modules', '.cache', 'obsidian-compat', target); +const dtsBase = join(cacheDir, 'node_modules', 'obsidian', 'obsidian'); // no extension +if (!existsSync(`${dtsBase}.d.ts`)) { + mkdirSync(cacheDir, { recursive: true }); + console.log(`compat: installing obsidian@${target} typings...`); + execSync( + `npm install --prefix "${cacheDir}" --no-save --no-audit --no-fund --loglevel=error obsidian@${target}`, + { stdio: 'inherit' }, + ); +} + +// Generate a tsconfig that points the "obsidian" import at the target typings. +const generated = join(root, 'tsconfig.compat.generated.json'); +writeFileSync(generated, `${JSON.stringify({ + extends: './tsconfig.json', + compilerOptions: { + noEmit: true, + skipLibCheck: true, + baseUrl: '.', + paths: { obsidian: [relative(root, dtsBase).split('\\').join('/')] }, + }, + include: ['src/**/*.ts'], +}, null, '\t')}\n`); + +console.log(`compat: type-checking src against Obsidian ${target} API...`); +execSync(`node "${join(root, 'node_modules', 'typescript', 'bin', 'tsc')}" -p "${generated}"`, { stdio: 'inherit' }); +console.log(`compat: ✓ src is compatible with Obsidian ${target}`); diff --git a/tsconfig.compat.json b/tsconfig.compat.json deleted file mode 100644 index 2874231..0000000 --- a/tsconfig.compat.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - // 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"] -} From 23c78133c4322ceed43f63d28ee164d59358b6a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 02:41:00 +0000 Subject: [PATCH 3/3] ci: remove advanced CodeQL workflow in favor of default setup The repository has CodeQL "default setup" enabled, which rejects SARIF uploads from an advanced workflow ("CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled"), failing the Analyze check on every PR to main. Drop the advanced .github/workflows/codeql.yml and let GitHub's default setup handle CodeQL scanning. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01SSikT9ZAfQF8K4SE3DatD3 --- .github/workflows/codeql.yml | 42 ------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 84d8741..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ "main", "master" ] - pull_request: - branches: [ "main", "master" ] - schedule: - - cron: '30 1 * * 1' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript-typescript' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - # Queries can be 'security-extended' or 'security-and-quality' - queries: security-extended - - - name: Auto-build - uses: github/codeql-action/autobuild@v4 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}"