changelog-machine 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +133 -75
  3. package/bin/cli.js +5 -1
  4. package/lib/changelog/contributors.d.ts +2 -0
  5. package/lib/changelog/contributors.js +31 -0
  6. package/lib/changelog/links.d.ts +31 -0
  7. package/lib/changelog/links.js +83 -0
  8. package/lib/changelog/render.d.ts +2 -0
  9. package/lib/changelog/render.js +69 -0
  10. package/lib/changelog/text.d.ts +5 -0
  11. package/lib/changelog/text.js +23 -0
  12. package/lib/changelog/update.d.ts +7 -0
  13. package/lib/changelog/update.js +119 -0
  14. package/lib/cli/files.d.ts +5 -0
  15. package/lib/cli/files.js +35 -0
  16. package/lib/cli/index.d.ts +1 -1
  17. package/lib/cli/index.js +44 -39
  18. package/lib/cli/options.d.ts +13 -0
  19. package/lib/cli/options.js +48 -0
  20. package/lib/cli/plans.d.ts +7 -0
  21. package/lib/cli/plans.js +67 -0
  22. package/lib/cli/progress.d.ts +2 -0
  23. package/lib/cli/progress.js +37 -0
  24. package/lib/commits/parse.d.ts +3 -0
  25. package/lib/commits/parse.js +87 -0
  26. package/lib/contributors/authors.d.ts +6 -0
  27. package/lib/contributors/authors.js +20 -0
  28. package/lib/contributors/privacy.d.ts +2 -0
  29. package/lib/contributors/privacy.js +9 -0
  30. package/lib/contributors/profile.d.ts +5 -0
  31. package/lib/contributors/profile.js +51 -0
  32. package/lib/contributors/repository.d.ts +7 -0
  33. package/lib/contributors/repository.js +36 -0
  34. package/lib/contributors/resolve.d.ts +10 -0
  35. package/lib/contributors/resolve.js +86 -0
  36. package/lib/git/command.d.ts +5 -0
  37. package/lib/git/command.js +39 -0
  38. package/lib/git/commits.d.ts +2 -0
  39. package/lib/git/commits.js +27 -0
  40. package/lib/git/history.d.ts +3 -0
  41. package/lib/git/history.js +41 -0
  42. package/lib/git/refs.d.ts +12 -0
  43. package/lib/git/refs.js +101 -0
  44. package/lib/git/remote.d.ts +1 -0
  45. package/lib/git/remote.js +59 -0
  46. package/lib/git/tags.d.ts +1 -0
  47. package/lib/git/tags.js +27 -0
  48. package/lib/index.d.ts +8 -1
  49. package/lib/index.js +6 -13
  50. package/lib/types.d.ts +31 -0
  51. package/lib/types.js +1 -0
  52. package/lib/version/json.d.ts +5 -0
  53. package/lib/version/json.js +35 -0
  54. package/lib/version/lockfiles.d.ts +3 -0
  55. package/lib/version/lockfiles.js +44 -0
  56. package/lib/version/plan.d.ts +11 -0
  57. package/lib/version/plan.js +51 -0
  58. package/package.json +34 -42
  59. package/lib/cli/commands/printHelp.d.ts +0 -2
  60. package/lib/cli/commands/printHelp.js +0 -15
  61. package/lib/cli/commands/printMarkdown.d.ts +0 -3
  62. package/lib/cli/commands/printMarkdown.js +0 -26
  63. package/lib/cli/commands/printVersion.d.ts +0 -2
  64. package/lib/cli/commands/printVersion.js +0 -12
  65. package/lib/cli/config/options.d.ts +0 -3
  66. package/lib/cli/config/options.js +0 -13
  67. package/lib/cli/config/readConfigFile.d.ts +0 -3
  68. package/lib/cli/config/readConfigFile.js +0 -25
  69. package/lib/core/async.d.ts +0 -9
  70. package/lib/core/async.js +0 -32
  71. package/lib/core/default.d.ts +0 -6
  72. package/lib/core/default.js +0 -9
  73. package/lib/core/git.d.ts +0 -8
  74. package/lib/core/git.js +0 -88
  75. package/lib/core/markdown.d.ts +0 -8
  76. package/lib/core/markdown.js +0 -85
  77. package/lib/core/test-helpers.d.ts +0 -6
  78. package/lib/core/test-helpers.js +0 -14
  79. package/lib/models/commit.d.ts +0 -6
  80. package/lib/models/commit.js +0 -2
  81. package/lib/models/config.d.ts +0 -30
  82. package/lib/models/config.js +0 -2
  83. package/lib/models/index.d.ts +0 -3
  84. package/lib/models/index.js +0 -15
  85. package/lib/models/tagGroup.d.ts +0 -7
  86. package/lib/models/tagGroup.js +0 -2
