diff --git a/README.md b/README.md index cb9b8fa..ca6a460 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,23 @@ const predictedVersion = await predictNextVersion(process.cwd(), releaseRules, a const identity = resolvePredictedIdentity(build, predictedVersion); ``` +## `readPackageVersion(repoRoot)` + +```ts +function readPackageVersion(repoRoot: string): string; +``` + +The plain `"version"` field from `package.json` at `repoRoot`, read unconditionally and with no git access at all. This is deliberately not the same thing as `resolveBuildIdentity(...).version`, which is a build *identity* -- the released version only when a tag genuinely points at HEAD, and a short commit hash otherwise. Reach for this one when you want the last released semantic version regardless of whether this checkout happens to be sitting on its tag: an OpenAPI document's `info.version`, a user agent string, anything wanting a stable semver rather than an honest answer about what is deployed. + +Throws rather than defaulting when `package.json` is missing or its `"version"` is absent, non-string, or empty -- the same stance every other export in this package takes. `defaultTagName` (`v${version}`) is the shared convention `resolveBuildIdentity` and `predictNextVersion` both apply to this value. + +```ts +import { readPackageVersion, resolveBuildIdentity } from '@exadev/build-identity'; + +const appVersion = readPackageVersion(process.cwd()); // "1.4.0", tagged or not +const build = resolveBuildIdentity(process.cwd(), 'exadev/example'); // "1.4.0" or "a1b2c3d" +``` + ## `build-identity` (CLI) The same three functions, wired together, as a command. It exists for build and deploy steps that are a shell invocation rather than a JavaScript config file, so nothing in them can `import` this package at all -- `wrangler deploy --var RELEASE_VERSION:...` being the case it was built for. A step that *is* a JavaScript config file (`next.config.ts`, `vite.config.ts`) should import the functions directly instead; see [Framework-agnosticism](#framework-agnosticism) below. diff --git a/src/index.ts b/src/index.ts index d147eac..8599923 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,6 @@ export { resolveBuildIdentity } from './resolve-build-identity'; export { resolvePredictedIdentity } from './resolve-predicted-identity'; export { predictNextVersion } from './predict-next-version'; export { loadCommitAnalyzer } from './load-commit-analyzer'; +export { readPackageVersion } from './package-version'; export type { BuildIdentity, DisplayIdentity, ResolveBuildIdentityOptions } from './types'; export type { AnalyzeCommits, PredictNextVersionOptions, ReleaseLevel, ReleaseRule } from './predict-next-version'; diff --git a/src/package-version.test.ts b/src/package-version.test.ts new file mode 100644 index 0000000..3bf70ba --- /dev/null +++ b/src/package-version.test.ts @@ -0,0 +1,47 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { defaultTagName, readPackageVersion } from './package-version'; + +describe('readPackageVersion', () => { + let root: string | undefined; + + afterEach(() => { + if (root !== undefined) rmSync(root, { recursive: true, force: true }); + root = undefined; + }); + + function writePackageJson(contents: string): string { + root = mkdtempSync(join(tmpdir(), 'build-identity-package-version-')); + writeFileSync(join(root, 'package.json'), contents); + return root; + } + + it("reads package.json's own version field, needing no git state at all -- the whole point of exposing this separately from resolveBuildIdentity, whose version is a release-or-commit identity rather than the plain field", () => { + expect(readPackageVersion(writePackageJson(JSON.stringify({ name: 'fixture', version: '1.4.0' })))).toBe('1.4.0'); + }); + + it('throws when package.json is missing entirely', () => { + const emptyRoot = mkdtempSync(join(tmpdir(), 'build-identity-package-version-')); + root = emptyRoot; + expect(() => readPackageVersion(emptyRoot)).toThrow(); + }); + + it('throws rather than defaulting when the version field is absent, the wrong type, or empty', () => { + expect(() => readPackageVersion(writePackageJson(JSON.stringify({ name: 'fixture' })))).toThrow(/non-empty string "version"/); + expect(() => readPackageVersion(writePackageJson(JSON.stringify({ name: 'fixture', version: 140 })))).toThrow(/non-empty string "version"/); + expect(() => readPackageVersion(writePackageJson(JSON.stringify({ name: 'fixture', version: '' })))).toThrow(/non-empty string "version"/); + }); + + it('throws rather than defaulting when package.json is not an object at all', () => { + expect(() => readPackageVersion(writePackageJson('"just a string"'))).toThrow(/non-empty string "version"/); + expect(() => readPackageVersion(writePackageJson('null'))).toThrow(/non-empty string "version"/); + }); +}); + +describe('defaultTagName', () => { + it('prefixes the version with v, the convention resolveBuildIdentity and predictNextVersion share by default', () => { + expect(defaultTagName('1.4.0')).toBe('v1.4.0'); + }); +});