relacher 0.0.4-rc.0 → 0.0.4-rc.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.
@@ -2,12 +2,4 @@ import { type DependencyList } from './shared';
2
2
  export interface CargoBuilderOptions {
3
3
  syncWorkspaceDeps?: boolean;
4
4
  }
5
- /**
6
- * Loads a single Cargo project from a target Cargo.toml.
7
- */
8
- export declare function cargoProject(cwd: string): DependencyList;
9
- /**
10
- * Loads a Cargo workspace, tracking member paths, dependency links, and recursive nested paths.
11
- */
12
- export declare function cargoWorkspace(cwd: string, options?: CargoBuilderOptions): DependencyList;
13
5
  export declare function cargoDeps(cwd: string, options?: CargoBuilderOptions): DependencyList;
@@ -17,6 +17,8 @@ export interface UpdateAction {
17
17
  required?: boolean;
18
18
  params: any;
19
19
  prepare(data: UpdateActionOptions): UpdateActionResolved;
20
+ skipIf(fn: (cwd: string) => boolean): this;
21
+ _skipIf(cwd: string): boolean;
20
22
  }
21
23
  export interface VersionFallback {
22
24
  readFallback(cwd: string): string | null;
@@ -44,13 +46,5 @@ type UpdateBuilderParams<T, K = undefined> = {
44
46
  };
45
47
  export declare const updateBuilder: <T, K = undefined>({ kind, apply, prepare }: UpdateBuilderParams<T, K>) => (targetPath: string, params: T & {
46
48
  required?: boolean;
47
- }) => {
48
- kind: string;
49
- path: string;
50
- params: T & {
51
- required?: boolean;
52
- };
53
- required: boolean | undefined;
54
- prepare(options: UpdateActionOptions): UpdateActionResolved;
55
- };
49
+ }) => UpdateAction;
56
50
  export {};
@@ -6,13 +6,5 @@ export type ChangelogUpdateParams = {
6
6
  };
7
7
  export declare const changelogUpdate: (targetPath: string, params: ChangelogUpdateParams & {
8
8
  required?: boolean;
9
- }) => {
10
- kind: string;
11
- path: string;
12
- params: ChangelogUpdateParams & {
13
- required?: boolean;
14
- };
15
- required: boolean | undefined;
16
- prepare(options: import("./builder").UpdateActionOptions): import("./builder").UpdateActionResolved;
17
- };
9
+ }) => import("./builder").UpdateAction;
18
10
  export declare function defaultChangelogTemplate({ version, date, commits }: ChangelogContext): string;
@@ -8,12 +8,4 @@ export type JsonUpdateParams = (parsed: any, report: DependencyUpdateReport, rep
8
8
  export declare const jsonFallback: (params: JsonFallbackParams) => VersionFallback;
9
9
  export declare const jsonUpdate: (targetPath: string, params: JsonUpdateParams & {
10
10
  required?: boolean;
11
- }) => {
12
- kind: string;
13
- path: string;
14
- params: JsonUpdateParams & {
15
- required?: boolean;
16
- };
17
- required: boolean | undefined;
18
- prepare(options: import("./builder").UpdateActionOptions): import("./builder").UpdateActionResolved;
19
- };
11
+ }) => import("./builder").UpdateAction;
@@ -11,12 +11,4 @@ export type RegexUpdateParams = {
11
11
  export declare const regexFallback: (params: RegexFallbackParams) => VersionFallback;
12
12
  export declare const regexUpdate: (targetPath: string, params: RegexUpdateParams & {
13
13
  required?: boolean;
14
- }) => {
15
- kind: string;
16
- path: string;
17
- params: RegexUpdateParams & {
18
- required?: boolean;
19
- };
20
- required: boolean | undefined;
21
- prepare(options: import("./builder").UpdateActionOptions): import("./builder").UpdateActionResolved;
22
- };
14
+ }) => import("./builder").UpdateAction;
@@ -8,12 +8,4 @@ export type TomlUpdateParams = (parsed: any, report: DependencyUpdateReport, rep
8
8
  export declare const tomlFallback: (params: TomlFallbackParams) => VersionFallback;
9
9
  export declare const tomlUpdate: (targetPath: string, params: TomlUpdateParams & {
10
10
  required?: boolean;
11
- }) => {
12
- kind: string;
13
- path: string;
14
- params: TomlUpdateParams & {
15
- required?: boolean;
16
- };
17
- required: boolean | undefined;
18
- prepare(options: import("./builder").UpdateActionOptions): import("./builder").UpdateActionResolved;
19
- };
11
+ }) => import("./builder").UpdateAction;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relacher",
3
- "version": "0.0.4-rc.0",
3
+ "version": "0.0.4-rc.2",
4
4
  "description": "Scriptable release orchestration library for monorepos",