@@ -0,0 +1,39 @@
1
+ import { spawn } from 'node:child_process';
2
+ export class GitError extends Error {
3
+ exitCode;
4
+ constructor(message, exitCode = null) {
5
+ super(message);
6
+ this.name = 'GitError';
7
+ this.exitCode = exitCode;
8
+ }
9
+ }
10
+ export async function git(args, cwd) {
11
+ return new Promise((resolve, reject) => {
12
+ const child = spawn('git', args, {
13
+ cwd,
14
+ shell: false,
15
+ stdio: ['ignore', 'pipe', 'pipe'],
16
+ env: Object.assign({}, process.env, { ['GIT_TERMINAL_PROMPT']: '0' }),
17
+ });
18
+ let stdout = '';
19
+ let stderr = '';
20
+ child.stdout.setEncoding('utf8');
21
+ child.stderr.setEncoding('utf8');
22
+ child.stdout.on('data', (chunk) => {
23
+ stdout += chunk;
24
+ });
25
+ child.stderr.on('data', (chunk) => {
26
+ stderr += chunk;
27
+ });
28
+ const context = `git ${args.map((arg) => JSON.stringify(arg)).join(' ')} in ${cwd ?? process.cwd()}`;
29
+ child.on('error', (error) => {
30
+ reject(new GitError(`Could not run ${context}: ${error.message}. Check that Git is installed and the working directory exists.`));
31
+ });
32
+ child.on('close', (code, signal) => {
33
+ if (code === 0)
34
+ resolve(stdout);
35
+ else
36
+ reject(new GitError(`${context} failed (${signal ? `signal ${signal}` : `exit ${code}`}): ${stderr.trim() || 'no diagnostic output'}`, code));
37
+ });
38
+ });
39
+ }
@@ -0,0 +1,2 @@
1
+ import type { Commit } from '../types.js';
2
+ export declare function readCommits(range: string, cwd?: string): Promise<Commit[]>;
@@ -0,0 +1,27 @@
1
+ import { git } from './command.js';
2
+ export async function readCommits(range, cwd) {
3
+ const format = '%H%x00%aN%x00%aE%x00%s%x00%b';
4
+ const fieldsPerCommit = 5;
5
+ const output = await git([
6
+ 'log',
7
+ '--reverse',
8
+ '--topo-order',
9
+ '--no-merges',
10
+ '-z',
11
+ `--format=${format}`,
12
+ range,
13
+ '--',
14
+ ], cwd);
15
+ if (!output)
16
+ return [];
17
+ const fields = output.split('\0');
18
+ if (fields.pop() !== '' || fields.length % fieldsPerCommit !== 0) {
19
+ throw new Error('Git returned an invalid NUL-delimited commit log.');
20
+ }
21
+ const commits = [];
22
+ for (let index = 0; index < fields.length; index += fieldsPerCommit) {
23
+ const [hash, name, email, subject, body] = fields.slice(index, index + fieldsPerCommit);
24
+ commits.push({ hash, name, email, subject, body });
25
+ }
26
+ return commits;
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { Release } from '../types.js';
2
+ export declare function getLatestReleaseTag(cwd?: string): Promise<string | undefined>;
3
+ export declare function getReleases(cwd?: string, afterTag?: string): Promise<Release[]>;
@@ -0,0 +1,41 @@
1
+ import { readCommits } from './commits.js';
2
+ import { readGitMetadata, readTagsInHistory, resolveBoundary, validateTagChain, } from './refs.js';
3
+ async function readReleaseRanges(tags, cwd, boundary) {
4
+ const releases = [];
5
+ for (let index = 0; index < tags.length; index++) {
6
+ const current = tags[index];
7
+ const previous = tags[index + 1] ?? (index === tags.length - 1 ? boundary : undefined);
8
+ releases.push({
9
+ tag: current.tag,
10
+ ...(previous ? { previousTag: previous.tag } : {}),
11
+ commits: await readCommits(previous ? `${previous.hash}..${current.hash}` : current.hash, cwd),
12
+ });
13
+ }
14
+ return releases;
15
+ }
16
+ export async function getLatestReleaseTag(cwd) {
17
+ const metadata = await readGitMetadata(cwd);
18
+ const tags = await readTagsInHistory(metadata, cwd, undefined, true);
19
+ await validateTagChain(tags, cwd);
20
+ return tags[0]?.tag;
21
+ }
22
+ export async function getReleases(cwd, afterTag) {
23
+ const metadata = await readGitMetadata(cwd);
24
+ const boundary = afterTag === undefined
25
+ ? undefined
26
+ : await resolveBoundary(afterTag, metadata, cwd);
27
+ const tags = await readTagsInHistory(metadata, cwd, boundary);
28
+ await validateTagChain(tags, cwd, boundary);
29
+ const base = tags[0] ?? boundary;
30
+ const hasUnreleasedRange = base ? metadata.head !== base.hash : true;
31
+ const releases = hasUnreleasedRange || afterTag !== undefined
32
+ ? [
33
+ {
34
+ tag: 'Unreleased',
35
+ ...(base ? { previousTag: base.tag } : {}),
36
+ commits: await readCommits(base ? `${base.hash}..${metadata.head}` : metadata.head, cwd),
37
+ },
38
+ ]
39
+ : [];
40
+ return [...releases, ...(await readReleaseRanges(tags, cwd, boundary))];
41
+ }
@@ -0,0 +1,12 @@
1
+ export type VersionTag = {
2
+ tag: string;
3
+ hash: string;
4
+ };
5
+ export type GitMetadata = {
6
+ head: string;
7
+ tagsByHash: Map<string, string[]>;
8
+ };
9
+ export declare function readGitMetadata(cwd?: string): Promise<GitMetadata>;
10
+ export declare function resolveBoundary(afterTag: string, metadata: GitMetadata, cwd?: string): Promise<VersionTag>;
11
+ export declare function readTagsInHistory(metadata: GitMetadata, cwd?: string, boundary?: VersionTag, decoratedOnly?: boolean): Promise<VersionTag[]>;
12
+ export declare function validateTagChain(tags: VersionTag[], cwd?: string, boundary?: VersionTag): Promise<void>;
@@ -0,0 +1,101 @@
1
+ import { GitError, git } from './command.js';
2
+ import { isVersionTag } from './tags.js';
3
+ async function requireFullHistory(cwd) {
4
+ if ((await git(['rev-parse', '--is-shallow-repository'], cwd)).trim() === 'true') {
5
+ throw new Error('Cannot generate releases from a shallow repository. Fetch the full history with git fetch --unshallow (including tags), then retry.');
6
+ }
7
+ }
8
+ async function readTagRefs(cwd) {
9
+ const output = await git([
10
+ 'for-each-ref',
11
+ '--format=%(refname:strip=2)%00%(objecttype)%00%(objectname)%00%(*objectname)%00',
12
+ 'refs/tags/',
13
+ ], cwd);
14
+ const tagsByHash = new Map();
15
+ const fields = output.split('\0');
16
+ if (fields.at(-1) === '')
17
+ fields.pop();
18
+ for (let index = 0; index + 3 < fields.length; index += 4) {
19
+ const [rawTag, rawType, rawObjectHash, rawPeeledHash] = fields.slice(index, index + 4);
20
+ const tag = rawTag.trim();
21
+ const type = rawType.trim();
22
+ const hash = type === 'tag' ? rawPeeledHash.trim() : rawObjectHash.trim();
23
+ if (!tag || !hash || !isVersionTag(tag))
24
+ continue;
25
+ const names = tagsByHash.get(hash) ?? [];
26
+ names.push(tag);
27
+ tagsByHash.set(hash, names);
28
+ }
29
+ return tagsByHash;
30
+ }
31
+ export async function readGitMetadata(cwd) {
32
+ await requireFullHistory(cwd);
33
+ const [head, tagsByHash] = await Promise.all([
34
+ git(['rev-parse', '--verify', 'HEAD^{commit}'], cwd).then((value) => value.trim()),
35
+ readTagRefs(cwd),
36
+ ]);
37
+ return { head, tagsByHash };
38
+ }
39
+ async function isAncestor(older, newer, cwd) {
40
+ try {
41
+ await git(['merge-base', '--is-ancestor', older, newer], cwd);
42
+ return true;
43
+ }
44
+ catch (error) {
45
+ if (error instanceof GitError && error.exitCode === 1)
46
+ return false;
47
+ throw error;
48
+ }
49
+ }
50
+ export async function resolveBoundary(afterTag, metadata, cwd) {
51
+ if (!isVersionTag(afterTag))
52
+ throw new Error(`Invalid version tag: ${afterTag}`);
53
+ const found = [...metadata.tagsByHash].find(([, names]) => names.includes(afterTag));
54
+ if (!found)
55
+ throw new Error(`Release boundary tag not found: ${afterTag}`);
56
+ const boundary = { tag: afterTag, hash: found[0] };
57
+ if (!(await isAncestor(boundary.hash, metadata.head, cwd))) {
58
+ throw new Error(`Release boundary tag is not reachable from HEAD: ${afterTag}`);
59
+ }
60
+ return boundary;
61
+ }
62
+ export async function readTagsInHistory(metadata, cwd, boundary, decoratedOnly = false) {
63
+ const args = ['rev-list', '--topo-order'];
64
+ if (decoratedOnly)
65
+ args.push('--simplify-by-decoration');
66
+ args.push(metadata.head);
67
+ if (boundary)
68
+ args.push(`^${boundary.hash}`);
69
+ const output = await git(args, cwd);
70
+ const tags = [];
71
+ const encountered = new Set();
72
+ const hashes = output.trim().split('\n');
73
+ if (boundary)
74
+ hashes.push(boundary.hash);
75
+ for (const hash of hashes) {
76
+ if (!hash || encountered.has(hash))
77
+ continue;
78
+ encountered.add(hash);
79
+ const names = metadata.tagsByHash.get(hash);
80
+ if (!names?.length)
81
+ continue;
82
+ if (names.length > 1) {
83
+ throw new Error(`Ambiguous release tags point to the same commit: ${names.join(', ')}.`);
84
+ }
85
+ if (hash !== boundary?.hash)
86
+ tags.push({ tag: names[0], hash });
87
+ }
88
+ return tags;
89
+ }
90
+ export async function validateTagChain(tags, cwd, boundary) {
91
+ for (let index = 1; index < tags.length; index++) {
92
+ if (!(await isAncestor(tags[index].hash, tags[index - 1].hash, cwd))) {
93
+ throw new Error(`Ambiguous release tags: ${tags[index - 1].tag} and ${tags[index].tag} are not ancestors of one another. Release tags must form a single ancestry chain.`);
94
+ }
95
+ }
96
+ if (boundary &&
97
+ tags.length &&
98
+ !(await isAncestor(boundary.hash, tags.at(-1).hash, cwd))) {
99
+ throw new Error(`Ambiguous release tags: ${boundary.tag} and ${tags.at(-1).tag} are not ancestors of one another. Release tags must form a single ancestry chain.`);
100
+ }
101
+ }
@@ -0,0 +1 @@
1
+ export declare function getRepositoryUrl(cwd?: string): Promise<string | undefined>;
@@ -0,0 +1,59 @@
1
+ import { GitError, git } from './command.js';
2
+ const remoteWhitespace = /\s/;
3
+ const supportedRemoteUrlScheme = /^(https|ssh):\/\//i;
4
+ const scpStyleRemote = /^(?:[^@/:]+@)?([^/:]+):(.+)$/;
5
+ const surroundingPathSlashes = /^\/+|\/+$/g;
6
+ const gitPathSuffix = /\.git$/;
7
+ const repositoryPathWithAtLeastTwoSegments = /^[^/]+(?:\/[^/]+)+$/;
8
+ const queryOrFragmentMarker = /[?#]/;
9
+ const supportedRepositoryHosts = ['github.com', 'gitlab.com', 'bitbucket.org'];
10
+ async function readOriginRemote(cwd) {
11
+ try {
12
+ return (await git(['config', '--get', 'remote.origin.url'], cwd)).trim();
13
+ }
14
+ catch (error) {
15
+ if (error instanceof GitError && error.exitCode === 1)
16
+ return undefined;
17
+ throw error;
18
+ }
19
+ }
20
+ function parseRemoteHostAndPath(remote) {
21
+ if (supportedRemoteUrlScheme.test(remote)) {
22
+ try {
23
+ const url = new URL(remote);
24
+ if (url.search || url.hash)
25
+ return undefined;
26
+ return { host: url.hostname.toLowerCase(), path: url.pathname };
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ const match = scpStyleRemote.exec(remote);
33
+ if (!match)
34
+ return undefined;
35
+ const [, host, path] = match;
36
+ return { host: host.toLowerCase(), path };
37
+ }
38
+ function isValidRepositoryPath(path) {
39
+ const hasTraversalSegment = path
40
+ .split('/')
41
+ .some((segment) => segment === '.' || segment === '..');
42
+ return (repositoryPathWithAtLeastTwoSegments.test(path) &&
43
+ !queryOrFragmentMarker.test(path) &&
44
+ !hasTraversalSegment);
45
+ }
46
+ export async function getRepositoryUrl(cwd) {
47
+ const remote = await readOriginRemote(cwd);
48
+ if (!remote || remoteWhitespace.test(remote))
49
+ return undefined;
50
+ const repository = parseRemoteHostAndPath(remote);
51
+ if (!repository || !supportedRepositoryHosts.includes(repository.host))
52
+ return undefined;
53
+ const path = repository.path
54
+ .replace(surroundingPathSlashes, '')
55
+ .replace(gitPathSuffix, '');
56
+ if (!isValidRepositoryPath(path))
57
+ return undefined;
58
+ return `https://${repository.host}/${path}`;
59
+ }
@@ -0,0 +1 @@
1
+ export declare function isVersionTag(tag: string): boolean;
@@ -0,0 +1,27 @@
1
+ const numericIdentifier = /^(0|[1-9]\d*)$/;
2
+ const allowedIdentifierCharacters = /^[A-Za-z0-9-]+$/;
3
+ const onlyDigits = /^\d+$/;
4
+ function validPrerelease(value) {
5
+ return value
6
+ .split('.')
7
+ .every((identifier) => allowedIdentifierCharacters.test(identifier) &&
8
+ (!onlyDigits.test(identifier) || numericIdentifier.test(identifier)));
9
+ }
10
+ function validBuild(value) {
11
+ return value
12
+ .split('.')
13
+ .every((identifier) => allowedIdentifierCharacters.test(identifier));
14
+ }
15
+ export function isVersionTag(tag) {
16
+ const version = tag.startsWith('v') ? tag.slice(1) : tag;
17
+ const [release, build, extraBuild] = version.split('+');
18
+ if (extraBuild !== undefined || (build !== undefined && !validBuild(build)))
19
+ return false;
20
+ const prereleaseStart = release.indexOf('-');
21
+ const core = prereleaseStart < 0 ? release : release.slice(0, prereleaseStart);
22
+ const parts = core.split('.');
23
+ if (parts.length !== 3 ||
24
+ !parts.every((part) => numericIdentifier.test(part)))
25
+ return false;
26
+ return (prereleaseStart < 0 || validPrerelease(release.slice(prereleaseStart + 1)));
27
+ }
package/lib/index.d.ts CHANGED
@@ -1 +1,8 @@
1
- export * from './core/markdown';
1
+ export { renderMarkdown } from './changelog/render.js';
2
+ export { parseCommit, recommendedBump } from './commits/parse.js';
3
+ export { resolveContributors } from './contributors/resolve.js';
4
+ export { getLatestReleaseTag, getReleases } from './git/history.js';
5
+ export { getRepositoryUrl } from './git/remote.js';
6
+ export type { Commit, ContributorProfile, Options, ParsedCommit, Release, } from './types.js';
7
+ export type { VersionPlan } from './version/plan.js';
8
+ export { planVersion } from './version/plan.js';
package/lib/index.js CHANGED
@@ -1,13 +1,6 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
- }) : (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- o[k2] = m[k];
8
- }));
9
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
10
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
11
- };
12
- Object.defineProperty(exports, "__esModule", { value: true });
13
- __exportStar(require("./core/markdown"), exports);
1
+ export { renderMarkdown } from './changelog/render.js';
2
+ export { parseCommit, recommendedBump } from './commits/parse.js';
3
+ export { resolveContributors } from './contributors/resolve.js';
4
+ export { getLatestReleaseTag, getReleases } from './git/history.js';
5
+ export { getRepositoryUrl } from './git/remote.js';
6
+ export { planVersion } from './version/plan.js';
package/lib/types.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ export interface Commit {
2
+ hash: string;
3
+ name: string;
4
+ email: string;
5
+ subject: string;
6
+ body: string;
7
+ }
8
+ export interface ParsedCommit extends Commit {
9
+ type: string;
10
+ scope?: string;
11
+ description: string;
12
+ breaking: boolean;
13
+ breakingNotes: string[];
14
+ pr?: string;
15
+ }
16
+ export interface Release {
17
+ tag: string;
18
+ previousTag?: string;
19
+ commits: Commit[];
20
+ }
21
+ export interface ContributorProfile {
22
+ username: string;
23
+ url: string;
24
+ }
25
+ export interface Options {
26
+ cwd?: string;
27
+ repositoryUrl?: string;
28
+ includeNonConventional?: boolean;
29
+ contributors?: Readonly<Record<string, ContributorProfile>>;
30
+ output?: string;
31
+ }
package/lib/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export type JsonObject = Record<string, unknown>;
2
+ export declare function isObject(value: unknown): value is JsonObject;
3
+ export declare function parseObject(text: string, path: string): JsonObject;
4
+ export declare function formatJson(value: JsonObject, original: string): string;
5
+ export declare function readOptionalFile(path: string): Promise<string | undefined>;
@@ -0,0 +1,35 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ const jsonIndentPattern = /(?:\r?\n)([\t ]+)"/;
3
+ const newlinePattern = /\n/g;
4
+ export function isObject(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ export function parseObject(text, path) {
8
+ let value;
9
+ try {
10
+ value = JSON.parse(text);
11
+ }
12
+ catch {
13
+ throw new Error(`Invalid JSON in ${path}. Fix the file before planning a version bump.`);
14
+ }
15
+ if (!isObject(value)) {
16
+ throw new Error(`Expected a JSON object in ${path}. Fix the file before planning a version bump.`);
17
+ }
18
+ return value;
19
+ }
20
+ export function formatJson(value, original) {
21
+ const newline = original.includes('\r\n') ? '\r\n' : '\n';
22
+ const indent = original.match(jsonIndentPattern)?.[1];
23
+ const content = JSON.stringify(value, null, indent).replace(newlinePattern, newline);
24
+ return content + (original.endsWith('\n') ? newline : '');
25
+ }
26
+ export async function readOptionalFile(path) {
27
+ try {
28
+ return await readFile(path, 'utf8');
29
+ }
30
+ catch (error) {
31
+ if (error.code === 'ENOENT')
32
+ return undefined;
33
+ throw error;
34
+ }
35
+ }
@@ -0,0 +1,3 @@
1
+ import type { VersionPlan } from './plan.js';
2
+ export declare function readLockfiles(cwd: string): Promise<VersionPlan['files']>;
3
+ export declare function updateLockfile(path: string, content: string, next: string): string;
@@ -0,0 +1,44 @@
1
+ import { resolve } from 'node:path';
2
+ import { formatJson, isObject, parseObject, readOptionalFile } from './json.js';
3
+ export async function readLockfiles(cwd) {
4
+ const lockNames = [
5
+ 'package-lock.json',
6
+ 'npm-shrinkwrap.json',
7
+ 'pnpm-lock.yaml',
8
+ 'yarn.lock',
9
+ ];
10
+ const lockTexts = await Promise.all(lockNames.map((name) => readOptionalFile(resolve(cwd, name))));
11
+ if (lockTexts[0] !== undefined && lockTexts[1] !== undefined) {
12
+ throw new Error('Both package-lock.json and npm-shrinkwrap.json exist. Remove the unused lockfile to resolve ambiguity before bumping.');
13
+ }
14
+ const unsupported = lockNames.filter((_, index) => index >= 2 && lockTexts[index] !== undefined);
15
+ if (unsupported.length) {
16
+ throw new Error(`Unsupported lockfile: ${unsupported.join(', ')}. Update it with its package manager instead; only npm lockfiles are supported.`);
17
+ }
18
+ const files = [];
19
+ for (let index = 0; index < 2; index++) {
20
+ const content = lockTexts[index];
21
+ if (content !== undefined)
22
+ files.push({ path: resolve(cwd, lockNames[index]), content });
23
+ }
24
+ return files;
25
+ }
26
+ export function updateLockfile(path, content, next) {
27
+ const lockfile = parseObject(content, path);
28
+ if (![1, 2, 3].includes(lockfile.lockfileVersion)) {
29
+ throw new Error(`Unsupported lockfileVersion in ${path}; expected npm lockfile version 1, 2, or 3.`);
30
+ }
31
+ if (typeof lockfile.version !== 'string') {
32
+ throw new Error(`Malformed ${path}: missing root version. Regenerate the lockfile with npm.`);
33
+ }
34
+ if (lockfile.packages !== undefined || lockfile.lockfileVersion !== 1) {
35
+ if (!isObject(lockfile.packages) ||
36
+ !isObject(lockfile.packages['']) ||
37
+ typeof lockfile.packages[''].version !== 'string') {
38
+ throw new Error(`Malformed ${path}: missing packages[''].version. Regenerate the lockfile with npm.`);
39
+ }
40
+ lockfile.packages[''].version = next;
41
+ }
42
+ lockfile.version = next;
43
+ return formatJson(lockfile, content);
44
+ }
@@ -0,0 +1,11 @@
1
+ import type { Commit } from '../types.js';
2
+ export interface VersionPlan {
3
+ current: string;
4
+ next?: string;
5
+ bump?: 'major' | 'minor' | 'patch';
6
+ files: {
7
+ path: string;
8
+ content: string;
9
+ }[];
10
+ }
11
+ export declare function planVersion(commits: Commit[], latestTag: string | undefined, cwd?: string): Promise<VersionPlan>;
@@ -0,0 +1,51 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { recommendedBump } from '../commits/parse.js';
4
+ import { formatJson, parseObject } from './json.js';
5
+ import { readLockfiles, updateLockfile } from './lockfiles.js';
6
+ const stableVersionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
7
+ const tagPrefixPattern = /^v/;
8
+ function readCurrentVersion(manifest) {
9
+ const current = manifest.version;
10
+ if (typeof current !== 'string' || !stableVersionPattern.test(current)) {
11
+ throw new Error('package.json version must be stable semver (for example 1.2.3); prerelease and build versions are unsupported.');
12
+ }
13
+ return current;
14
+ }
15
+ function validateLatestTag(latestTag, current) {
16
+ if (latestTag !== undefined &&
17
+ latestTag.replace(tagPrefixPattern, '') !== current) {
18
+ throw new Error(`Latest tag ${JSON.stringify(latestTag)} does not match package.json version ${current}. ` +
19
+ 'Tag the current release or restore package.json and its lockfile to the latest tagged version before retrying; refusing a repeated inferred bump.');
20
+ }
21
+ }
22
+ function bumpVersion(current, bump) {
23
+ const parts = current.split('.').map((part) => BigInt(part));
24
+ const bumpIndex = { major: 0, minor: 1, patch: 2 }[bump];
25
+ parts[bumpIndex] += BigInt(1);
26
+ for (let index = bumpIndex + 1; index < parts.length; index++)
27
+ parts[index] = BigInt(0);
28
+ return parts.join('.');
29
+ }
30
+ export async function planVersion(commits, latestTag, cwd = process.cwd()) {
31
+ const packagePath = resolve(cwd, 'package.json');
32
+ const packageText = await readFile(packagePath, 'utf8');
33
+ const manifest = parseObject(packageText, packagePath);
34
+ const current = readCurrentVersion(manifest);
35
+ validateLatestTag(latestTag, current);
36
+ const bump = recommendedBump(commits);
37
+ if (!bump)
38
+ return { current, files: [] };
39
+ const next = bumpVersion(current, bump);
40
+ const lockfiles = await readLockfiles(cwd);
41
+ const files = lockfiles.map(({ path, content }) => ({
42
+ path,
43
+ content: updateLockfile(path, content, next),
44
+ }));
45
+ manifest.version = next;
46
+ files.unshift({
47
+ path: packagePath,
48
+ content: formatJson(manifest, packageText),
49
+ });
50
+ return { current, next, bump, files };
51
+ }
package/package.json CHANGED
@@ -1,60 +1,52 @@
1
1
  {
2
2
  "name": "changelog-machine",
3
- "displayName": "Changelog Machine",
4
- "version": "1.1.0",
5
- "description": "Changelog generator based on Git commit messages and tags",
3
+ "version": "2.0.0",
4
+ "description": "Minimal, dependency-free conventional changelog and version generator",
5
+ "type": "module",
6
6
  "main": "./lib/index.js",
7
- "module": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "import": "./lib/index.js"
12
+ }
13
+ },
8
14
  "bin": {
9
15
  "changelog-machine": "./bin/cli.js"
10
16
  },
