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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSikT9ZAfQF8K4SE3DatD3
This commit is contained in:
Claude 2026-07-06 16:41:28 +00:00
parent d896015765
commit 692fc0f293
5 changed files with 67 additions and 37 deletions

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/

17
package-lock.json generated
View file

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

View file

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

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