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..eaa1e9a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -86,6 +86,75 @@ 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]); +}; + +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 = { + $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.defaults || Object.keys(next.defaults).length === 0) { + delete next.defaults; + } + 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); +});