11
- "scripts": {
12
- "build": "tsc -p tsconfig.json",
13
- "test": "jest",
14
- "lint": "eslint -c .eslintrc.json --ext .ts ./src/**/*.ts",
15
- "changelog": "ts-node src/cli/index.ts --config changelog.config.json",
16
- "prepublish": "npm run build",
17
- "version": "npm run changelog && git add CHANGELOG.md"
18
- },
19
17
  "files": [
20
- "lib/"
21
- ],
22
- "keywords": [
23
- "Changelog",
24
- "Generator"
18
+ "lib",
19
+ "bin",
20
+ "LICENSE.md"
25
21
  ],
26
22
  "engines": {
27
- "node": ">=4.2.0"
23
+ "node": ">=24"
28
24
  },
29
- "author": {
30
- "name": "Philipp Kief",
31
- "email": "philipp.kief@gmx.de",
32
- "url": "https://pkief.github.io"
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.build.json",
27
+ "test": "tsc --noEmit && node --import ./scripts/test-loader.js --test src/*/*.test.ts",
28
+ "lint": "biome check src scripts",
29
+ "changelog": "npm run build && node bin/cli.js --full",
30
+ "prepack": "npm run build"
33
31
  },
34
- "funding": "https://github.com/sponsors/pkief",
32
+ "keywords": [
33
+ "changelog",
34
+ "conventional-commits",
35
+ "git"
36
+ ],
37
+ "author": "Philipp Kief <philipp.kief@gmx.de>",
35
38
  "license": "MIT",
