relacher 0.0.1 → 0.0.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.
- package/README.md +12 -16
- package/lib-types/builder/cargo.d.ts +6 -6
- package/lib-types/builder/shared.d.ts +2 -3
- package/lib-types/default-data.d.ts +3 -0
- package/lib-types/index.d.ts +1 -0
- package/lib-types/prepare.d.ts +7 -6
- package/lib-types/run.d.ts +5 -0
- package/lib-types/src/builder/cargo.d.ts +13 -0
- package/lib-types/src/builder/index.d.ts +3 -0
- package/lib-types/src/builder/pnpm.d.ts +13 -0
- package/lib-types/src/builder/shared.d.ts +11 -0
- package/lib-types/src/default-data.d.ts +3 -0
- package/lib-types/src/index.d.ts +6 -0
- package/lib-types/src/prepare.d.ts +15 -0
- package/lib-types/src/print.d.ts +2 -0
- package/lib-types/src/run.d.ts +12 -0
- package/lib-types/src/types.d.ts +71 -0
- package/lib-types/src/updater/changelog.d.ts +10 -0
- package/lib-types/src/updater/index.d.ts +26 -0
- package/lib-types/src/updater/json.d.ts +12 -0
- package/lib-types/src/updater/regex.d.ts +13 -0
- package/lib-types/src/updater/toml.d.ts +12 -0
- package/lib-types/src/vcs/git.d.ts +15 -0
- package/lib-types/src/vcs/index.d.ts +9 -0
- package/lib-types/src/vcs/jj.d.ts +15 -0
- package/lib-types/types.d.ts +9 -25
- package/lib-types/updater/changelog.d.ts +10 -0
- package/lib-types/updater/index.d.ts +26 -0
- package/lib-types/updater/json.d.ts +12 -0
- package/lib-types/updater/regex.d.ts +13 -0
- package/lib-types/updater/toml.d.ts +12 -0
- package/lib-types/vcs/git.d.ts +1 -0
- package/lib-types/vcs/index.d.ts +1 -0
- package/lib-types/vcs/jj.d.ts +1 -0
- package/package.json +2 -2
- package/src/builder/cargo.ts +81 -18
- package/src/builder/pnpm.ts +14 -12
- package/src/builder/shared.ts +2 -9
- package/src/default-data.ts +15 -0
- package/src/index.ts +1 -0
- package/src/prepare.ts +68 -108
- package/src/print.ts +9 -4
- package/src/run.ts +50 -56
- package/src/types.ts +14 -46
- package/src/updater/changelog.ts +76 -0
- package/src/updater/index.ts +31 -0
- package/src/updater/json.ts +95 -0
- package/src/updater/regex.ts +48 -0
- package/src/updater/toml.ts +57 -0
- package/src/vcs/git.ts +24 -3
- package/src/vcs/index.ts +1 -0
- package/src/vcs/jj.ts +23 -6
package/src/builder/cargo.ts
CHANGED
|
@@ -2,9 +2,14 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import type { DependencyConfig } from '../types';
|
|
5
|
+
import { regexUpdate, tomlUpdate, tomlFallback } from '../updater';
|
|
5
6
|
|
|
6
7
|
import { decorateList, type DependencyList } from './shared';
|
|
7
8
|
|
|
9
|
+
export interface CargoBuilderOptions {
|
|
10
|
+
syncWorkspaceDeps?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Loads a single Cargo project from a target Cargo.toml.
|
|
10
15
|
*/
|
|
@@ -15,13 +20,14 @@ export function cargoProject(cwd: string): DependencyList {
|
|
|
15
20
|
if (fs.existsSync(rootCargo)) {
|
|
16
21
|
const fileContent = fs.readFileSync(rootCargo, 'utf8');
|
|
17
22
|
const nameMatch = fileContent.match(/^name\s*=\s*"([^"]+)"/m);
|
|
18
|
-
const parsedName = nameMatch
|
|
23
|
+
const parsedName = nameMatch?.[1] ?? '';
|
|
19
24
|
if (parsedName) {
|
|
20
25
|
configs.push({
|
|
21
26
|
name: parsedName,
|
|
22
27
|
watch: ['.'],
|
|
23
28
|
depends: [],
|
|
24
|
-
|
|
29
|
+
versionFallback: tomlFallback({ path: 'Cargo.toml', toml: 'package.version' }),
|
|
30
|
+
updates: [tomlUpdate({ path: 'Cargo.toml', toml: 'package.version' })],
|
|
25
31
|
});
|
|
26
32
|
}
|
|
27
33
|
}
|
|
@@ -30,11 +36,12 @@ export function cargoProject(cwd: string): DependencyList {
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/**
|
|
33
|
-
* Loads a Cargo workspace, tracking member paths and
|
|
39
|
+
* Loads a Cargo workspace, tracking member paths, dependency links, and recursive nested paths.
|
|
34
40
|
*/
|
|
35
|
-
export function cargoWorkspace(cwd: string): DependencyList {
|
|
41
|
+
export function cargoWorkspace(cwd: string, options?: CargoBuilderOptions): DependencyList {
|
|
36
42
|
const configs: DependencyConfig[] = [];
|
|
37
43
|
const rootCargo = path.join(cwd, 'Cargo.toml');
|
|
44
|
+
const discoveredPaths = new Set<string>();
|
|
38
45
|
|
|
39
46
|
if (fs.existsSync(rootCargo)) {
|
|
40
47
|
const content = fs.readFileSync(rootCargo, 'utf8');
|
|
@@ -44,47 +51,106 @@ export function cargoWorkspace(cwd: string): DependencyList {
|
|
|
44
51
|
const crateInfos: { name: string; memberPath: string; content: string }[] = [];
|
|
45
52
|
|
|
46
53
|
for (const member of rawMembers) {
|
|
54
|
+
if (!member) continue;
|
|
47
55
|
const globPath = path.join(cwd, member);
|
|
48
56
|
const cargoPaths = globPath.endsWith('*')
|
|
49
|
-
? fs
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
? fs.existsSync(path.dirname(globPath))
|
|
58
|
+
? fs
|
|
59
|
+
.readdirSync(path.dirname(globPath))
|
|
60
|
+
.map((dir) => path.join(path.dirname(globPath), dir, 'Cargo.toml'))
|
|
61
|
+
: []
|
|
52
62
|
: [path.join(globPath, 'Cargo.toml')];
|
|
53
63
|
|
|
54
64
|
for (const p of cargoPaths) {
|
|
55
65
|
if (fs.existsSync(p)) {
|
|
66
|
+
const memberPath = path.dirname(p);
|
|
67
|
+
discoveredPaths.add(memberPath);
|
|
56
68
|
const fileContent = fs.readFileSync(p, 'utf8');
|
|
57
69
|
const nameMatch = fileContent.match(/^name\s*=\s*"([^"]+)"/m);
|
|
58
|
-
const parsedName = nameMatch
|
|
70
|
+
const parsedName = nameMatch?.[1] ?? '';
|
|
59
71
|
if (parsedName) {
|
|
60
72
|
crateInfos.push({
|
|
61
73
|
name: parsedName,
|
|
62
|
-
memberPath
|
|
74
|
+
memberPath,
|
|
75
|
+
content: fileContent,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Discover nested packages dynamically via path dependencies
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (i < crateInfos.length) {
|
|
85
|
+
const info = crateInfos[i]!;
|
|
86
|
+
const pathDepsMatch = [
|
|
87
|
+
...info.content.matchAll(/([a-zA-Z0-9_-]+)\s*=\s*\{[^}]*path\s*=\s*"([^"]+)"/g),
|
|
88
|
+
];
|
|
89
|
+
for (const match of pathDepsMatch) {
|
|
90
|
+
const depName = match[1];
|
|
91
|
+
const depPath = match[2];
|
|
92
|
+
|
|
93
|
+
// Verify matches are defined strings to resolve TypeScript's undefined safety warnings
|
|
94
|
+
if (typeof depName !== 'string' || typeof depPath !== 'string') {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const fullDepPath = path.resolve(info.memberPath, depPath);
|
|
99
|
+
|
|
100
|
+
if (!discoveredPaths.has(fullDepPath)) {
|
|
101
|
+
discoveredPaths.add(fullDepPath);
|
|
102
|
+
const cargoPath = path.join(fullDepPath, 'Cargo.toml');
|
|
103
|
+
|
|
104
|
+
if (fs.existsSync(cargoPath)) {
|
|
105
|
+
const fileContent = fs.readFileSync(cargoPath, 'utf8');
|
|
106
|
+
const nameMatch = fileContent.match(/^name\s*=\s*"([^"]+)"/m);
|
|
107
|
+
const parsedName = nameMatch?.[1] ?? depName;
|
|
108
|
+
|
|
109
|
+
crateInfos.push({
|
|
110
|
+
name: parsedName,
|
|
111
|
+
memberPath: fullDepPath,
|
|
63
112
|
content: fileContent,
|
|
64
113
|
});
|
|
65
114
|
}
|
|
66
115
|
}
|
|
67
116
|
}
|
|
117
|
+
i++;
|
|
68
118
|
}
|
|
69
119
|
|
|
70
120
|
for (const info of crateInfos) {
|
|
71
121
|
const relativePath = path.relative(cwd, info.memberPath);
|
|
72
|
-
const
|
|
122
|
+
const posixRelativePath = relativePath.split(path.sep).join(path.posix.sep);
|
|
123
|
+
const relativeCargo = posixRelativePath
|
|
124
|
+
? path.posix.join(posixRelativePath, 'Cargo.toml')
|
|
125
|
+
: 'Cargo.toml';
|
|
73
126
|
|
|
74
127
|
const depends: string[] = [];
|
|
75
128
|
for (const other of crateInfos) {
|
|
76
129
|
if (other.name === info.name) continue;
|
|
77
|
-
const depPattern = new RegExp(
|
|
130
|
+
const depPattern = new RegExp(`^\\s*${other.name}\\s*=`, 'm');
|
|
78
131
|
if (depPattern.test(info.content)) {
|
|
79
132
|
depends.push(other.name);
|
|
80
133
|
}
|
|
81
134
|
}
|
|
82
135
|
|
|
136
|
+
const updates = [tomlUpdate({ path: relativeCargo, toml: 'package.version' })];
|
|
137
|
+
|
|
138
|
+
if (options?.syncWorkspaceDeps) {
|
|
139
|
+
updates.push(
|
|
140
|
+
regexUpdate({
|
|
141
|
+
path: 'Cargo.toml',
|
|
142
|
+
search: `(${info.name}\\s*=\\s*\\{[^}]*version\\s*=\\s*")[^"]+(")`,
|
|
143
|
+
replace: `$1{{version}}$2`,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
83
148
|
configs.push({
|
|
84
149
|
name: info.name,
|
|
85
|
-
watch: [
|
|
150
|
+
watch: [posixRelativePath || '.'],
|
|
86
151
|
depends,
|
|
87
|
-
|
|
152
|
+
versionFallback: tomlFallback({ path: relativeCargo, toml: 'package.version' }),
|
|
153
|
+
updates,
|
|
88
154
|
});
|
|
89
155
|
}
|
|
90
156
|
}
|
|
@@ -93,15 +159,12 @@ export function cargoWorkspace(cwd: string): DependencyList {
|
|
|
93
159
|
return decorateList(configs);
|
|
94
160
|
}
|
|
95
161
|
|
|
96
|
-
|
|
97
|
-
* Auto-detects whether the directory corresponds to a workspace or single-project setup.
|
|
98
|
-
*/
|
|
99
|
-
export function cargoDeps(cwd: string): DependencyList {
|
|
162
|
+
export function cargoDeps(cwd: string, options?: CargoBuilderOptions): DependencyList {
|
|
100
163
|
const rootCargo = path.join(cwd, 'Cargo.toml');
|
|
101
164
|
if (fs.existsSync(rootCargo)) {
|
|
102
165
|
const content = fs.readFileSync(rootCargo, 'utf8');
|
|
103
166
|
if (content.includes('members =')) {
|
|
104
|
-
return cargoWorkspace(cwd);
|
|
167
|
+
return cargoWorkspace(cwd, options);
|
|
105
168
|
}
|
|
106
169
|
if (content.includes('package =') || content.match(/^name\s*=/m)) {
|
|
107
170
|
return cargoProject(cwd);
|
package/src/builder/pnpm.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import type { DependencyConfig } from '../types';
|
|
5
|
+
import { jsonUpdate, jsonFallback } from '../updater';
|
|
5
6
|
|
|
6
7
|
import { decorateList, type DependencyList } from './shared';
|
|
7
8
|
|
|
@@ -21,13 +22,12 @@ export function pnpmProject(cwd: string): DependencyList {
|
|
|
21
22
|
name: parsed.name,
|
|
22
23
|
watch: ['.'],
|
|
23
24
|
depends: [],
|
|
25
|
+
versionFallback: jsonFallback({ path: 'package.json', json: 'version' }),
|
|
24
26
|
updates: [
|
|
25
|
-
{
|
|
26
|
-
kind: 'regex',
|
|
27
|
+
jsonUpdate({
|
|
27
28
|
path: 'package.json',
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
29
|
+
json: 'version',
|
|
30
|
+
}),
|
|
31
31
|
],
|
|
32
32
|
});
|
|
33
33
|
}
|
|
@@ -114,7 +114,10 @@ export function pnpmWorkspace(cwd: string): DependencyList {
|
|
|
114
114
|
|
|
115
115
|
for (const info of packageInfos) {
|
|
116
116
|
const relativePath = path.relative(cwd, info.memberPath);
|
|
117
|
-
const
|
|
117
|
+
const posixRelativePath = relativePath.split(path.sep).join(path.posix.sep);
|
|
118
|
+
const relativePJson = posixRelativePath
|
|
119
|
+
? path.posix.join(posixRelativePath, 'package.json')
|
|
120
|
+
: 'package.json';
|
|
118
121
|
|
|
119
122
|
const parsed = JSON.parse(info.content);
|
|
120
123
|
const depends: string[] = [];
|
|
@@ -134,15 +137,14 @@ export function pnpmWorkspace(cwd: string): DependencyList {
|
|
|
134
137
|
|
|
135
138
|
configs.push({
|
|
136
139
|
name: info.name,
|
|
137
|
-
watch: [
|
|
140
|
+
watch: [posixRelativePath || '.'],
|
|
138
141
|
depends,
|
|
142
|
+
versionFallback: jsonFallback({ path: relativePJson, json: 'version' }),
|
|
139
143
|
updates: [
|
|
140
|
-
{
|
|
141
|
-
kind: 'regex',
|
|
144
|
+
jsonUpdate({
|
|
142
145
|
path: relativePJson,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
},
|
|
146
|
+
json: 'version',
|
|
147
|
+
}),
|
|
146
148
|
],
|
|
147
149
|
});
|
|
148
150
|
}
|
package/src/builder/shared.ts
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export function changelogUpdate(options: Omit<ChangelogUpdate, 'kind'>): ChangelogUpdate {
|
|
4
|
-
return { kind: 'changelog', ...options };
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function regexUpdate(options: Omit<RegexUpdate, 'kind'>): RegexUpdate {
|
|
8
|
-
return { kind: 'regex', ...options };
|
|
9
|
-
}
|
|
1
|
+
import type { DependencyConfig } from '../types';
|
|
2
|
+
import type { UpdateAction } from '../updater';
|
|
10
3
|
|
|
11
4
|
export class SingleDependencyBuilder {
|
|
12
5
|
constructor(private config: DependencyConfig) { }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CascadeRules, SizePatterns } from './types';
|
|
2
|
+
|
|
3
|
+
export const defaultSizes: SizePatterns = {
|
|
4
|
+
major: { pattern: '^[a-z]+(?:\\([^)]+\\))?!:|BREAKING CHANGE' },
|
|
5
|
+
minor: { pattern: '^feat|^revert' },
|
|
6
|
+
patch: { pattern: '^fix|^build|^refactor|^nit|^style' },
|
|
7
|
+
skip: { pattern: '^release|^chore|^infra|^docs|^test|^ci|^build' },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const defaultCascadeRules: Required<CascadeRules> = {
|
|
11
|
+
skip: { skip: 'skip', patch: 'patch', minor: 'patch', major: 'patch' },
|
|
12
|
+
patch: { skip: 'patch', patch: 'patch', minor: 'minor', major: 'minor' },
|
|
13
|
+
minor: { skip: 'minor', patch: 'minor', minor: 'minor', major: 'minor' },
|
|
14
|
+
major: { skip: 'major', patch: 'major', minor: 'major', major: 'major' },
|
|
15
|
+
};
|
package/src/index.ts
CHANGED
package/src/prepare.ts
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type CascadeRules,
|
|
16
|
-
type UpdateAction,
|
|
17
|
-
type ChangelogContext,
|
|
4
|
+
import { defaultCascadeRules, defaultSizes } from './default-data';
|
|
5
|
+
import type {
|
|
6
|
+
BumpSize,
|
|
7
|
+
Commit,
|
|
8
|
+
VcsProvider,
|
|
9
|
+
DependencyConfig,
|
|
10
|
+
DependencyUpdateReport,
|
|
11
|
+
IntermediateReport,
|
|
12
|
+
PrepareOptions,
|
|
13
|
+
SizePatterns,
|
|
14
|
+
CascadeRules,
|
|
18
15
|
} from './types';
|
|
19
|
-
|
|
20
|
-
export function getTomlValue(content: string): string | null {
|
|
21
|
-
const packageMatch = content.match(/\[package\][^]*?(?=^\[|z)/);
|
|
22
|
-
const targetText = packageMatch ? packageMatch[0] : content;
|
|
23
|
-
const versionMatch = targetText.match(/^version\s*=\s*"([^"]+)"/m);
|
|
24
|
-
return versionMatch && versionMatch[1] ? versionMatch[1] : null;
|
|
25
|
-
}
|
|
16
|
+
import type { UpdateAction, UpdateActionResolved } from './updater';
|
|
26
17
|
|
|
27
18
|
export function bumpVersion(version: string, size: BumpSize): string {
|
|
28
19
|
const parts = version.split('.').map(Number);
|
|
@@ -37,12 +28,32 @@ export function bumpVersion(version: string, size: BumpSize): string {
|
|
|
37
28
|
return version;
|
|
38
29
|
}
|
|
39
30
|
|
|
40
|
-
export function getCurrentVersion(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
export async function getCurrentVersion(
|
|
32
|
+
dep: DependencyConfig,
|
|
33
|
+
vcs: VcsProvider,
|
|
34
|
+
cwd: string,
|
|
35
|
+
): Promise<{ version: string | null; isFallback: boolean }> {
|
|
36
|
+
// 1. Look for releases in git history first
|
|
37
|
+
const latestTag = await vcs.getLatestTag(dep.name);
|
|
38
|
+
if (latestTag) {
|
|
39
|
+
let version = latestTag;
|
|
40
|
+
|
|
41
|
+
// Strip package-specific prefixes (e.g., "api-v1.0.0" or "api/v1.0.0" -> "v1.0.0")
|
|
42
|
+
if (version.startsWith(`${dep.name}-`)) {
|
|
43
|
+
version = version.slice(dep.name.length + 1);
|
|
44
|
+
} else if (version.startsWith(`${dep.name}/`)) {
|
|
45
|
+
version = version.slice(dep.name.length + 1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { version: version.replace(/^v/, ''), isFallback: false };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Resolve via user-defined fallback
|
|
52
|
+
if (dep.versionFallback) {
|
|
53
|
+
return { version: dep.versionFallback.readFallback(cwd), isFallback: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { version: null, isFallback: false };
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
export function matchBumpSize(message: string, sizes: SizePatterns): BumpSize {
|
|
@@ -125,18 +136,35 @@ export async function initReportItems(
|
|
|
125
136
|
): Promise<IntermediateReport[]> {
|
|
126
137
|
const reports: IntermediateReport[] = [];
|
|
127
138
|
for (const dep of cargoDeps) {
|
|
128
|
-
const currentVersion = getCurrentVersion(dep, cwd);
|
|
139
|
+
const { version: currentVersion, isFallback } = await getCurrentVersion(dep, vcs, cwd);
|
|
140
|
+
const isMissingVersion = currentVersion === null;
|
|
141
|
+
const actualVersion = currentVersion || '0.0.0';
|
|
142
|
+
|
|
129
143
|
const commits = await vcs.getCommits(dep.name, dep.watch || []);
|
|
130
144
|
const selfBump = evaluateCommitsBump(commits, sizes);
|
|
145
|
+
|
|
146
|
+
let isErroneous = isMissingVersion;
|
|
147
|
+
for (const u of dep.updates || []) {
|
|
148
|
+
if (u.required) {
|
|
149
|
+
const filePath = path.resolve(cwd, u.path);
|
|
150
|
+
if (!fs.existsSync(filePath)) {
|
|
151
|
+
isErroneous = true;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
reports.push({
|
|
132
158
|
name: dep.name,
|
|
133
|
-
currentVersion,
|
|
134
|
-
newVersion:
|
|
159
|
+
currentVersion: actualVersion,
|
|
160
|
+
newVersion: actualVersion,
|
|
135
161
|
bump: selfBump,
|
|
136
162
|
originalBump: selfBump,
|
|
137
163
|
commits,
|
|
138
164
|
updates: dep.updates || [],
|
|
139
165
|
depends: dep.depends || [],
|
|
166
|
+
isErroneous,
|
|
167
|
+
isFirstRelease: isFallback,
|
|
140
168
|
});
|
|
141
169
|
}
|
|
142
170
|
return reports;
|
|
@@ -155,39 +183,6 @@ export async function prepare(
|
|
|
155
183
|
return finalizeReports(sorted);
|
|
156
184
|
}
|
|
157
185
|
|
|
158
|
-
export function buildChangelogBlock(version: string, commits: Commit[]): string {
|
|
159
|
-
const date = new Date().toISOString().split('T')[0] ?? '';
|
|
160
|
-
let block = `## [${version}] - ${date}\n\n`;
|
|
161
|
-
|
|
162
|
-
if (commits.length === 0) {
|
|
163
|
-
return block + `*No notable changes.*\n`;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const breaking: string[] = [];
|
|
167
|
-
const features: string[] = [];
|
|
168
|
-
const fixes: string[] = [];
|
|
169
|
-
const others: string[] = [];
|
|
170
|
-
|
|
171
|
-
for (const c of commits) {
|
|
172
|
-
if (c.message.includes('!:') || c.message.includes('BREAKING CHANGE')) {
|
|
173
|
-
breaking.push(`- ${c.hash} ${c.message}`);
|
|
174
|
-
} else if (c.message.startsWith('feat')) {
|
|
175
|
-
features.push(`- ${c.hash} ${c.message}`);
|
|
176
|
-
} else if (c.message.startsWith('fix')) {
|
|
177
|
-
fixes.push(`- ${c.hash} ${c.message}`);
|
|
178
|
-
} else {
|
|
179
|
-
others.push(`- ${c.hash} ${c.message}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (breaking.length) block += `### ⚠️ BREAKING CHANGES\n${breaking.join('\n')}\n\n`;
|
|
184
|
-
if (features.length) block += `### ✨ Features\n${features.join('\n')}\n\n`;
|
|
185
|
-
if (fixes.length) block += `### 🐛 Bug Fixes\n${fixes.join('\n')}\n\n`;
|
|
186
|
-
if (others.length) block += `### 🛠 Other Changes\n${others.join('\n')}\n\n`;
|
|
187
|
-
|
|
188
|
-
return block.trim() + '\n';
|
|
189
|
-
}
|
|
190
|
-
|
|
191
186
|
export function finalizeReports(sorted: IntermediateReport[]): DependencyUpdateReport[] {
|
|
192
187
|
const allCommitsMap = new Map<string, Commit>();
|
|
193
188
|
for (const item of sorted) {
|
|
@@ -206,57 +201,22 @@ export function finalizeReports(sorted: IntermediateReport[]): DependencyUpdateR
|
|
|
206
201
|
commits: item.commits,
|
|
207
202
|
updates: mapResolvedUpdates(item.updates, item.newVersion, item.commits, globalCommits),
|
|
208
203
|
depends: item.depends,
|
|
204
|
+
isErroneous: item.isErroneous,
|
|
205
|
+
isFirstRelease: item.isFirstRelease,
|
|
209
206
|
}));
|
|
210
207
|
}
|
|
211
208
|
|
|
212
|
-
export function defaultChangelogTemplate({ version, date, commits }: ChangelogContext): string {
|
|
213
|
-
let block = `## [${version}] - ${date}\n\n`;
|
|
214
|
-
if (commits.length === 0) return block + `*No notable changes.*\n`;
|
|
215
|
-
|
|
216
|
-
const groups: { breaking: string[]; feat: string[]; fix: string[]; other: string[] } = {
|
|
217
|
-
breaking: [],
|
|
218
|
-
feat: [],
|
|
219
|
-
fix: [],
|
|
220
|
-
other: [],
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
for (const c of commits) {
|
|
224
|
-
const item = `- ${c.shortHash} ${c.message}`;
|
|
225
|
-
if (c.isBreaking) groups.breaking.push(item);
|
|
226
|
-
else if (c.type === 'feat') groups.feat.push(item);
|
|
227
|
-
else if (c.type === 'fix') groups.fix.push(item);
|
|
228
|
-
else groups.other.push(item);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (groups.breaking.length) block += `### ⚠️ BREAKING CHANGES\n${groups.breaking.join('\n')}\n\n`;
|
|
232
|
-
if (groups.feat.length) block += `### ✨ Features\n${groups.feat.join('\n')}\n\n`;
|
|
233
|
-
if (groups.fix.length) block += `### 🐛 Bug Fixes\n${groups.fix.join('\n')}\n\n`;
|
|
234
|
-
if (groups.other.length) block += `### 🛠 Other Changes\n${groups.other.join('\n')}\n\n`;
|
|
235
|
-
|
|
236
|
-
return block.trim() + '\n';
|
|
237
|
-
}
|
|
238
|
-
|
|
239
209
|
export function mapResolvedUpdates(
|
|
240
210
|
updates: UpdateAction[],
|
|
241
211
|
newVersion: string,
|
|
242
212
|
crateCommits: Commit[],
|
|
243
213
|
globalCommits: Commit[],
|
|
244
|
-
):
|
|
245
|
-
return updates.map((
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
version: newVersion,
|
|
253
|
-
date: new Date().toISOString().split('T')[0] ?? '',
|
|
254
|
-
commits: targetCommits,
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const content = u.template ? u.template(context) : defaultChangelogTemplate(context);
|
|
258
|
-
return { ...u, resolvedBlock: content };
|
|
259
|
-
}
|
|
260
|
-
return u;
|
|
261
|
-
});
|
|
214
|
+
): UpdateActionResolved[] {
|
|
215
|
+
return updates.map((act) =>
|
|
216
|
+
act.prepare({
|
|
217
|
+
newVersion,
|
|
218
|
+
crateCommits,
|
|
219
|
+
globalCommits,
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
262
222
|
}
|
package/src/print.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { defaultSizes } from './default-data';
|
|
1
2
|
import { matchBumpSize } from './prepare';
|
|
2
|
-
import { defaultSizes } from './types';
|
|
3
3
|
import type { DependencyUpdateReport } from './types';
|
|
4
4
|
|
|
5
5
|
// ANSI Escape Codes for Terminal Colors
|
|
@@ -57,14 +57,19 @@ function getIconForFile(filename: string): string {
|
|
|
57
57
|
|
|
58
58
|
return icons[ext] || ''; // Default file icon
|
|
59
59
|
}
|
|
60
|
+
|
|
60
61
|
export function prettyPrint(reports: DependencyUpdateReport[]): void {
|
|
61
62
|
for (const report of reports) {
|
|
62
63
|
if (report.bump === 'skip') continue;
|
|
63
64
|
|
|
64
65
|
const bumpColor =
|
|
65
66
|
report.bump === 'major' ? c.red : report.bump === 'minor' ? c.yellow : c.green;
|
|
67
|
+
|
|
68
|
+
// Check if it's a first release (fallback was used instead of finding tags)
|
|
69
|
+
const firstReleaseBadge = report.isFirstRelease ? ` ${c.cyan}🌱 (first release)${c.reset}` : '';
|
|
70
|
+
|
|
66
71
|
process.stdout.write(
|
|
67
|
-
`${c.bold}📦 ${report.name.padEnd(12)}${c.reset} ${highlightVersion(report.currentVersion, report.newVersion)} ${bumpColor}${c.bold}[${report.bump}]${c.reset}\n`,
|
|
72
|
+
`${c.bold}📦 ${report.name.padEnd(12)}${c.reset} ${highlightVersion(report.currentVersion, report.newVersion)} ${bumpColor}${c.bold}[${report.bump}]${c.reset}${firstReleaseBadge}\n`,
|
|
68
73
|
);
|
|
69
74
|
|
|
70
75
|
const files = report.updates.map(
|
|
@@ -80,8 +85,9 @@ export function prettyPrint(reports: DependencyUpdateReport[]): void {
|
|
|
80
85
|
|
|
81
86
|
const marker = affectsBump ? `${c.green}✦${c.reset}` : `${c.gray}○${c.reset}`;
|
|
82
87
|
|
|
88
|
+
// User string is now wrapped with <>
|
|
83
89
|
console.log(
|
|
84
|
-
` ${marker} ${c.yellow}${commit.shortHash}${c.reset} ${commit.message} ${c.gray}
|
|
90
|
+
` ${marker} ${c.yellow}${commit.shortHash}${c.reset} ${commit.message} ${c.gray}<${commit.author}> ${commit.date}${c.reset}`,
|
|
85
91
|
);
|
|
86
92
|
}
|
|
87
93
|
|
|
@@ -103,7 +109,6 @@ export function prettyPrint(reports: DependencyUpdateReport[]): void {
|
|
|
103
109
|
);
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
// Changelog Preview
|
|
107
112
|
console.log('');
|
|
108
113
|
}
|
|
109
114
|
}
|
package/src/run.ts
CHANGED
|
@@ -4,73 +4,26 @@ import path from 'node:path';
|
|
|
4
4
|
import type { DependencyUpdateReport, VcsProvider } from './types';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Builds the multi-line commit message showcasing version bumps, included commits,
|
|
8
|
+
* cascade triggers, and first-release indicators.
|
|
8
9
|
*/
|
|
9
|
-
export
|
|
10
|
-
reports: DependencyUpdateReport[],
|
|
11
|
-
vcs: VcsProvider,
|
|
12
|
-
options: { cwd: string },
|
|
13
|
-
): Promise<void> {
|
|
14
|
-
const cwd = options.cwd;
|
|
15
|
-
|
|
16
|
-
// 1. Apply file updates
|
|
17
|
-
for (const report of reports) {
|
|
18
|
-
for (const u of report.updates) {
|
|
19
|
-
const filePath = path.resolve(cwd, u.path);
|
|
20
|
-
const dirPath = path.dirname(filePath);
|
|
21
|
-
if (!fs.existsSync(dirPath)) {
|
|
22
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (u.kind === 'toml') {
|
|
26
|
-
if (fs.existsSync(filePath)) {
|
|
27
|
-
let content = fs.readFileSync(filePath, 'utf8');
|
|
28
|
-
// Update package version inside the target Cargo.toml package block
|
|
29
|
-
content = content.replace(
|
|
30
|
-
/(\[package\][^]*?^version\s*=\s*")[^"]+(")/m,
|
|
31
|
-
`$1${report.newVersion}$2`,
|
|
32
|
-
);
|
|
33
|
-
// Sync workspace dependency entries if internal dependencies changed
|
|
34
|
-
for (const depReport of reports) {
|
|
35
|
-
if (depReport.name === report.name) continue;
|
|
36
|
-
const depRegex = new RegExp(
|
|
37
|
-
`(${depReport.name}\\s*=\\s*\\{[^}]*version\\s*=\\s*")[^"]+(")`,
|
|
38
|
-
'g',
|
|
39
|
-
);
|
|
40
|
-
content = content.replace(depRegex, `$1${depReport.newVersion}$2`);
|
|
41
|
-
}
|
|
42
|
-
fs.writeFileSync(filePath, content);
|
|
43
|
-
}
|
|
44
|
-
} else if (u.kind === 'regex') {
|
|
45
|
-
if (fs.existsSync(filePath)) {
|
|
46
|
-
let content = fs.readFileSync(filePath, 'utf8');
|
|
47
|
-
const searchRegex = new RegExp(u.search, 'g');
|
|
48
|
-
content = content.replace(searchRegex, u.resolvedReplace ?? '');
|
|
49
|
-
fs.writeFileSync(filePath, content);
|
|
50
|
-
}
|
|
51
|
-
} else if (u.kind === 'changelog') {
|
|
52
|
-
const oldContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
53
|
-
const newContent = (u.resolvedBlock ?? '') + '\n' + oldContent;
|
|
54
|
-
fs.writeFileSync(filePath, newContent.trim() + '\n');
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 2. Build multi-line commit message
|
|
10
|
+
export function generateCommitMessage(reports: DependencyUpdateReport[]): string | null {
|
|
60
11
|
const activeReports = reports.filter((r) => r.bump !== 'skip');
|
|
61
12
|
if (activeReports.length === 0) {
|
|
62
|
-
return;
|
|
13
|
+
return null;
|
|
63
14
|
}
|
|
64
15
|
|
|
65
16
|
const tagList = activeReports.map((r) => `${r.name}-v${r.newVersion}`).join(', ');
|
|
66
17
|
let commitMessage = `chore: release ${tagList}\n\n`;
|
|
67
18
|
|
|
68
19
|
for (const report of activeReports) {
|
|
69
|
-
|
|
20
|
+
const firstReleaseBadge = report.isFirstRelease ? ' (first release)' : '';
|
|
21
|
+
|
|
22
|
+
commitMessage += `${report.name} (${report.currentVersion} -> ${report.newVersion}) [${report.bump}]${firstReleaseBadge}\n`;
|
|
70
23
|
|
|
71
24
|
// Add commits
|
|
72
25
|
for (const commit of report.commits) {
|
|
73
|
-
commitMessage += ` - ${commit.shortHash} ${commit.message} (
|
|
26
|
+
commitMessage += ` - ${commit.shortHash} ${commit.message} (<${commit.author}> ${commit.date})\n`;
|
|
74
27
|
}
|
|
75
28
|
|
|
76
29
|
// Add cascades
|
|
@@ -86,10 +39,51 @@ export async function run(
|
|
|
86
39
|
commitMessage += '\n';
|
|
87
40
|
}
|
|
88
41
|
|
|
42
|
+
return commitMessage.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Applies calculated updates to files on disk, stages, commits, and tags using the provided VcsProvider.
|
|
47
|
+
*/
|
|
48
|
+
export async function run(
|
|
49
|
+
reports: DependencyUpdateReport[],
|
|
50
|
+
vcs: VcsProvider,
|
|
51
|
+
options: { cwd: string },
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const erroneous = reports.filter((r) => r.isErroneous);
|
|
54
|
+
if (erroneous.length > 0) {
|
|
55
|
+
const names = erroneous.map((r) => r.name).join(', ');
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Cannot run because the following packages are missing versions or required updates: ${names}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cwd = options.cwd;
|
|
62
|
+
|
|
63
|
+
// 1. Apply file updates
|
|
64
|
+
for (const report of reports) {
|
|
65
|
+
for (const u of report.updates) {
|
|
66
|
+
const filePath = path.resolve(cwd, u.path);
|
|
67
|
+
const dirPath = path.dirname(filePath);
|
|
68
|
+
if (!fs.existsSync(dirPath)) {
|
|
69
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
u.apply(report, reports, cwd);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2. Build multi-line commit message
|
|
77
|
+
const commitMessage = generateCommitMessage(reports);
|
|
78
|
+
if (!commitMessage) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
89
82
|
// 3. Stage and execute commit
|
|
90
|
-
await vcs.commit(commitMessage
|
|
83
|
+
await vcs.commit(commitMessage);
|
|
91
84
|
|
|
92
85
|
// 4. Create tags
|
|
86
|
+
const activeReports = reports.filter((r) => r.bump !== 'skip');
|
|
93
87
|
for (const report of activeReports) {
|
|
94
88
|
const tagName = `${report.name}-v${report.newVersion}`;
|
|
95
89
|
await vcs.tag(tagName);
|