From d9eda9bda0beed1847e77d5140ceafe2815b84cc Mon Sep 17 00:00:00 2001 From: Frederik Bosch <6979916+fbosch@users.noreply.github.com> Date: Sat, 31 Jan 2026 22:39:51 +0100 Subject: [PATCH 1/2] fix(config): strip defaults --- src/add.ts | 3 +- src/config.ts | 68 +++++++++++++++++++++++++++++++++ src/init.ts | 3 +- src/remove.ts | 3 +- src/sync.ts | 10 +++++ tests/cli-add.test.js | 30 ++++++++++++++- tests/sync-tool-version.test.js | 54 ++++++++++++++++++++++++++ 7 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 tests/sync-tool-version.test.js diff --git a/src/add.ts b/src/add.ts index 3dd199a..e5b0f80 100644 --- a/src/add.ts +++ b/src/add.ts @@ -5,6 +5,7 @@ import { DEFAULT_CONFIG, type DocsCacheConfig, resolveConfigPath, + stripDefaultConfigValues, validateConfig, writeConfig, } from "./config"; @@ -132,7 +133,7 @@ export const addSources = async (params: { if (target.mode === "package") { const pkg = rawPackage ?? {}; - pkg["docs-cache"] = nextConfig; + pkg["docs-cache"] = stripDefaultConfigValues(nextConfig); await writeFile(resolvedPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8"); } else { await writeConfig(resolvedPath, nextConfig); diff --git a/src/config.ts b/src/config.ts index d07209b..c9ef2e7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -86,6 +86,74 @@ export const DEFAULT_CONFIG: DocsCacheConfig = { sources: [], }; +const isEqualStringArray = (left?: string[], right?: string[]) => { + if (!left || !right) { + return left === right; + } + if (left.length !== right.length) { + return false; + } + return left.every((value, index) => value === right[index]); +}; + +export const stripDefaultConfigValues = ( + config: DocsCacheConfig, +): DocsCacheConfig => { + const next: DocsCacheConfig = { + ...config, + sources: config.sources, + }; + if (next.$schema === DEFAULT_CONFIG.$schema) { + delete next.$schema; + } + if (next.cacheDir === DEFAULT_CACHE_DIR) { + delete next.cacheDir; + } + if (next.index === false) { + delete next.index; + } + if (next.targetMode === DEFAULT_TARGET_MODE) { + delete next.targetMode; + } + if (next.defaults) { + const defaultValues = DEFAULT_CONFIG.defaults as DocsCacheDefaults; + const pruned: Partial = { ...next.defaults }; + if (pruned.ref === defaultValues.ref) { + delete pruned.ref; + } + if (pruned.mode === defaultValues.mode) { + delete pruned.mode; + } + if (isEqualStringArray(pruned.include, defaultValues.include)) { + delete pruned.include; + } + if (pruned.targetMode === defaultValues.targetMode) { + delete pruned.targetMode; + } + if (pruned.depth === defaultValues.depth) { + delete pruned.depth; + } + if (pruned.required === defaultValues.required) { + delete pruned.required; + } + if (pruned.maxBytes === defaultValues.maxBytes) { + delete pruned.maxBytes; + } + if (pruned.maxFiles === defaultValues.maxFiles) { + delete pruned.maxFiles; + } + if (isEqualStringArray(pruned.allowHosts, defaultValues.allowHosts)) { + delete pruned.allowHosts; + } + if (Object.keys(pruned).length === 0) { + delete next.defaults; + } else { + next.defaults = pruned; + } + } + return next; +}; + const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/src/init.ts b/src/init.ts index 54df0bc..093f38d 100644 --- a/src/init.ts +++ b/src/init.ts @@ -10,6 +10,7 @@ import { DEFAULT_CACHE_DIR, DEFAULT_CONFIG_FILENAME, type DocsCacheConfig, + stripDefaultConfigValues, writeConfig, } from "./config"; @@ -130,7 +131,7 @@ export const initConfig = async ( if (answers.index) { baseConfig.index = true; } - pkg["docs-cache"] = baseConfig; + pkg["docs-cache"] = stripDefaultConfigValues(baseConfig); await writeFile( resolvedConfigPath, `${JSON.stringify(pkg, null, 2)}\n`, diff --git a/src/remove.ts b/src/remove.ts index 56c537a..4197152 100644 --- a/src/remove.ts +++ b/src/remove.ts @@ -4,6 +4,7 @@ import { DEFAULT_CONFIG, type DocsCacheConfig, resolveConfigPath, + stripDefaultConfigValues, validateConfig, writeConfig, } from "./config"; @@ -148,7 +149,7 @@ export const removeSources = async (params: { if (target.mode === "package") { const pkg = rawPackage ?? {}; - pkg["docs-cache"] = nextConfig; + pkg["docs-cache"] = stripDefaultConfigValues(nextConfig); await writeFile(resolvedPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8"); } else { await writeConfig(resolvedPath, nextConfig); diff --git a/src/sync.ts b/src/sync.ts index c8abff3..f557066 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -212,6 +212,16 @@ const loadToolVersion = async () => { ); const pkg = JSON.parse(raw.toString()); return typeof pkg.version === "string" ? pkg.version : "0.0.0"; + } catch { + // fallback to dist/chunks relative location + } + 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"; } diff --git a/tests/cli-add.test.js b/tests/cli-add.test.js index 8dae993..9e1b255 100644 --- a/tests/cli-add.test.js +++ b/tests/cli-add.test.js @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; -import { readFile } from "node:fs/promises"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { test } from "node:test"; @@ -113,3 +113,31 @@ test("add supports full https gitlab url", async () => { const config = JSON.parse(raw); assert.equal(config.sources[0].repo, "https://gitlab.com/acme/docs.git"); }); + +test("add writes package.json without default fields", async () => { + const tmpRoot = path.join(tmpdir(), `docs-cache-add-package-${Date.now()}`); + await mkdir(tmpRoot, { recursive: true }); + const packagePath = path.join(tmpRoot, "package.json"); + await writeFile( + packagePath, + JSON.stringify({ name: "x", version: "0.0.0" }), + "utf8", + ); + + await execFileAsync("node", [ + "bin/docs-cache.mjs", + "add", + "--offline", + "https://github.com/fbosch/docs-cache.git", + "--config", + packagePath, + ]); + + const raw = await readFile(packagePath, "utf8"); + const pkg = JSON.parse(raw); + assert.ok(pkg["docs-cache"]); + assert.equal(pkg["docs-cache"].cacheDir, undefined); + assert.equal(pkg["docs-cache"].index, undefined); + assert.equal(pkg["docs-cache"].defaults, undefined); + assert.equal(pkg["docs-cache"].targetMode, undefined); +}); diff --git a/tests/sync-tool-version.test.js b/tests/sync-tool-version.test.js new file mode 100644 index 0000000..5047f5f --- /dev/null +++ b/tests/sync-tool-version.test.js @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { test } from "node:test"; + +import { runSync } from "../dist/api.mjs"; + +test("sync writes lock toolVersion from package.json", async () => { + const tmpRoot = path.join( + tmpdir(), + `docs-cache-tool-version-${Date.now().toString(36)}`, + ); + await mkdir(tmpRoot, { recursive: true }); + const configPath = path.join(tmpRoot, "docs.config.json"); + const cacheDir = path.join(tmpRoot, ".docs"); + + await writeFile( + configPath, + JSON.stringify( + { + $schema: + "https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json", + sources: [ + { + id: "local", + repo: "https://example.com/repo.git", + }, + ], + }, + null, + 2, + ), + "utf8", + ); + + await runSync({ + configPath, + cacheDirOverride: cacheDir, + json: true, + lockOnly: true, + offline: true, + failOnMiss: false, + }); + + const lockRaw = await readFile(path.join(tmpRoot, "docs.lock"), "utf8"); + const lock = JSON.parse(lockRaw); + const pkgRaw = await readFile( + path.resolve(process.cwd(), "package.json"), + "utf8", + ); + const pkg = JSON.parse(pkgRaw); + assert.equal(lock.toolVersion, pkg.version); +}); From 51f99421e823b89eb6abdb58d3cb4872adbc69c4 Mon Sep 17 00:00:00 2001 From: Frederik Bosch <6979916+fbosch@users.noreply.github.com> Date: Sat, 31 Jan 2026 22:44:03 +0100 Subject: [PATCH 2/2] chore: refactor --- src/config.ts | 97 ++++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/src/config.ts b/src/config.ts index c9ef2e7..eaa1e9a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -96,60 +96,61 @@ const isEqualStringArray = (left?: string[], right?: string[]) => { return left.every((value, index) => value === right[index]); }; +const isObject = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const pruneDefaults = ( + value: Record, + baseline: Record, +): Record => { + const result: Record = {}; + for (const [key, entry] of Object.entries(value)) { + const base = baseline[key]; + if (Array.isArray(entry) && Array.isArray(base)) { + if (!isEqualStringArray(entry, base)) { + result[key] = entry; + } + continue; + } + if (isObject(entry) && isObject(base)) { + const pruned = pruneDefaults(entry, base); + if (Object.keys(pruned).length > 0) { + result[key] = pruned; + } + continue; + } + if (entry !== base) { + result[key] = entry; + } + } + return result; +}; + export const stripDefaultConfigValues = ( config: DocsCacheConfig, ): DocsCacheConfig => { + const baseline: DocsCacheConfig = { + ...DEFAULT_CONFIG, + $schema: config.$schema, + defaults: { + ...DEFAULT_CONFIG.defaults, + ...(config.targetMode ? { targetMode: config.targetMode } : undefined), + }, + }; + const pruned = pruneDefaults( + config as unknown as Record, + baseline as unknown as Record, + ); const next: DocsCacheConfig = { - ...config, + $schema: pruned.$schema as DocsCacheConfig["$schema"], + cacheDir: pruned.cacheDir as DocsCacheConfig["cacheDir"], + index: pruned.index as DocsCacheConfig["index"], + targetMode: pruned.targetMode as DocsCacheConfig["targetMode"], + defaults: pruned.defaults as DocsCacheConfig["defaults"], sources: config.sources, }; - if (next.$schema === DEFAULT_CONFIG.$schema) { - delete next.$schema; - } - if (next.cacheDir === DEFAULT_CACHE_DIR) { - delete next.cacheDir; - } - if (next.index === false) { - delete next.index; - } - if (next.targetMode === DEFAULT_TARGET_MODE) { - delete next.targetMode; - } - if (next.defaults) { - const defaultValues = DEFAULT_CONFIG.defaults as DocsCacheDefaults; - const pruned: Partial = { ...next.defaults }; - if (pruned.ref === defaultValues.ref) { - delete pruned.ref; - } - if (pruned.mode === defaultValues.mode) { - delete pruned.mode; - } - if (isEqualStringArray(pruned.include, defaultValues.include)) { - delete pruned.include; - } - if (pruned.targetMode === defaultValues.targetMode) { - delete pruned.targetMode; - } - if (pruned.depth === defaultValues.depth) { - delete pruned.depth; - } - if (pruned.required === defaultValues.required) { - delete pruned.required; - } - if (pruned.maxBytes === defaultValues.maxBytes) { - delete pruned.maxBytes; - } - if (pruned.maxFiles === defaultValues.maxFiles) { - delete pruned.maxFiles; - } - if (isEqualStringArray(pruned.allowHosts, defaultValues.allowHosts)) { - delete pruned.allowHosts; - } - if (Object.keys(pruned).length === 0) { - delete next.defaults; - } else { - next.defaults = pruned; - } + if (!next.defaults || Object.keys(next.defaults).length === 0) { + delete next.defaults; } return next; };