36
- "bugs": {
37
- "url": "https://github.com/PKief/changelog-machine/issues"
38
- },
39
- "homepage": "https://github.com/PKief/changelog-machine/blob/main/README.md",
40
39
  "repository": {
41
40
  "type": "git",
42
41
  "url": "git+https://github.com/PKief/changelog-machine.git"
43
42
  },
44
- "devDependencies": {
45
- "@types/jest": "^27.4.0",
46
- "@types/minimist": "^1.2.2",
47
- "@typescript-eslint/eslint-plugin": "^5.4.0",
48
- "@typescript-eslint/parser": "^5.4.0",
49
- "eslint": "^8.3.0",
50
- "eslint-config-prettier": "^8.3.0",
51
- "eslint-plugin-prettier": "^4.0.0",
52
- "jest": "^27.5.0",
53
- "ts-jest": "^27.1.3",
54
- "ts-node": "^10.4.0",
55
- "typescript": "^4.5.5"
43
+ "bugs": {
44
+ "url": "https://github.com/PKief/changelog-machine/issues"
56
45
  },
57
- "dependencies": {
58
- "minimist": "^1.2.5"
46
+ "funding": "https://github.com/sponsors/pkief",
47
+ "devDependencies": {
48
+ "@biomejs/biome": "^2.6.3",
49
+ "@types/node": "^24.0.0",
50
+ "typescript": "^7.0.2"
59
51
  }
60
- }
52
+ }
@@ -1,2 +0,0 @@
1
- declare const printHelp: () => void;
2
- export { printHelp };