Merge pull request #46 from firstsun-dev/claude/obsidian-version-adoption-56aej4

Lower minAppVersion to 1.11.0 with runtime compatibility guards
This commit is contained in:
ClaudiaFang 2026-07-07 10:44:09 +08:00 committed by GitHub
commit 0fd289f79f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 137 additions and 86 deletions

View file

@ -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}}"

3
.gitignore vendored
View file

@ -21,6 +21,9 @@ data.json
# test coverage
coverage/
# generated by scripts/typecheck-compat.mjs
tsconfig.compat.generated.json
# Claude worktrees
.claude/

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/",

34
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",
@ -1602,9 +1602,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -1622,9 +1619,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -1642,9 +1636,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -1662,9 +1653,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -1682,9 +1670,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -1702,9 +1687,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -7172,9 +7154,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -7196,9 +7175,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -7220,9 +7196,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -7244,9 +7217,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [

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",
@ -11,7 +11,8 @@
},
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"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",

View file

@ -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}`);

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();
});
});

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"
}