@sr-labs/devkit 0.1.0 → 0.1.3

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 (54) hide show
  1. package/README.md +6 -66
  2. package/dist/cli.d.ts +2 -0
  3. package/{packages/changelog/src/cli.ts → dist/cli.js} +1 -2
  4. package/dist/commands.d.ts +7 -0
  5. package/dist/commands.js +7 -0
  6. package/dist/index.js +2 -0
  7. package/dist/main.d.ts +2 -0
  8. package/dist/main.js +24 -0
  9. package/package.json +26 -14
  10. package/.gitlab-ci.yml +0 -24
  11. package/.nvmrc +0 -1
  12. package/AGENTS.md +0 -15
  13. package/CHANGELOG.md +0 -17
  14. package/docs/agents/domain.md +0 -51
  15. package/docs/agents/issue-tracker.md +0 -46
  16. package/docs/agents/triage-labels.md +0 -15
  17. package/docs/specs/devkit-rename-and-layout.md +0 -71
  18. package/packages/changelog/README.md +0 -7
  19. package/packages/changelog/package.json +0 -33
  20. package/packages/changelog/src/commits.ts +0 -29
  21. package/packages/changelog/src/git.ts +0 -11
  22. package/packages/changelog/src/index.ts +0 -4
  23. package/packages/changelog/src/main.ts +0 -33
  24. package/packages/changelog/src/notes.ts +0 -60
  25. package/packages/changelog/test/notes.test.ts +0 -47
  26. package/packages/changelog/tsconfig.json +0 -6
  27. package/packages/core/README.md +0 -3
  28. package/packages/core/package.json +0 -27
  29. package/packages/core/src/git.ts +0 -41
  30. package/packages/core/src/index.ts +0 -1
  31. package/packages/core/tsconfig.json +0 -5
  32. package/packages/devkit/README.md +0 -11
  33. package/packages/devkit/package.json +0 -34
  34. package/packages/devkit/src/cli.ts +0 -4
  35. package/packages/devkit/src/commands.ts +0 -14
  36. package/packages/devkit/src/main.ts +0 -27
  37. package/packages/devkit/test/cli.test.ts +0 -74
  38. package/packages/devkit/tsconfig.json +0 -6
  39. package/packages/release-tag/README.md +0 -6
  40. package/packages/release-tag/package.json +0 -34
  41. package/packages/release-tag/src/cli.ts +0 -4
  42. package/packages/release-tag/src/index.ts +0 -3
  43. package/packages/release-tag/src/main.ts +0 -79
  44. package/packages/release-tag/src/package-files.ts +0 -92
  45. package/packages/release-tag/src/version.ts +0 -41
  46. package/packages/release-tag/test/cli.test.ts +0 -68
  47. package/packages/release-tag/test/package-files.test.ts +0 -70
  48. package/packages/release-tag/test/version.test.ts +0 -32
  49. package/packages/release-tag/tsconfig.json +0 -6
  50. package/pnpm-workspace.yaml +0 -2
  51. package/tsconfig.base.json +0 -18
  52. package/tsconfig.json +0 -9
  53. package/tsconfig.test.json +0 -10
  54. /package/{packages/devkit/src/index.ts → dist/index.d.ts} +0 -0
