bunset 0.0.2

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.
package/src/index.ts ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { resolveOptions } from "./cli.ts";
4
+ import { loadConfig } from "./config.ts";
5
+ import {
6
+ parseCommit,
7
+ groupCommits,
8
+ filterCommitsForPackage,
9
+ } from "./commits.ts";
10
+ import { buildChangelogEntry, writeChangelog } from "./changelog.ts";
11
+ import {
12
+ getLastTag,
13
+ getCommitsSince,
14
+ getCommitFiles,
15
+ commitAndTag,
16
+ } from "./git.ts";
17
+ import { getUpdatedDependencies } from "./deps.ts";
18
+ import {
19
+ isWorkspace,
20
+ getAllPackages,
21
+ getChangedPackages,
22
+ } from "./workspace.ts";
23
+ import { bumpVersion, updatePackageVersion } from "./version.ts";
24
+ import type { ParsedCommit, GroupedCommits } from "./types.ts";
25
+
26
+ const cwd = process.cwd();
27
+
28
+ const isWs = await isWorkspace(cwd);
29
+ const config = await loadConfig(cwd);
30
+ const options = await resolveOptions(isWs, config);
31
+
32
+ const allPackages = await getAllPackages(cwd);
33
+ const lastTag = await getLastTag(cwd);
34
+ const rawCommits = await getCommitsSince(cwd, lastTag);
35
+
36
+ if (rawCommits.length === 0) {
37
+ console.log("No commits found since last tag. Nothing to do.");
38
+ process.exit(0);
39
+ }
40
+
41
+ const parsed = rawCommits.map((c) => parseCommit(c.hash, c.message));
42
+
43
+ // In a monorepo with filtering, fetch the file list for each commit
44
+ const shouldFilter = isWs && options.filterByPackage;
45
+ if (shouldFilter) {
46
+ await Promise.all(
47
+ parsed.map(async (commit) => {
48
+ commit.files = await getCommitFiles(cwd, commit.hash);
49
+ }),
50
+ );
51
+ }
52
+
53
+ const globalGroups = groupCommits(parsed);
54
+
55
+ if (options.sections.every((type) => globalGroups[type].length === 0)) {
56
+ console.log("No categorized commits found. Nothing to do.");
57
+ process.exit(0);
58
+ }
59
+
60
+ let packages =
61
+ options.scope === "changed"
62
+ ? await getChangedPackages(cwd, allPackages, lastTag)
63
+ : allPackages;
64
+
65
+ if (packages.length === 0) {
66
+ console.log("No changed packages found. Nothing to do.");
67
+ process.exit(0);
68
+ }
69
+
70
+ function getPackageGroups(
71
+ pkg: (typeof packages)[number],
72
+ allParsed: ParsedCommit[],
73
+ ): GroupedCommits {
74
+ if (!shouldFilter) return globalGroups;
75
+ const filtered = filterCommitsForPackage(allParsed, pkg.path, cwd);
76
+ return groupCommits(filtered);
77
+ }
78
+
79
+ function packageHasChanges(groups: GroupedCommits): boolean {
80
+ return options.sections.some((type) => groups[type].length > 0);
81
+ }
82
+
83
+ if (options.dryRun) {
84
+ console.log("--- Dry Run ---\n");
85
+
86
+ const tags: string[] = [];
87
+
88
+ for (const pkg of packages) {
89
+ const groups = getPackageGroups(pkg, parsed);
90
+ const hasChanges = packageHasChanges(groups);
91
+
92
+ if (!hasChanges && options.perPackageTags) {
93
+ console.log(`${pkg.name}: no matching commits, skipping.`);
94
+ continue;
95
+ }
96
+
97
+ const oldVersion = pkg.version ?? "0.0.0";
98
+ const newVersion = bumpVersion(oldVersion, options.bump);
99
+ console.log(`${pkg.name}: ${oldVersion} → ${newVersion}`);
100
+
101
+ const updatedDeps = await getUpdatedDependencies(
102
+ cwd,
103
+ pkg.packageJsonPath,
104
+ lastTag,
105
+ );
106
+ const entry = buildChangelogEntry(
107
+ newVersion,
108
+ groups,
109
+ updatedDeps,
110
+ options.sections,
111
+ );
112
+
113
+ console.log(`\nChangelog entry for ${pkg.name}:`);
114
+ console.log(entry);
115
+
116
+ if (options.tag) {
117
+ if (options.perPackageTags) {
118
+ tags.push(`${pkg.name}@${newVersion}`);
119
+ } else {
120
+ tags.push(`${options.tagPrefix}${newVersion}`);
121
+ }
122
+ }
123
+ }
124
+
125
+ if (options.commit) {
126
+ const msg =
127
+ packages.length === 1
128
+ ? `chore: release ${packages[0]!.name}@${bumpVersion(packages[0]!.version ?? "0.0.0", options.bump)}`
129
+ : `chore: release ${packages.length} packages`;
130
+ console.log(`Would commit: ${msg}`);
131
+ } else {
132
+ console.log("Will not commit (--commit not set).");
133
+ }
134
+
135
+ if (tags.length > 0) {
136
+ console.log(`Would tag: ${tags.join(", ")}`);
137
+ } else if (!options.tag) {
138
+ console.log("Will not tag (--tag not set).");
139
+ }
140
+
141
+ console.log("\nNo files were modified.");
142
+ process.exit(0);
143
+ }
144
+
145
+ const tags: string[] = [];
146
+
147
+ for (const pkg of packages) {
148
+ const groups = getPackageGroups(pkg, parsed);
149
+ const hasChanges = packageHasChanges(groups);
150
+
151
+ // per-package-tags + no changes → skip entirely
152
+ if (!hasChanges && options.perPackageTags) {
153
+ console.log(`${pkg.name}: no matching commits, skipping.`);
154
+ continue;
155
+ }
156
+
157
+ const { oldVersion, newVersion } = await updatePackageVersion(
158
+ pkg.packageJsonPath,
159
+ options.bump,
160
+ );
161
+ console.log(`${pkg.name}: ${oldVersion} → ${newVersion}`);
162
+
163
+ const updatedDeps = await getUpdatedDependencies(
164
+ cwd,
165
+ pkg.packageJsonPath,
166
+ lastTag,
167
+ );
168
+ const entry = buildChangelogEntry(
169
+ newVersion,
170
+ groups,
171
+ updatedDeps,
172
+ options.sections,
173
+ );
174
+ await writeChangelog(pkg.path, entry);
175
+
176
+ if (options.tag) {
177
+ if (options.perPackageTags) {
178
+ tags.push(`${pkg.name}@${newVersion}`);
179
+ } else {
180
+ tags.push(`${options.tagPrefix}${newVersion}`);
181
+ }
182
+ }
183
+ }
184
+
185
+ if (options.commit) {
186
+ const msg =
187
+ packages.length === 1
188
+ ? `chore: release ${packages[0]!.name}@${(await Bun.file(packages[0]!.packageJsonPath).json()).version}`
189
+ : `chore: release ${packages.length} packages`;
190
+ await commitAndTag(cwd, msg, options.tag ? tags : []);
191
+ console.log(`Committed: ${msg}`);
192
+ if (tags.length > 0) {
193
+ console.log(`Tagged: ${tags.join(", ")}`);
194
+ }
195
+ }
196
+
197
+ console.log("Done.");
package/src/types.ts ADDED
@@ -0,0 +1,50 @@
1
+ export type BumpType = "patch" | "minor" | "major";
2
+ export type PackageScope = "all" | "changed";
3
+ export type CommitType =
4
+ | "feature"
5
+ | "bugfix"
6
+ | "refactor"
7
+ | "perf"
8
+ | "style"
9
+ | "test"
10
+ | "docs"
11
+ | "build"
12
+ | "ops"
13
+ | "chore";
14
+
15
+ export interface CliOptions {
16
+ scope: PackageScope;
17
+ bump: BumpType;
18
+ commit: boolean;
19
+ tag: boolean;
20
+ perPackageTags: boolean;
21
+ sections: CommitType[];
22
+ dryRun: boolean;
23
+ filterByPackage: boolean;
24
+ tagPrefix: string;
25
+ }
26
+
27
+ export interface ParsedCommit {
28
+ hash: string;
29
+ message: string;
30
+ type: CommitType | null;
31
+ commitScope: string | null;
32
+ description: string;
33
+ files: string[];
34
+ }
35
+
36
+ export type GroupedCommits = Record<CommitType, ParsedCommit[]>;
37
+
38
+ export interface PackageInfo {
39
+ name: string;
40
+ path: string;
41
+ packageJsonPath: string;
42
+ version: string;
43
+ dependencies: Record<string, string>;
44
+ devDependencies: Record<string, string>;
45
+ }
46
+
47
+ export interface UpdatedDependency {
48
+ name: string;
49
+ newVersion: string;
50
+ }
@@ -0,0 +1,40 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { parseSemver, bumpVersion } from "./version.ts";
3
+
4
+ describe("parseSemver", () => {
5
+ test("parses standard semver", () => {
6
+ expect(parseSemver("1.2.3")).toEqual([1, 2, 3]);
7
+ });
8
+
9
+ test("strips leading v", () => {
10
+ expect(parseSemver("v2.0.1")).toEqual([2, 0, 1]);
11
+ });
12
+
13
+ test("handles 0.0.0", () => {
14
+ expect(parseSemver("0.0.0")).toEqual([0, 0, 0]);
15
+ });
16
+ });
17
+
18
+ describe("bumpVersion", () => {
19
+ test("bumps patch", () => {
20
+ expect(bumpVersion("1.2.3", "patch")).toBe("1.2.4");
21
+ });
22
+
23
+ test("bumps minor and resets patch", () => {
24
+ expect(bumpVersion("1.2.3", "minor")).toBe("1.3.0");
25
+ });
26
+
27
+ test("bumps major and resets minor/patch", () => {
28
+ expect(bumpVersion("1.2.3", "major")).toBe("2.0.0");
29
+ });
30
+
31
+ test("bumps from 0.0.0", () => {
32
+ expect(bumpVersion("0.0.0", "patch")).toBe("0.0.1");
33
+ expect(bumpVersion("0.0.0", "minor")).toBe("0.1.0");
34
+ expect(bumpVersion("0.0.0", "major")).toBe("1.0.0");
35
+ });
36
+
37
+ test("handles v prefix", () => {
38
+ expect(bumpVersion("v1.0.0", "patch")).toBe("1.0.1");
39
+ });
40
+ });
package/src/version.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { BumpType } from "./types.ts";
2
+
3
+ export function parseSemver(version: string): [number, number, number] {
4
+ const clean = version.startsWith("v") ? version.slice(1) : version;
5
+ const [major, minor, patch] = clean.split(".").map(Number);
6
+ return [major ?? 0, minor ?? 0, patch ?? 0];
7
+ }
8
+
9
+ export function bumpVersion(current: string, bump: BumpType): string {
10
+ const [major, minor, patch] = parseSemver(current);
11
+ switch (bump) {
12
+ case "major":
13
+ return `${major + 1}.0.0`;
14
+ case "minor":
15
+ return `${major}.${minor + 1}.0`;
16
+ case "patch":
17
+ return `${major}.${minor}.${patch + 1}`;
18
+ }
19
+ }
20
+
21
+ export async function updatePackageVersion(
22
+ packageJsonPath: string,
23
+ bump: BumpType,
24
+ ): Promise<{ oldVersion: string; newVersion: string }> {
25
+ const file = Bun.file(packageJsonPath);
26
+ const pkg = await file.json();
27
+ const oldVersion = pkg.version ?? "0.0.0";
28
+ const newVersion = bumpVersion(oldVersion, bump);
29
+ pkg.version = newVersion;
30
+ await Bun.write(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
31
+ return { oldVersion, newVersion };
32
+ }
@@ -0,0 +1,68 @@
1
+ import { $ } from "bun";
2
+ import { join } from "node:path";
3
+ import type { PackageInfo } from "./types.ts";
4
+
5
+ export async function isWorkspace(rootDir: string): Promise<boolean> {
6
+ const pkg = await Bun.file(join(rootDir, "package.json")).json();
7
+ return Array.isArray(pkg.workspaces) && pkg.workspaces.length > 0;
8
+ }
9
+
10
+ export async function getAllPackages(rootDir: string): Promise<PackageInfo[]> {
11
+ const rootPkg = await Bun.file(join(rootDir, "package.json")).json();
12
+ const patterns: string[] = rootPkg.workspaces ?? [];
13
+
14
+ if (patterns.length === 0) {
15
+ return [packageInfoFromJson(rootPkg, rootDir)];
16
+ }
17
+
18
+ const packages: PackageInfo[] = [];
19
+
20
+ for (const pattern of patterns) {
21
+ const glob = new Bun.Glob(`${pattern}/package.json`);
22
+ for await (const match of glob.scan({ cwd: rootDir, absolute: true })) {
23
+ const dir = match.replace(/\/package\.json$/, "");
24
+ const pkg = await Bun.file(match).json();
25
+ packages.push(packageInfoFromJson(pkg, dir));
26
+ }
27
+ }
28
+
29
+ return packages;
30
+ }
31
+
32
+ function packageInfoFromJson(pkg: Record<string, unknown>, dir: string): PackageInfo {
33
+ return {
34
+ name: (pkg.name as string) ?? "unknown",
35
+ path: dir,
36
+ packageJsonPath: join(dir, "package.json"),
37
+ version: (pkg.version as string) ?? "0.0.0",
38
+ dependencies: (pkg.dependencies as Record<string, string>) ?? {},
39
+ devDependencies: (pkg.devDependencies as Record<string, string>) ?? {},
40
+ };
41
+ }
42
+
43
+ export async function getChangedPackages(
44
+ rootDir: string,
45
+ allPackages: PackageInfo[],
46
+ sinceRef: string | null,
47
+ ): Promise<PackageInfo[]> {
48
+ if (!sinceRef) return allPackages;
49
+
50
+ let result;
51
+ try {
52
+ result =
53
+ await $`git -C ${rootDir} diff --name-only ${sinceRef}..HEAD`.quiet();
54
+ } catch {
55
+ return allPackages;
56
+ }
57
+
58
+ const changedFiles = result
59
+ .text()
60
+ .trim()
61
+ .split("\n")
62
+ .filter(Boolean)
63
+ .map((f) => join(rootDir, f));
64
+
65
+ return allPackages.filter((pkg) =>
66
+ changedFiles.some((f) => f.startsWith(pkg.path)),
67
+ );
68
+ }