Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 0 additions & 42 deletions .github/workflows/codeql.yml

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ data.json
# test coverage
coverage/

# generated by scripts/typecheck-compat.mjs
tsconfig.compat.generated.json

# Claude worktrees
.claude/

Expand Down
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -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/",
Expand Down
34 changes: 2 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
62 changes: 62 additions & 0 deletions scripts/typecheck-compat.mjs
Original file line number Diff line number Diff line change
@@ -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('/')] },

Check warning on line 55 in scripts/typecheck-compat.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#split().join()`.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AZ86bKrwZ3Nc1zahhPLE&open=AZ86bKrwZ3Nc1zahhPLE&pullRequest=46
},
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}`);
24 changes: 19 additions & 5 deletions src/settings.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
18 changes: 16 additions & 2 deletions src/ui/SyncConflictModal.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Expand Down
28 changes: 28 additions & 0 deletions tests/ui/SyncConflictModal.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
3 changes: 2 additions & 1 deletion versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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