5
5
  "type": "module",
6
6
  "private": false,
@@ -56,7 +56,9 @@
56
56
  "oxfmt": "^0.19.0",
57
57
  "oxlint": "^1.64.0",
58
58
  "prettier": "^3.8.3",
59
- "smol-toml": "^1.6.1",
60
59
  "typescript": "^6.0.3"
60
+ },
61
+ "dependencies": {
62
+ "smol-toml": "^1.6.1"
61
63
  }
62
64
  }
@@ -12,244 +12,248 @@ export interface CargoBuilderOptions {
12
12
  syncWorkspaceDeps?: boolean;
13
13
  }
14
14
 
15
- /**
16
- * Loads a single Cargo project from a target Cargo.toml.
17
- */
18
- export function cargoProject(cwd: string): DependencyList {
19
- const configs: DependencyConfig[] = [];
20
- const rootCargo = path.join(cwd, 'Cargo.toml');
21
- const lockCargo = path.join(cwd, 'Cargo.lock');
22
- const hasLock = fs.existsSync(lockCargo);
23
-
24
- if (fs.existsSync(rootCargo)) {
25
- try {
26
- const fileContent = fs.readFileSync(rootCargo, 'utf8');
27
- const doc = parse(fileContent) as any;
28
- const parsedName = doc.package?.name;
29
-
30
- if (parsedName) {
31
- const updates = [
32
- tomlUpdate('Cargo.toml', (d, r) => {
33
- if (d.package) d.package.version = r.newVersion;
34
- }),
35
- ];
36
-
37
- if (hasLock) {
38
- updates.push(
39
- tomlUpdate('Cargo.lock', (d, r) => {
40
- if (Array.isArray(d.package)) {
41
- // Only update local packages (missing source/checksum attributes)
42
- const pkg = d.package.find(
43
- (p: any) => p.name === parsedName && !p.source && !p.checksum,
44
- );
45
- if (pkg) pkg.version = r.newVersion;
46
- }
47
- }),
48
- );
49
- }
50
-
51
- configs.push({
52
- name: parsedName,
53
- watch: ['.'],
54
- depends: [],
55
- versionFallback: tomlFallback({
56
- path: 'Cargo.toml',
57
- read: (d) => d.package?.version,
58
- }),
59
- updates,
60
- });
61
- }
62
- } catch {
63
- // Ignore parse error
15
+ interface PathDep {
16
+ name: string;
17
+ path: string;
18
+ }
19
+
20
+ function isWorkspace(cargoPath: string): boolean {
21
+ try {
22
+ const doc = parse(fs.readFileSync(cargoPath, 'utf8')) as any;
23
+ return !!doc?.workspace;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ function checkDir(dir: string): { isRoot: boolean; hasCargo: boolean } {
30
+ const cargoPath = path.join(dir, 'Cargo.toml');
31
+ const hasCargo = fs.existsSync(cargoPath);
32
+ return { isRoot: hasCargo && isWorkspace(cargoPath), hasCargo };
33
+ }
34
+
35
+ function findCargoWorkspaceRoot(cwd: string): string {
36
+ let current = path.resolve(cwd);
37
+ let bestRoot = current;
38
+ while (true) {
39
+ const { isRoot, hasCargo } = checkDir(current);
40
+ if (isRoot) return current;
41
+ if (hasCargo) bestRoot = current;
42
+ const parent = path.dirname(current);
43
+ if (parent === current) break;
44
+ current = parent;
45
+ }
46
+ return bestRoot;
47
+ }
48
+
49
+ function getPathFromVal(name: string, val: any, wsDeps: Record<string, any>): string | undefined {
50
+ if (val && typeof val === 'object') {
51
+ if (val.path) return val.path;
52
+ if (val.workspace === true && wsDeps[name]?.path) {
53
+ return wsDeps[name].path;
64
54
  }
65
55
  }
56
+ return undefined;
57
+ }
66
58
 
67
- return decorateList(configs);
59
+ function collectDepsFromBlock(block: any, wsDeps: Record<string, any>): PathDep[] {
60
+ if (!block || typeof block !== 'object') return [];
61
+ const list: PathDep[] = [];
62
+ for (const [name, val] of Object.entries(block)) {
63
+ const p = getPathFromVal(name, val, wsDeps);
64
+ if (p) list.push({ name, path: p });
65
+ }
66
+ return list;
68
67
  }
69
68
 
70
- /**
71
- * Loads a Cargo workspace, tracking member paths, dependency links, and recursive nested paths.
72
- */
73
- export function cargoWorkspace(cwd: string, options?: CargoBuilderOptions): DependencyList {
74
- const configs: DependencyConfig[] = [];
75
- const rootCargo = path.join(cwd, 'Cargo.toml');
76
- const lockCargo = path.join(cwd, 'Cargo.lock');
77
- const hasLock = fs.existsSync(lockCargo);
78
- const discoveredPaths = new Set<string>();
69
+ function collectTargetDeps(doc: any, wsDeps: Record<string, any>): PathDep[] {
70
+ if (!doc.target || typeof doc.target !== 'object') return [];
71
+ const list: PathDep[] = [];
72
+ for (const targetValue of Object.values(doc.target)) {
73
+ if (targetValue && typeof targetValue === 'object') {
74
+ list.push(...collectDepsFromBlock((targetValue as any).dependencies, wsDeps));
75
+ list.push(...collectDepsFromBlock((targetValue as any)['dev-dependencies'], wsDeps));
76
+ list.push(...collectDepsFromBlock((targetValue as any)['build-dependencies'], wsDeps));
77
+ }
78
+ }
79
+ return list;
80
+ }
81
+
82
+ function extractPathDependencies(doc: any, wsDeps: Record<string, any> = {}): PathDep[] {
83
+ return [
84
+ ...collectDepsFromBlock(doc.dependencies, wsDeps),
85
+ ...collectDepsFromBlock(doc['dev-dependencies'], wsDeps),
86
+ ...collectDepsFromBlock(doc['build-dependencies'], wsDeps),
87
+ ...collectTargetDeps(doc, wsDeps),
88
+ ];
89
+ }
90
+
91
+ class CrateScanner {
92
+ workspaceRoot: string;
93
+ workspaceDeps: Record<string, any>;
94
+ discoveredPaths = new Set<string>();
95
+ crateInfos: { name: string; memberPath: string; doc: any }[] = [];
96
+
97
+ constructor(workspaceRoot: string, workspaceDeps: Record<string, any>) {
98
+ this.workspaceRoot = workspaceRoot;
99
+ this.workspaceDeps = workspaceDeps;
100
+ }
79
101
 
80
- if (fs.existsSync(rootCargo)) {
102
+ registerCrate(p: string) {
103
+ const absPath = path.resolve(this.workspaceRoot, p);
104
+ if (this.discoveredPaths.has(absPath)) return;
105
+ this.discoveredPaths.add(absPath);
106
+ const cargoPath = path.join(absPath, 'Cargo.toml');
107
+ if (!fs.existsSync(cargoPath)) return;
81
108
  try {
82
- const content = fs.readFileSync(rootCargo, 'utf8');
83
- const rootDoc = parse(content) as any;
84
- const members = rootDoc.workspace?.members;
85
-
86
- if (Array.isArray(members)) {
87
- const crateInfos: { name: string; memberPath: string; doc: any }[] = [];
88
-
89
- for (const member of members) {
90
- const globPath = path.join(cwd, member);
91
- const cargoPaths = globPath.endsWith('*')
92
- ? fs.existsSync(path.dirname(globPath))
93
- ? fs
94
- .readdirSync(path.dirname(globPath))
95
- .map((dir) => path.join(path.dirname(globPath), dir, 'Cargo.toml'))
96
- : []
97
- : [path.join(globPath, 'Cargo.toml')];
98
-
99
- for (const p of cargoPaths) {
100
- if (fs.existsSync(p)) {
101
- const memberPath = path.dirname(p);
102
- discoveredPaths.add(memberPath);
103
- const fileContent = fs.readFileSync(p, 'utf8');
104
- const doc = parse(fileContent) as any;
105
- const parsedName = doc.package?.name;
106
- if (parsedName) {
107
- crateInfos.push({ name: parsedName, memberPath, doc });
108
- }
109
- }
110
- }
111
- }
112
-
113
- // Discover nested packages dynamically via path dependencies on the parsed objects
114
- let i = 0;
115
- while (i < crateInfos.length) {
116
- const info = crateInfos[i]!;
117
- const deps = [
118
- ...(Object.values(info.doc.dependencies || {}) as any[]),
119
- ...(Object.values(info.doc['dev-dependencies'] || {}) as any[]),
120
- ...(Object.values(info.doc['build-dependencies'] || {}) as any[]),
121
- ];
122
-
123
- for (const dep of deps) {
124
- if (dep && typeof dep === 'object' && dep.path) {
125
- const fullDepPath = path.resolve(info.memberPath, dep.path);
126
- if (!discoveredPaths.has(fullDepPath)) {
127
- discoveredPaths.add(fullDepPath);
128
- const cargoPath = path.join(fullDepPath, 'Cargo.toml');
129
-
130
- if (fs.existsSync(cargoPath)) {
131
- const fileContent = fs.readFileSync(cargoPath, 'utf8');
132
- const doc = parse(fileContent) as any;
133
- const parsedName = doc.package?.name;
134
- if (parsedName) {
135
- crateInfos.push({ name: parsedName, memberPath: fullDepPath, doc });
136
- }
137
- }
138
- }
139
- }
140
- }
141
- i++;
142
- }
143
-
144
- for (const info of crateInfos) {
145
- const relativePath = path.relative(cwd, info.memberPath);
146
- const posixRelativePath = relativePath.split(path.sep).join(path.posix.sep);
147
- const relativeCargo = posixRelativePath
148
- ? path.posix.join(posixRelativePath, 'Cargo.toml')
149
- : 'Cargo.toml';
150
-
151
- const depends: string[] = [];
152
- const allDeps = {
153
- ...(info.doc.dependencies || {}),
154
- ...(info.doc['dev-dependencies'] || {}),
155
- ...(info.doc['build-dependencies'] || {}),
156
- };
157
-
158
- for (const other of crateInfos) {
159
- if (other.name === info.name) continue;
160
- if (allDeps[other.name]) {
161
- depends.push(other.name);
162
- }
163
- }
164
-
165
- const updates = [
166
- tomlUpdate(relativeCargo, (doc, report, reports) => {
167
- // Update self
168
- if (doc.package) {
169
- doc.package.version = report.newVersion;
170
- }
171
-
172
- // Keep local path workspace dependencies perfectly in sync internally
173
- const depTypes = ['dependencies', 'dev-dependencies', 'build-dependencies'];
174
- for (const depType of depTypes) {
175
- if (!doc[depType]) continue;
176
- for (const depReport of reports) {
177
- if (depReport.name === report.name) continue;
178
- const dep = doc[depType][depReport.name];
179
- if (dep && typeof dep === 'object' && dep.version) {
180
- dep.version = depReport.newVersion;
181
- } else if (typeof dep === 'string') {
182
- doc[depType][depReport.name] = depReport.newVersion;
183
- }
184
- }
185
- }
186
- }),
187
- ];
188
-
189
- if (hasLock) {
190
- updates.push(
191
- tomlUpdate('Cargo.lock', (doc, report) => {
192
- if (Array.isArray(doc.package)) {
193
- // Only update local packages (missing source/checksum attributes)
194
- const pkg = doc.package.find(
195
- (p: any) => p.name === info.name && !p.source && !p.checksum,
196
- );
197
- if (pkg) pkg.version = report.newVersion;
198
- }
199
- }),
200
- );
201
- }
202
-
203
- if (options?.syncWorkspaceDeps) {
204
- updates.push(
205
- tomlUpdate('Cargo.toml', (doc, report) => {
206
- if (doc.workspace?.dependencies?.[info.name]) {
207
- const dep = doc.workspace.dependencies[info.name];
208
- if (typeof dep === 'object' && dep.version) {
209
- dep.version = report.newVersion;
210
- } else if (typeof dep === 'string') {
211
- doc.workspace.dependencies[info.name] = report.newVersion;
212
- }
213
- }
214
- }),
215
- );
216
- }
217
-
218
- configs.push({
219
- name: info.name,
220
- watch: [posixRelativePath || '.'],
221
- depends,
222
- versionFallback: tomlFallback({
223
- path: relativeCargo,
224
- read: (doc) => doc.package?.version,
225
- }),
226
- updates,
227
- });
228
- }
109
+ const doc = parse(fs.readFileSync(cargoPath, 'utf8')) as any;
110
+ if (doc.package?.name) {
111
+ this.crateInfos.push({ name: doc.package.name, memberPath: absPath, doc });
229
112
  }
230
- } catch {
231
- // Ignore parse error
113
+ } catch { }
114
+ }
115
+ }
116
+
117
+ function expandGlob(workspaceRoot: string, member: string): string[] {
118
+ const globPath = path.join(workspaceRoot, member);
119
+ if (!globPath.endsWith('*')) return [path.join(globPath, 'Cargo.toml')];
120
+ const dir = path.dirname(globPath);
121
+ if (!fs.existsSync(dir)) return [];
122
+ return fs.readdirSync(dir).map((d) => path.join(dir, d, 'Cargo.toml'));
123
+ }
124
+
125
+ function registerWorkspaceMembers(scanner: CrateScanner, rootDoc: any) {
126
+ if (rootDoc.package?.name) {
127
+ scanner.registerCrate(scanner.workspaceRoot);
128
+ }
129
+ const members = rootDoc.workspace?.members;
130
+ if (!Array.isArray(members)) return;
131
+ for (const member of members) {
132
+ const paths = expandGlob(scanner.workspaceRoot, member).filter(fs.existsSync);
133
+ paths.forEach((p) => scanner.registerCrate(path.dirname(p)));
134
+ }
135
+ }
136
+
137
+ function runRecursiveDiscovery(scanner: CrateScanner) {
138
+ let i = 0;
139
+ while (i < scanner.crateInfos.length) {
140
+ const info = scanner.crateInfos[i]!;
141
+ const foundDeps = extractPathDependencies(info.doc, scanner.workspaceDeps);
142
+ for (const dep of foundDeps) {
143
+ const fullDepPath = path.resolve(info.memberPath, dep.path);
144
+ scanner.registerCrate(fullDepPath);
232
145
  }
146
+ i++;
233
147
  }
148
+ }
234
149
 
235
- return decorateList(configs);
150
+ function updateBlockEntry(block: any, depName: string, newVersion: string) {
151
+ const dep = block[depName];
152
+ if (dep && typeof dep === 'object' && dep.version) {
153
+ dep.version = newVersion;
154
+ } else if (typeof dep === 'string') {
155
+ block[depName] = newVersion;
156
+ }
236
157
  }
237
158
 
238
- export function cargoDeps(cwd: string, options?: CargoBuilderOptions): DependencyList {
239
- const rootCargo = path.join(cwd, 'Cargo.toml');
240
- if (fs.existsSync(rootCargo)) {
241
- try {
242
- const content = fs.readFileSync(rootCargo, 'utf8');
243
- const doc = parse(content) as any;
244
- if (doc.workspace?.members) {
245
- return cargoWorkspace(cwd, options);
246
- }
247
- if (doc.package?.name) {
248
- return cargoProject(cwd);
159
+ function syncDeps(block: any, reports: any[], selfName: string) {
160
+ if (!block || typeof block !== 'object') return;
161
+ for (const r of reports) {
162
+ if (r.name !== selfName && block[r.name]) {
163
+ updateBlockEntry(block, r.name, r.newVersion);
164
+ }
165
+ }
166
+ }
167
+
168
+ function syncAllBlocks(doc: any, reports: any[], selfName: string) {
169
+ syncDeps(doc.dependencies, reports, selfName);
170
+ syncDeps(doc['dev-dependencies'], reports, selfName);
171
+ syncDeps(doc['build-dependencies'], reports, selfName);
172
+ if (doc.target && typeof doc.target === 'object') {
173
+ for (const targetVal of Object.values(doc.target)) {
174
+ if (targetVal && typeof targetVal === 'object') {
175
+ syncDeps((targetVal as any).dependencies, reports, selfName);
176
+ syncDeps((targetVal as any)['dev-dependencies'], reports, selfName);
177
+ syncDeps((targetVal as any)['build-dependencies'], reports, selfName);
249
178
  }
250
- } catch {
251
- // Fallback below
252
179
  }
253
180
  }
254
- return decorateList([]);
181
+ }
182
+
183
+ function createSelfUpdate(relativeCargo: string, selfName: string) {
184
+ return tomlUpdate(relativeCargo, (doc, report, reports) => {
185
+ if (doc.package) {
186
+ doc.package.version = report.newVersion;
187
+ }
188
+ syncAllBlocks(doc, reports, selfName);
189
+ });
190
+ }
191
+
192
+ function createLockUpdate(selfName: string, hasLock: boolean) {
193
+ return tomlUpdate('Cargo.lock', (doc, report) => {
194
+ if (Array.isArray(doc.package)) {
195
+ const pkg = doc.package.find((p: any) => p.name === selfName && !p.source && !p.checksum);
196
+ if (pkg) pkg.version = report.newVersion;
197
+ }
198
+ }).skipIf(() => !hasLock);
199
+ }
200
+
201
+ function createWorkspaceDepUpdate(selfName: string, options?: CargoBuilderOptions) {
202
+ return tomlUpdate('Cargo.toml', (doc, report) => {
203
+ const dep = doc.workspace?.dependencies?.[selfName];
204
+ if (typeof dep === 'object' && dep.version) {
205
+ dep.version = report.newVersion;
206
+ } else if (typeof dep === 'string') {
207
+ doc.workspace.dependencies[selfName] = report.newVersion;
208
+ }
209
+ }).skipIf(() => !options?.syncWorkspaceDeps);
210
+ }
211
+
212
+ function buildCrateConfig(
213
+ info: any,
214
+ workspaceRoot: string,
215
+ localNames: Set<string>,
216
+ hasLock: boolean,
217
+ wsDeps: Record<string, any>,
218
+ options?: CargoBuilderOptions,
219
+ ): DependencyConfig {
220
+ const relPath = path.relative(workspaceRoot, info.memberPath);
221
+ const posixPath = relPath.split(path.sep).join(path.posix.sep);
222
+ const relCargo = posixPath ? path.posix.join(posixPath, 'Cargo.toml') : 'Cargo.toml';
223
+ const depends = extractPathDependencies(info.doc, wsDeps)
224
+ .map((fd) => fd.name)
225
+ .filter((name) => localNames.has(name) && name !== info.name);
226
+
227
+ return {
228
+ name: info.name,
229
+ watch: [posixPath || '.'],
230
+ depends,
231
+ versionFallback: tomlFallback({ path: relCargo, read: (doc) => doc.package?.version }),
232
+ updates: [
233
+ createSelfUpdate(relCargo, info.name),
234
+ createLockUpdate(info.name, hasLock),
235
+ createWorkspaceDepUpdate(info.name, options),
236
+ ],
237
+ };
238
+ }
239
+
240
+ export function cargoDeps(cwd: string, options?: CargoBuilderOptions): DependencyList {
241
+ const workspaceRoot = findCargoWorkspaceRoot(cwd);
242
+ const rootCargo = path.join(workspaceRoot, 'Cargo.toml');
243
+ if (!fs.existsSync(rootCargo)) return decorateList([]);
244
+ try {
245
+ const rootDoc = parse(fs.readFileSync(rootCargo, 'utf8')) as any;
246
+ const scanner = new CrateScanner(workspaceRoot, rootDoc.workspace?.dependencies || {});
247
+ if (rootDoc.workspace) registerWorkspaceMembers(scanner, rootDoc);
248
+ else if (rootDoc.package?.name) scanner.registerCrate(workspaceRoot);
249
+ runRecursiveDiscovery(scanner);
250
+ const hasLock = fs.existsSync(path.join(workspaceRoot, 'Cargo.lock'));
251
+ const localNames = new Set(scanner.crateInfos.map((c) => c.name));
252
+ const configs = scanner.crateInfos.map((info) =>
253
+ buildCrateConfig(info, workspaceRoot, localNames, hasLock, scanner.workspaceDeps, options),
254
+ );
255
+ return decorateList(configs);
256
+ } catch {
257
+ return decorateList([]);
258
+ }
255
259
  }
@@ -29,7 +29,7 @@ export function pnpmProject(cwd: string): DependencyList {
29
29
  updates: [
30
30
  jsonUpdate('package.json', (parsed, report) => {
31
31
  parsed.version = report.newVersion;
32
- }),
32
+ }).skipIf((cwd) => !fs.existsSync(path.join(cwd, 'package.json'))),
33
33
  ],
34
34
  });
35
35
  }
@@ -173,7 +173,7 @@ export function pnpmWorkspace(cwd: string): DependencyList {
173
173
  }
174
174
  }
175
175
  }
176
- }),
176
+ }).skipIf((cwd) => !fs.existsSync(path.join(cwd, relativePJson))),
177
177
  ],
178
178
  });
