From 335444643149aaf3dad000717856e0cc6d581749 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 23 Sep 2026 02:58:31 +0000 Subject: [PATCH] Single-source the version from package.json (closes #5) src/index.ts imports package.json for VERSION and bin/quak.ts passes VERSION to commander, so package.json is the only place the version is written. tsc copies package.json to dist/package.json, so the import resolves from the built output; script/build runs the built CLI with --version to prove it. A test checks VERSION and quak --version against package.json. Model: opus-5-5 --- TODO.md | 6 ++++++ bin/quak.ts | 3 ++- script/build | 14 ++++++++++++++ src/index.ts | 6 +++++- test/smoke.test.ts | 44 +++++++++++++++++++++++++++++++++++++++----- 5 files changed, 66 insertions(+), 7 deletions(-) diff --git a/TODO.md b/TODO.md index bfc840b..0622278 100644 --- a/TODO.md +++ b/TODO.md @@ -18,6 +18,12 @@ Tag v1.0.0. # Completed Steps +- 2026-09-23: Single-sourced the version string (issue 5). `package.json` is the + only place it is written: `src/index.ts` imports it for `VERSION` and + `bin/quak.ts` passes `VERSION` to commander. tsc copies `package.json` to + `dist/package.json`, so the import resolves from the built output too, and + `script/build` runs the built CLI with `--version` to prove it. A test checks + that `VERSION` and `quak --version` both equal the `package.json` version. - 2026-09-23: Tested that `Library.close()` waits for the originals precache (issue 93). The test that holds a precache fetch open while `close()` runs now runs once with only the thumbnail fill and once with only the originals fill, diff --git a/bin/quak.ts b/bin/quak.ts index 12b89a3..e4ad124 100644 --- a/bin/quak.ts +++ b/bin/quak.ts @@ -19,6 +19,7 @@ import { fixMissingThumbnailsCommand, } from "../src/cli-commands.js"; import { loadSession } from "../src/cli-session.js"; +import { VERSION } from "../src/index.js"; const paths = envPaths("quak", { suffix: "" }); @@ -27,7 +28,7 @@ const program = new Command(); program .name("quak") .description("CLI for the Ente end-to-end encrypted photo service") - .version("0.0.0") + .version(VERSION) .option( "--cache-dir ", "Directory for the local metadata/content cache " + diff --git a/script/build b/script/build index 05d5180..0a074b7 100755 --- a/script/build +++ b/script/build @@ -45,10 +45,24 @@ for (const bin of bins) { ' } +# src/index.ts imports ../package.json for the version, which tsc copies to +# dist/package.json. Running the built CLI proves that import resolves from +# dist/ and reports the version package.json declares. +verify_version() { + built="$(node dist/bin/quak.js --version)" + declared="$(node -p 'require("./package.json").version')" + if [ "$built" != "$declared" ]; then + echo "build: dist/bin/quak.js reports $built, package.json declares $declared" >&2 + exit 1 + fi + echo "build: dist/bin/quak.js reports version $built" +} + main() { cd "$ROOT" yarn run tsc verify_entrypoints + verify_version } main "$@" diff --git a/src/index.ts b/src/index.ts index 438e60e..a0a4186 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,8 @@ -export const VERSION = "0.0.0"; +// package.json is the one place the version is written. tsc copies it to +// dist/package.json, so this path resolves from source and from dist/src/. +import pkg from "../package.json" with { type: "json" }; + +export const VERSION: string = pkg.version; export { Client, diff --git a/test/smoke.test.ts b/test/smoke.test.ts index 77fe023..a33c747 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -1,9 +1,43 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { readFileSync } from "node:fs"; import { VERSION } from "../src/index.js"; -describe("quak", () => { - it("exports a version string", () => { - expect(typeof VERSION).toBe("string"); - expect(VERSION.length).toBeGreaterThan(0); +const packageVersion = ( + JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf-8"), + ) as { version: string } +).version; + +class ExitCalled extends Error {} + +describe("version", () => { + const argv = process.argv; + + afterEach(() => { + process.argv = argv; + vi.restoreAllMocks(); + }); + + it("exports the version from package.json", () => { + expect(VERSION).toBe(packageVersion); + }); + + // Runs bin/quak.ts with --version. commander prints the version and then + // calls process.exit, which is stubbed to throw so the test survives. + it("reports the version from package.json in quak --version", async () => { + const printed: string[] = []; + vi.spyOn(process.stdout, "write").mockImplementation((chunk) => { + printed.push(String(chunk)); + return true; + }); + vi.spyOn(process, "exit").mockImplementation(() => { + throw new ExitCalled(); + }); + process.argv = ["node", "quak", "--version"]; + + await expect(import("../bin/quak.js")).rejects.toBeInstanceOf( + ExitCalled, + ); + expect(printed.join("").trim()).toBe(packageVersion); }); });