mirror of
https://github.com/firstsun-dev/git-files-sync.git
synced 2026-07-22 06:54:27 +00:00
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:
commit
0fd289f79f
10 changed files with 137 additions and 86 deletions
42
.github/workflows/codeql.yml
vendored
42
.github/workflows/codeql.yml
vendored
|
|
@ -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
3
.gitignore
vendored
|
|
@ -21,6 +21,9 @@ data.json
|
|||
# test coverage
|
||||
coverage/
|
||||
|
||||
# generated by scripts/typecheck-compat.mjs
|
||||
tsconfig.compat.generated.json
|
||||
|
||||
# Claude worktrees
|
||||
.claude/
|
||||
|
||||
|
|
|
|||
|
|
@ -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
34
package-lock.json
generated
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
62
scripts/typecheck-compat.mjs
Normal file
62
scripts/typecheck-compat.mjs
Normal 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}`);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
28
tests/ui/SyncConflictModal.test.ts
Normal file
28
tests/ui/SyncConflictModal.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Reference in a new issue