Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
47 changes: 47 additions & 0 deletions src/package-version.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});