From 4efca059da505f52894569e082c5fd9923f5cf95 Mon Sep 17 00:00:00 2001 From: Frederik Bosch <6979916+fbosch@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:36:33 +0100 Subject: [PATCH 1/3] fix(sync): resolve tool version from module dir --- src/commands/sync.ts | 73 ++++++++++++++++++++++----------- tests/sync-tool-version.test.js | 2 +- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 064d02d..c1343df 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { access, mkdir, readFile } from "node:fs/promises"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import pc from "picocolors"; import type { DocsCacheLock, DocsCacheLockSource } from "#cache/lock"; import { readLock, resolveLockPath, writeLock } from "#cache/lock"; @@ -70,6 +71,9 @@ const normalizePatterns = (patterns?: string[]) => { return Array.from(new Set(normalized)).sort(); }; +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + const RULES_HASH_BLACKLIST = [ "id", "repo", @@ -187,35 +191,56 @@ export const getSyncPlan = async ( }; }; -const loadToolVersion = async () => { - const cwdPath = path.resolve(process.cwd(), "package.json"); +const TOOL_PACKAGE_NAME = "docs-cache"; + +const readToolVersionFromPackageFile = async (packagePath: string) => { try { - const raw = await readFile(cwdPath, "utf8"); - const pkg = JSON.parse(raw.toString()); - return typeof pkg.version === "string" ? pkg.version : "0.0.0"; + const raw = await readFile(packagePath, "utf8"); + const parsed: unknown = JSON.parse(raw.toString()); + if (!isRecord(parsed)) { + return null; + } + const pkgName = parsed.name; + const pkgVersion = parsed.version; + if (pkgName !== TOOL_PACKAGE_NAME) { + return null; + } + if (typeof pkgVersion !== "string" || pkgVersion.length === 0) { + return null; + } + return pkgVersion; } catch { - // fallback to bundle-relative location + return null; } - try { - const raw = await readFile( - new URL("../package.json", import.meta.url), - "utf8", - ); - const pkg = JSON.parse(raw.toString()); - return typeof pkg.version === "string" ? pkg.version : "0.0.0"; - } catch { - // fallback to dist/chunks relative location +}; + +const findToolVersionFrom = async (startDir: string) => { + let currentDir = startDir; + while (true) { + const packagePath = path.join(currentDir, "package.json"); + const version = await readToolVersionFromPackageFile(packagePath); + if (version) { + return version; + } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + return null; + } + currentDir = parentDir; } - try { - const raw = await readFile( - new URL("../../package.json", import.meta.url), - "utf8", - ); - const pkg = JSON.parse(raw.toString()); - return typeof pkg.version === "string" ? pkg.version : "0.0.0"; - } catch { - return "0.0.0"; +}; + +const loadToolVersion = async () => { + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const moduleVersion = await findToolVersionFrom(moduleDir); + if (moduleVersion) { + return moduleVersion; + } + const cwdVersion = await findToolVersionFrom(process.cwd()); + if (cwdVersion) { + return cwdVersion; } + return "0.0.0"; }; const buildLockSource = ( diff --git a/tests/sync-tool-version.test.js b/tests/sync-tool-version.test.js index 3ab6ff0..c424ccd 100644 --- a/tests/sync-tool-version.test.js +++ b/tests/sync-tool-version.test.js @@ -49,7 +49,7 @@ test("sync writes lock toolVersion from package.json", async () => { ); const lock = JSON.parse(lockRaw); const pkgRaw = await readFile( - path.resolve(process.cwd(), "package.json"), + new URL("../package.json", import.meta.url), "utf8", ); const pkg = JSON.parse(pkgRaw); From 5d44659a633464f34e20bd2a34f2ec4bdb6153b5 Mon Sep 17 00:00:00 2001 From: Frederik Bosch <6979916+fbosch@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:49:04 +0100 Subject: [PATCH 2/3] refactor(core): extract isrecord to shared utility --- src/cache/lock.ts | 4 +--- src/commands/sync.ts | 4 +--- src/is-record.ts | 2 ++ tests/sync-tool-version.test.js | 30 ++++++++++++++++++++++-------- 4 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 src/is-record.ts diff --git a/src/cache/lock.ts b/src/cache/lock.ts index c42c69b..e734dee 100644 --- a/src/cache/lock.ts +++ b/src/cache/lock.ts @@ -1,5 +1,6 @@ import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; +import { isRecord } from "#core/is-record"; export interface DocsCacheLockSource { repo: string; @@ -19,9 +20,6 @@ export interface DocsCacheLock { export const DEFAULT_LOCK_FILENAME = "docs-lock.json"; -const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value); - const assertString = (value: unknown, label: string): string => { if (typeof value !== "string" || value.length === 0) { throw new Error(`${label} must be a non-empty string.`); diff --git a/src/commands/sync.ts b/src/commands/sync.ts index c1343df..97c49db 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -19,6 +19,7 @@ import { type DocsCacheResolvedSource, loadConfig, } from "#config"; +import { isRecord } from "#core/is-record"; import { resolveCacheDir, resolveTargetDir } from "#core/paths"; import { fetchSource } from "#git/fetch-source"; import { resolveRemoteCommit } from "#git/resolve-remote"; @@ -71,9 +72,6 @@ const normalizePatterns = (patterns?: string[]) => { return Array.from(new Set(normalized)).sort(); }; -const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value); - const RULES_HASH_BLACKLIST = [ "id", "repo", diff --git a/src/is-record.ts b/src/is-record.ts new file mode 100644 index 0000000..c632f81 --- /dev/null +++ b/src/is-record.ts @@ -0,0 +1,2 @@ +export const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/tests/sync-tool-version.test.js b/tests/sync-tool-version.test.js index c424ccd..e6d4856 100644 --- a/tests/sync-tool-version.test.js +++ b/tests/sync-tool-version.test.js @@ -33,15 +33,29 @@ test("sync writes lock toolVersion from package.json", async () => { ), "utf8", ); + await writeFile( + path.join(tmpRoot, "package.json"), + JSON.stringify({ + name: "not-docs-cache", + version: "9.9.9", + }), + "utf8", + ); - await runSync({ - configPath, - cacheDirOverride: cacheDir, - json: true, - lockOnly: true, - offline: true, - failOnMiss: false, - }); + const originalCwd = process.cwd(); + try { + process.chdir(tmpRoot); + await runSync({ + configPath, + cacheDirOverride: cacheDir, + json: true, + lockOnly: true, + offline: true, + failOnMiss: false, + }); + } finally { + process.chdir(originalCwd); + } const lockRaw = await readFile( path.join(tmpRoot, DEFAULT_LOCK_FILENAME), From 345f813a8639fc78d8b5f944f52b8cae3c2b127b Mon Sep 17 00:00:00 2001 From: Frederik Bosch <6979916+fbosch@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:09:08 +0100 Subject: [PATCH 3/3] chore: cleanup --- src/commands/sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 97c49db..27bf2a3 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -194,7 +194,7 @@ const TOOL_PACKAGE_NAME = "docs-cache"; const readToolVersionFromPackageFile = async (packagePath: string) => { try { const raw = await readFile(packagePath, "utf8"); - const parsed: unknown = JSON.parse(raw.toString()); + const parsed: unknown = JSON.parse(raw); if (!isRecord(parsed)) { return null; }