@@ -1,47 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { parseCommit } from '../src/commits.ts';
3
- import { formatChangelog, formatSection, formatTagNotes, prependSection } from '../src/notes.ts';
4
-
5
- const c = (subject: string, body = '') => ({ subject, body });
6
-
7
- describe('parseCommit', () => {
8
- it('parses type, scope and breaking markers', () => {
9
- expect(parseCommit(c('feat(ui)!: x'))).toEqual({
10
- type: 'feat',
11
- scope: 'ui',
12
- description: 'x',
13
- breaking: true,
14
- breakingNote: undefined
15
- });
16
- expect(parseCommit(c('fix: y', 'details\n\nBREAKING CHANGE: drops z\nmore'))?.breakingNote).toBe('drops z');
17
- expect(parseCommit(c('Merge ticket 04: reverb'))).toBeNull();
18
- });
19
- });
20
-
21
- describe('notes', () => {
22
- it('groups tag notes', () => {
23
- const n = formatTagNotes('v0.3.0', [c('feat(engine): x'), c('fix: y'), c('docs: z')]);
24
- expect(n).toBe('v0.3.0\n\nFeatures:\n- engine: x\n\nFixes:\n- y\n');
25
- });
26
-
27
- it('lists breaking changes first, using the footer text when present', () => {
28
- const n = formatTagNotes('v1.0.0', [c('feat(api)!: new x'), c('fix: y', 'BREAKING CHANGE: y renamed')]);
29
- expect(n).toBe('v1.0.0\n\nBreaking Changes:\n- api: new x\n- y renamed\n\nFeatures:\n- api: new x\n\nFixes:\n- y\n');
30
- });
31
-
32
- it('formats markdown sections and a whole changelog', () => {
33
- const r = { tag: 'v0.2.0', date: '2026-10-01', commits: [c('perf: faster'), c('chore: tidy')] };
34
- expect(formatSection(r)).toBe('## v0.2.0 - 2026-10-01\n\n### Performance\n\n- faster\n');
35
- expect(formatSection({ tag: 'Unreleased', date: '', commits: [] })).toBe('## Unreleased\n');
36
- expect(formatChangelog([r])).toBe('# Changelog\n\n## v0.2.0 - 2026-10-01\n\n### Performance\n\n- faster\n');
37
- });
38
-
39
- it('prepends above the newest release, keeping the preamble', () => {
40
- const existing = '# Changelog\n\nHand-written intro.\n\n## v0.1.0 - 2026-01-01\n\n- old\n';
41
- expect(prependSection(existing, '## v0.2.0 - 2026-02-01\n')).toBe(
42
- '# Changelog\n\nHand-written intro.\n\n## v0.2.0 - 2026-02-01\n\n## v0.1.0 - 2026-01-01\n\n- old\n'
43
- );
44
- expect(prependSection('', '## v0.1.0\n')).toBe('# Changelog\n\n## v0.1.0\n');
45
- expect(prependSection('# Changelog\n', '## v0.1.0\n')).toBe('# Changelog\n\n## v0.1.0\n');
46
- });
47
- });
@@ -1,6 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": { "rootDir": "src", "outDir": "dist" },
4
- "include": ["src"],
5
- "references": [{ "path": "../core" }]
6
- }
@@ -1,3 +0,0 @@
1
- # @sr-labs/core
2
-
3
- Shared git helpers (`git`, `listVersionTags`, `readCommits`) used by the [devkit](../../README.md) tools. Library only, no bin.
@@ -1,27 +0,0 @@
1
- {
2
- "name": "@sr-labs/core",
3
- "version": "0.1.0",
4
- "description": "Shared git helpers for @sr-labs/devkit tools.",
5
- "type": "module",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://gitlab.com/sr-labs/devkit.git",
10
- "directory": "packages/core"
11
- },
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
- "exports": {
16
- ".": {
17
- "types": "./dist/index.d.ts",
18
- "default": "./dist/index.js"
19
- }
20
- },
21
- "files": [
22
- "dist"
23
- ],
24
- "engines": {
25
- "node": ">=20"
26
- }
27
- }
@@ -1,41 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
-
3
- export interface Commit {
4
- subject: string;
5
- body: string;
6
- }
7
-
8
- export const git = (...args: string[]) => execFileSync('git', args, { encoding: 'utf8' }).trim();
9
-
10
- const VERSION_TAG = /^v\d+\.\d+\.\d+$/;
11
-
12
- export interface Tag {
13
- name: string;
14
- /** Tag creation date, YYYY-MM-DD. */
15
- date: string;
16
- }
17
-
18
- /** vX.Y.Z tags, newest version first. */
19
- export function listVersionTags(): Tag[] {
20
- return git('for-each-ref', '--sort=-v:refname', '--format=%(refname:lstrip=2)%09%(creatordate:short)', 'refs/tags/v*')
21
- .split('\n')
22
- .map((line) => {
23
- const [name, date = ''] = line.split('\t');
24
- return { name, date };
25
- })
26
- .filter((t) => VERSION_TAG.test(t.name));
27
- }
28
-
29
- const SEP = '\x1e';
30
-
31
- /** Commits in a `git log` range, newest first. */
32
- export function readCommits(range: string): Commit[] {
33
- return git('log', range, `--format=%s%x1f%b${SEP}`)
34
- .split(SEP)
35
- .map((s) => s.trim())
36
- .filter(Boolean)
37
- .map((s) => {
38
- const [subject, body = ''] = s.split('\x1f');
39
- return { subject, body };
40
- });
41
- }
@@ -1 +0,0 @@
1
- export * from './git.ts';
@@ -1,5 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": { "rootDir": "src", "outDir": "dist" },
4
- "include": ["src"]
5
- }
@@ -1,11 +0,0 @@
1
- # @sr-labs/devkit
2
-
3
- One `devkit` command for the tools that set up a project's development environment.
4
-
5
- ```sh
6
- devkit release [--apply] [--push] ... # same as release-tag
7
- devkit changelog [--stdout] ... # same as changelog
8
- devkit --help | --version
9
- ```
10
-
11
- An unknown subcommand prints an error and the help text and exits non-zero. See the [devkit README](../../README.md).
@@ -1,34 +0,0 @@
1
- {
2
- "name": "@sr-labs/devkit",
3
- "version": "0.1.0",
4
- "description": "One devkit command for project development-environment tools: release, changelog.",
5
- "type": "module",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://gitlab.com/sr-labs/devkit.git",
10
- "directory": "packages/devkit"
11
- },
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
- "exports": {
16
- ".": {
17
- "types": "./dist/index.d.ts",
18
- "default": "./dist/index.js"
19
- }
20
- },
21
- "bin": {
22
- "devkit": "./dist/cli.js"
23
- },
24
- "files": [
25
- "dist"
26
- ],
27
- "engines": {
28
- "node": ">=20"
29
- },
30
- "dependencies": {
31
- "@sr-labs/changelog": "workspace:^",
32
- "@sr-labs/release-tag": "workspace:^"
33
- }
34
- }
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- import { main } from './main.ts';
3
-
4
- process.exitCode = main(process.argv.slice(2));
@@ -1,14 +0,0 @@
1
- import { main as changelog } from '@sr-labs/changelog';
2
- import { main as release } from '@sr-labs/release-tag';
3
-
4
- export interface Command {
5
- description: string;
6
- /** Runs the tool with the remaining args and returns the exit code. */
7
- run: (argv: string[]) => number;
8
- }
9
-
10
- /** Subcommand registry. Adding a tool is one entry. */
11
- export const commands: Record<string, Command> = {
12
- release: { description: 'Compute the next vX.Y.Z, bump files, update CHANGELOG.md, commit and tag', run: release },
13
- changelog: { description: 'Generate CHANGELOG.md from Conventional Commits between tags', run: changelog }
14
- };
@@ -1,27 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { commands } from './commands.ts';
3
-
4
- const help = () =>
5
- `Usage: devkit <command> [options]\n\nCommands:\n${Object.entries(commands)
6
- .map(([name, c]) => ` ${name.padEnd(10)}${c.description}`)
7
- .join('\n')}\n\nOptions:\n --help, -h Show this help\n --version, -v Show the devkit version\n`;
8
-
9
- /** Runs the devkit CLI with args (without node/script) and returns the exit code. */
10
- export function main(argv: string[]): number {
11
- const [name, ...rest] = argv;
12
- if (name === '--help' || name === '-h' || name === undefined) {
13
- process.stdout.write(help());
14
- return 0;
15
- }
16
- if (name === '--version' || name === '-v') {
17
- const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
18
- console.log(pkg.version);
19
- return 0;
20
- }
21
- if (!Object.hasOwn(commands, name)) {
22
- console.error(`Unknown command: ${name}\n`);
23
- process.stderr.write(help());
24
- return 1;
25
- }
26
- return commands[name].run(rest);
27
- }
@@ -1,74 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { beforeEach, describe, expect, it } from 'vitest';
6
-
7
- // Runs the built CLIs (pnpm test builds first) against a throwaway repo.
8
- const devkit = join(import.meta.dirname, '../dist/cli.js');
9
- const releaseTag = join(import.meta.dirname, '../../release-tag/dist/cli.js');
10
- const changelog = join(import.meta.dirname, '../../changelog/dist/cli.js');
11
- const env = {
12
- ...process.env,
13
- GIT_AUTHOR_NAME: 't',
14
- GIT_AUTHOR_EMAIL: 't@t',
15
- GIT_COMMITTER_NAME: 't',
16
- GIT_COMMITTER_EMAIL: 't@t',
17
- GIT_CONFIG_GLOBAL: '/dev/null'
18
- };
19
-
20
- let dir: string;
21
- const run = (cmd: string, ...args: string[]) => execFileSync(cmd, args, { cwd: dir, env, encoding: 'utf8', stdio: 'pipe' });
22
- const commit = (msg: string) => run('git', 'commit', '-q', '--allow-empty', '-m', msg);
23
-
24
- beforeEach(() => {
25
- dir = mkdtempSync(join(tmpdir(), 'devkit-cli-'));
26
- run('git', 'init', '-q', '-b', 'main');
27
- writeFileSync(join(dir, 'package.json'), '{\n "name": "x",\n "version": "0.0.0"\n}\n');
28
- run('git', 'add', '.');
29
- commit('chore: init');
30
- });
31
-
32
- describe('devkit cli', () => {
33
- it('release dry run matches release-tag and changes nothing', () => {
34
- commit('feat: a');
35
- const out = run('node', devkit, 'release');
36
- expect(out).toBe(run('node', releaseTag));
37
- expect(out).toContain('(none) -> v0.1.0 (minor, 2 commits)');
38
- expect(run('git', 'tag')).toBe('');
39
- });
40
-
41
- it('release --apply tags and writes the changelog; changelog regenerates the same output', () => {
42
- commit('feat(ui): a');
43
- run('node', devkit, 'release', '--apply');
44
- expect(run('git', 'tag', '--list')).toBe('v0.1.0\n');
45
- expect(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version).toBe('0.1.0');
46
-
47
- const written = readFileSync(join(dir, 'CHANGELOG.md'), 'utf8');
48
- expect(run('node', devkit, 'changelog', '--stdout')).toBe(written);
49
- expect(run('node', devkit, 'changelog', '--stdout')).toBe(run('node', changelog, '--stdout'));
50
- });
51
-
52
- it('unknown subcommand exits non-zero with an error and help', () => {
53
- try {
54
- run('node', devkit, 'nope');
55
- expect.unreachable();
56
- } catch (e) {
57
- const err = e as { status: number; stderr: string };
58
- expect(err.status).toBe(1);
59
- expect(err.stderr).toContain('Unknown command: nope');
60
- expect(err.stderr).toContain('Usage: devkit');
61
- }
62
- });
63
-
64
- it('--help lists subcommands', () => {
65
- const out = run('node', devkit, '--help');
66
- expect(out).toContain('release');
67
- expect(out).toContain('changelog');
68
- });
69
-
70
- it('--version prints the umbrella version', () => {
71
- const pkg = JSON.parse(readFileSync(join(import.meta.dirname, '../package.json'), 'utf8'));
72
- expect(run('node', devkit, '--version')).toBe(`${pkg.version}\n`);
73
- });
74
- });
@@ -1,6 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": { "rootDir": "src", "outDir": "dist" },
4
- "include": ["src"],
5
- "references": [{ "path": "../changelog" }, { "path": "../release-tag" }]
6
- }
@@ -1,6 +0,0 @@
1
- # @sr-labs/release-tag
2
-
3
- `release-tag [--apply] [--push] [--no-changelog] [--changelog CHANGELOG.md]` computes the next `vX.Y.Z` from
4
- Conventional Commits since the latest tag. Dry run by default; `--apply` bumps package files (npm workspaces
5
- included), updates `CHANGELOG.md`, commits `chore(release): vX.Y.Z` and creates an annotated tag.
6
- See the [devkit README](../../README.md).
@@ -1,34 +0,0 @@
1
- {
2
- "name": "@sr-labs/release-tag",
3
- "version": "0.1.0",
4
- "description": "Compute the next vX.Y.Z from Conventional Commits, bump package files, update CHANGELOG.md, commit and tag.",
5
- "type": "module",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://gitlab.com/sr-labs/devkit.git",
10
- "directory": "packages/release-tag"
11
- },
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
- "exports": {
16
- ".": {
17
- "types": "./dist/index.d.ts",
18
- "default": "./dist/index.js"
19
- }
20
- },
21
- "bin": {
22
- "release-tag": "./dist/cli.js"
23
- },
24
- "files": [
25
- "dist"
26
- ],
27
- "engines": {
28
- "node": ">=20"
29
- },
30
- "dependencies": {
31
- "@sr-labs/changelog": "workspace:^",
32
- "@sr-labs/core": "workspace:^"
33
- }
34
- }
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- import { main } from './main.ts';
3
-
4
- process.exitCode = main(process.argv.slice(2));
@@ -1,3 +0,0 @@
1
- export { main } from './main.ts';
2
- export * from './package-files.ts';
3
- export * from './version.ts';
@@ -1,79 +0,0 @@
1
- // Usage: release-tag [--apply] [--push] [--no-changelog] [--changelog CHANGELOG.md]
2
- // Computes the next vX.Y.Z from Conventional Commits since the latest tag.
3
- // Dry run by default; --apply bumps package.json/package-lock.json (and npm/pnpm workspace packages),
4
- // prepends the release to CHANGELOG.md, commits, and creates an annotated tag; --push also pushes the tag.
5
- import { execFileSync } from 'node:child_process';
6
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
7
- import { parseArgs } from 'node:util';
8
- import {
9
- formatChangelog,
10
- formatSection,
11
- formatTagNotes,
12
- prependSection,
13
- readReleases,
14
- type Release
15
- } from '@sr-labs/changelog';
16
- import { git, listVersionTags, readCommits } from '@sr-labs/core';
17
- import { readWorkspaces, setPackageVersion, versionFiles } from './package-files.ts';
18
- import { applyBump, decideBump, formatVersion, parseVersion } from './version.ts';
19
-
20
- /** Runs the release-tag CLI with args (without node/script) and returns the exit code. */
21
- export function main(argv: string[]): number {
22
- const { values } = parseArgs({
23
- args: argv,
24
- options: {
25
- apply: { type: 'boolean', default: false },
26
- push: { type: 'boolean', default: false },
27
- 'no-changelog': { type: 'boolean', default: false },
28
- changelog: { type: 'string', default: 'CHANGELOG.md' }
29
- }
30
- });
31
- const push = values.push;
32
- const apply = values.apply || push;
33
-
34
- const tags = listVersionTags();
35
- const last = tags[0]?.name;
36
- const current = (last && parseVersion(last)) || { major: 0, minor: 0, patch: 0 };
37
- const commits = readCommits(last ? `${last}..HEAD` : 'HEAD');
38
-
39
- const bump = decideBump(commits, current);
40
- if (!bump) {
41
- console.log(`No feat/fix/perf commits since ${last ?? 'start'}. No tag.`);
42
- return 0;
43
- }
44
-
45
- const next = formatVersion(applyBump(current, bump));
46
- const notes = formatTagNotes(next, commits);
47
- console.log(`${last ?? '(none)'} -> ${next} (${bump}, ${commits.length} commits)\n\n${notes}`);
48
-
49
- if (!apply) {
50
- console.log('Dry run. Pass --apply to bump, commit and tag, --push to also push it.');
51
- return 0;
52
- }
53
- if (git('status', '--porcelain', '--untracked-files=no')) {
54
- console.error('Working tree dirty. Commit or stash first.');
55
- return 1;
56
- }
57
-
58
- const root = process.cwd();
59
- const workspaces = readWorkspaces(root);
60
- const files = versionFiles(root, workspaces);
61
- for (const f of files) writeFileSync(f, setPackageVersion(readFileSync(f, 'utf8'), next, workspaces));
62
-
63
- if (!values['no-changelog']) {
64
- const file = values.changelog;
65
- const release: Release = { tag: next, date: new Date().toLocaleDateString('sv-SE'), commits };
66
- const text = existsSync(file)
67
- ? prependSection(readFileSync(file, 'utf8'), formatSection(release))
68
- : formatChangelog([release, ...readReleases(tags)]);
69
- writeFileSync(file, text);
70
- files.push(file);
71
- }
72
-
73
- execFileSync('git', ['add', ...files], { stdio: 'inherit' });
74
- execFileSync('git', ['commit', '-m', `chore(release): ${next}`], { stdio: 'inherit' });
75
- execFileSync('git', ['tag', '-a', next, '-m', notes], { stdio: 'inherit' });
76
- console.log(`Created ${next}`);
77
- if (push) execFileSync('git', ['push', 'origin', next], { stdio: 'inherit' });
78
- return 0;
79
- }
@@ -1,92 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
-
4
- export interface Workspace {
5
- /** Path relative to the repo root, as npm writes it in package-lock.json. */
6
- path: string;
7
- name: string;
8
- }
9
-
10
- const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const;
11
-
12
- type Manifest = { version?: string } & Partial<Record<(typeof DEP_FIELDS)[number], Record<string, string>>>;
13
-
14
- function bumpManifest(m: Manifest, version: string, internal: ReadonlySet<string>): void {
15
- m.version = version;
16
- for (const field of DEP_FIELDS) {
17
- const deps = m[field];
18
- for (const name of Object.keys(deps ?? {})) {
19
- // `workspace:` ranges are rewritten by pnpm/yarn at publish time.
20
- if (deps && internal.has(name) && !deps[name].startsWith('workspace:')) deps[name] = `^${version}`;
21
- }
22
- }
23
- }
24
-
25
- /**
26
- * Rewrites the version of a package.json / package-lock.json text: top-level "version", the lockfile's
27
- * root and workspace entries, and ranges on sibling workspace packages (released in lockstep).
28
- */
29
- export function setPackageVersion(text: string, tag: string, workspaces: Workspace[] = []): string {
30
- const version = tag.replace(/^v/, '');
31
- const internal = new Set(workspaces.map((w) => w.name));
32
- const json = JSON.parse(text) as Manifest & { packages?: Record<string, Manifest> };
33
- bumpManifest(json, version, internal);
34
- for (const path of ['', ...workspaces.map((w) => w.path)]) {
35
- const entry = json.packages?.[path];
36
- if (entry) bumpManifest(entry, version, internal);
37
- }
38
- const indent = /^(\s+)"/m.exec(text)?.[1] ?? ' ';
39
- return JSON.stringify(json, null, indent) + '\n';
40
- }
41
-
42
- /** `packages:` list of a pnpm-workspace.yaml (block-list form). */
43
- export function parsePnpmWorkspace(yaml: string): string[] {
44
- const patterns: string[] = [];
45
- let inPackages = false;
46
- for (const line of yaml.split('\n')) {
47
- if (/^packages:\s*(#.*)?$/.test(line)) inPackages = true;
48
- else if (inPackages) {
49
- const m = /^\s+-\s*(['"]?)(.+?)\1\s*(#.*)?$/.exec(line);
50
- if (m) patterns.push(m[2]);
51
- else if (/^\S/.test(line)) break;
52
- }
53
- }
54
- return patterns;
55
- }
56
-
57
- /**
58
- * Workspaces from pnpm-workspace.yaml, else the root package.json `workspaces`.
59
- * Supports literal paths and `dir/*` patterns; `!path` excludes a literal path.
60
- */
61
- export function readWorkspaces(root: string): Workspace[] {
62
- const pnpmFile = join(root, 'pnpm-workspace.yaml');
63
- let patterns: string[];
64
- if (existsSync(pnpmFile)) patterns = parsePnpmWorkspace(readFileSync(pnpmFile, 'utf8'));
65
- else {
66
- const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as {
67
- workspaces?: string[] | { packages?: string[] };
68
- };
69
- patterns = (Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages) ?? [];
70
- }
71
- const clean = (p: string) => p.replace(/^!/, '').replace(/^\.\//, '').replace(/\/$/, '');
72
- const excluded = new Set(patterns.filter((p) => p.startsWith('!')).map(clean));
73
- return patterns
74
- .filter((p) => !p.startsWith('!'))
75
- .map(clean)
76
- .flatMap((p) => {
77
- if (!p.endsWith('/*')) return [p];
78
- const dir = p.slice(0, -2);
79
- return readdirSync(join(root, dir), { withFileTypes: true })
80
- .filter((d) => d.isDirectory())
81
- .map((d) => `${dir}/${d.name}`);
82
- })
83
- .filter((path) => !excluded.has(path) && existsSync(join(root, path, 'package.json')))
84
- .map((path) => ({ path, name: (JSON.parse(readFileSync(join(root, path, 'package.json'), 'utf8')) as { name: string }).name }));
85
- }
86
-
87
- /** Files whose version a release rewrites, relative to the root. */
88
- export function versionFiles(root: string, workspaces: Workspace[]): string[] {
89
- return ['package.json', 'package-lock.json', ...workspaces.map((w) => `${w.path}/package.json`)].filter((f) =>
90
- existsSync(join(root, f))
91
- );
92
- }
@@ -1,41 +0,0 @@
1
- import { parseCommit, type Commit } from '@sr-labs/changelog';
2
-
3
- export type Bump = 'major' | 'minor' | 'patch';
4
-
5
- export interface Version {
6
- major: number;
7
- minor: number;
8
- patch: number;
9
- }
10
-
11
- const RELEASE_TYPES = new Set(['feat', 'fix', 'perf']);
12
-
13
- export function parseVersion(tag: string): Version | null {
14
- const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
15
- return m ? { major: +m[1], minor: +m[2], patch: +m[3] } : null;
16
- }
17
-
18
- export function formatVersion(v: Version): string {
19
- return `v${v.major}.${v.minor}.${v.patch}`;
20
- }
21
-
22
- /**
23
- * Conventional Commits to bump. Pre-1.0 a breaking change bumps minor, not major.
24
- * Returns null when nothing release-worthy (feat/fix/perf/breaking) is present.
25
- */
26
- export function decideBump(commits: Commit[], current: Version): Bump | null {
27
- let bump: Bump | null = null;
28
- for (const p of commits.map(parseCommit)) {
29
- if (!p) continue;
30
- if (p.breaking) return current.major === 0 ? 'minor' : 'major';
31
- if (p.type === 'feat') bump = 'minor';
32
- else if (RELEASE_TYPES.has(p.type) && bump === null) bump = 'patch';
33
- }
34
- return bump;
35
- }
36
-
37
- export function applyBump(v: Version, bump: Bump): Version {
38
- if (bump === 'major') return { major: v.major + 1, minor: 0, patch: 0 };
39
- if (bump === 'minor') return { major: v.major, minor: v.minor + 1, patch: 0 };
40
- return { major: v.major, minor: v.minor, patch: v.patch + 1 };
41
- }
@@ -1,68 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { beforeEach, describe, expect, it } from 'vitest';
6
-
7
- // Runs the built CLIs (npm test builds first) against a throwaway repo.
8
- const releaseTag = join(import.meta.dirname, '../dist/cli.js');
9
- const changelog = join(import.meta.dirname, '../../changelog/dist/cli.js');
10
- const env = {
11
- ...process.env,
12
- GIT_AUTHOR_NAME: 't',
13
- GIT_AUTHOR_EMAIL: 't@t',
14
- GIT_COMMITTER_NAME: 't',
15
- GIT_COMMITTER_EMAIL: 't@t',
16
- GIT_CONFIG_GLOBAL: '/dev/null'
17
- };
18
-
19
- let dir: string;
20
- const run = (cmd: string, ...args: string[]) => execFileSync(cmd, args, { cwd: dir, env, encoding: 'utf8', stdio: 'pipe' });
21
- const commit = (msg: string) => run('git', 'commit', '-q', '--allow-empty', '-m', msg);
22
- const today = new Date().toLocaleDateString('sv-SE');
23
-
24
- beforeEach(() => {
25
- dir = mkdtempSync(join(tmpdir(), 'release-tag-cli-'));
26
- run('git', 'init', '-q', '-b', 'main');
27
- writeFileSync(join(dir, 'package.json'), '{\n "name": "x",\n "version": "0.0.0"\n}\n');
28
- run('git', 'add', '.');
29
- commit('chore: init');
30
- });
31
-
32
- describe('release-tag cli', () => {
33
- it('dry run changes nothing', () => {
34
- commit('feat: a');
35
- expect(run('node', releaseTag)).toContain('(none) -> v0.1.0 (minor, 2 commits)');
36
- expect(run('git', 'tag')).toBe('');
37
- });
38
-
39
- it('applies: bumps, writes changelog, commits and tags; changelog cli regenerates the same history', () => {
40
- commit('feat(ui): a');
41
- run('node', releaseTag, '--apply');
42
- commit('fix: b');
43
- commit('docs: c');
44
- run('node', releaseTag, '--apply');
45
-
46
- expect(run('git', 'tag', '--list')).toBe('v0.1.0\nv0.1.1\n');
47
- expect(run('git', 'log', '-1', '--format=%s')).toBe('chore(release): v0.1.1\n');
48
- expect(run('git', 'tag', '-l', '--format=%(contents)', 'v0.1.1')).toBe('v0.1.1\n\nFixes:\n- b\n\n');
49
- expect(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version).toBe('0.1.1');
50
-
51
- const written = readFileSync(join(dir, 'CHANGELOG.md'), 'utf8');
52
- expect(written).toBe(
53
- `# Changelog\n\n## v0.1.1 - ${today}\n\n### Fixes\n\n- b\n\n## v0.1.0 - ${today}\n\n### Features\n\n- ui: a\n`
54
- );
55
- expect(run('node', changelog, '--stdout')).toBe(written);
56
-
57
- commit('feat: d');
58
- expect(run('node', changelog, '--stdout', '--unreleased')).toBe(
59
- `# Changelog\n\n## Unreleased\n\n### Features\n\n- d\n\n${written.replace('# Changelog\n\n', '')}`
60
- );
61
- });
62
-
63
- it('refuses a dirty tree', () => {
64
- commit('fix: a');
65
- writeFileSync(join(dir, 'package.json'), '{}');
66
- expect(() => run('node', releaseTag, '--apply')).toThrow(/Working tree dirty/);
67
- });
68
- });