179
179
  }
package/src/prepare.ts CHANGED
@@ -33,12 +33,10 @@ export async function getCurrentVersion(
33
33
  vcs: VcsProvider,
34
34
  cwd: string,
35
35
  ): Promise<{ version: string | null; isFallback: boolean }> {
36
- // 1. Look for releases in git history first
37
36
  const latestTag = await vcs.getLatestTag(dep.name);
38
37
  if (latestTag) {
39
38
  let version = latestTag;
40
39
 
41
- // Strip package-specific prefixes (e.g., "api-v1.0.0" or "api/v1.0.0" -> "v1.0.0")
42
40
  if (version.startsWith(`${dep.name}-`)) {
43
41
  version = version.slice(dep.name.length + 1);
44
42
  } else if (version.startsWith(`${dep.name}/`)) {
@@ -48,7 +46,6 @@ export async function getCurrentVersion(
48
46
  return { version: version.replace(/^v/, ''), isFallback: false };
49
47
  }
50
48
 
51
- // 2. Resolve via user-defined fallback
52
49
  if (dep.versionFallback) {
53
50
  return { version: dep.versionFallback.readFallback(cwd), isFallback: true };
54
51
  }
@@ -145,6 +142,9 @@ export async function initReportItems(
145
142
 
146
143
  let isErroneous = isMissingVersion;
147
144
  for (const u of dep.updates || []) {
145
+ if (u._skipIf?.(cwd)) {
146
+ continue;
147
+ }
148
148
  if (u.required) {
149
149
  const filePath = path.resolve(cwd, u.path);
150
150
  if (!fs.existsSync(filePath)) {
@@ -20,6 +20,8 @@ export interface UpdateAction {
20
20
  required?: boolean;
21
21
  params: any;
22
22
  prepare(data: UpdateActionOptions): UpdateActionResolved;
23
+ skipIf(fn: (cwd: string) => boolean): this;
24
+ _skipIf(cwd: string): boolean;
23
25
  }
24
26
 
25
27
  export interface VersionFallback {
@@ -33,7 +35,7 @@ export type ApplyActionFnArgs<T, K = undefined> = {
33
35
  report: DependencyUpdateReport;
34
36
  reports: DependencyUpdateReport[];
35
37
  cwd: string;
36
- preparedData: K; // Added preparedData to the arguments
38
+ preparedData: K;
37
39
  };
38
40
 
39
41
  type ApplyActionFn<T, K = undefined> = (args: ApplyActionFnArgs<T, K>) => void;
@@ -48,36 +50,52 @@ type PrepareActionFn<T, K> = (args: PrepareActionFnArgs<T>) => K;
48
50
 
49
51
  type UpdateBuilderParams<T, K = undefined> = {
50
52
  kind: string;
51
- apply: ApplyActionFn<T, K>; // Passed K here
53
+ apply: ApplyActionFn<T, K>;
52
54
  prepare?: PrepareActionFn<T, K>;
53
55
  };
54
56
 
55
57
  export const updateBuilder =
56
58
  <T, K = undefined>({ kind, apply, prepare }: UpdateBuilderParams<T, K>) =>
57
- (targetPath: string, params: T & { required?: boolean }) => ({
58
- kind,
59
- path: targetPath,
60
- params,
61
- required: params.required,
62
- prepare(options: UpdateActionOptions): UpdateActionResolved {
63
- const preparedData = prepare?.({ params, targetPath, options }) as K;
59
+ (targetPath: string, params: T & { required?: boolean }) => {
60
+ let skipIfCallback: ((cwd: string) => boolean) | undefined;
64
61
 
65
- return {
66
- kind,
67
- targetPath,
68
- params,
69
- preparedData,
70
- apply(report: DependencyUpdateReport, reports: DependencyUpdateReport[], cwd: string) {
71
- apply({
72
- preparedData,
73
- targetPath,
74
- params,
75
- options,
76
- report,
77
- reports,
78
- cwd,
79
- });
80
- },
81
- };
82
- },
83
- });
62
+ const action: UpdateAction = {
63
+ kind,
64
+ path: targetPath,
65
+ params,
66
+ required: params.required,
67
+ skipIf(fn: (cwd: string) => boolean) {
68
+ skipIfCallback = fn;
69
+ return this;
70
+ },
71
+ _skipIf(cwd: string) {
72
+ return skipIfCallback ? skipIfCallback(cwd) : false;
73
+ },
74
+ prepare(options: UpdateActionOptions): UpdateActionResolved {
75
+ const preparedData = prepare?.({ params, targetPath, options }) as K;
76
+
77
+ return {
78
+ kind,
79
+ targetPath,
80
+ params,
81
+ preparedData,
82
+ apply(report: DependencyUpdateReport, reports: DependencyUpdateReport[], cwd: string) {
83
+ if (skipIfCallback?.(cwd)) {
84
+ return;
85
+ }
86
+ apply({
87
+ preparedData,
88
+ targetPath,
89
+ params,
90
+ options,
91
+ report,
92
+ reports,
93
+ cwd,
94
+ });
95
+ },
96
+ };
97
+ },
98
+ };
99
+
100
+ return action;
101
+ };
@@ -3,4 +3,3 @@ export * from './changelog';
3
3
  export * from './json';
4
4
  export * from './regex';
5
5
  export * from './toml';
6
-