@openrewrite/rewrite 8.69.0-20251210-164937 → 8.69.0-20251210-194227
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/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/javascript/dependency-manager.d.ts +31 -0
- package/dist/javascript/dependency-manager.d.ts.map +1 -0
- package/dist/javascript/dependency-manager.js +243 -0
- package/dist/javascript/dependency-manager.js.map +1 -0
- package/dist/javascript/index.d.ts +1 -0
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +1 -0
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/package-manager.d.ts +43 -0
- package/dist/javascript/package-manager.d.ts.map +1 -1
- package/dist/javascript/package-manager.js +85 -0
- package/dist/javascript/package-manager.js.map +1 -1
- package/dist/javascript/recipes/index.d.ts +1 -0
- package/dist/javascript/recipes/index.d.ts.map +1 -1
- package/dist/javascript/recipes/index.js +1 -0
- package/dist/javascript/recipes/index.js.map +1 -1
- package/dist/javascript/recipes/upgrade-dependency-version.d.ts +6 -6
- package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -1
- package/dist/javascript/recipes/upgrade-dependency-version.js +51 -86
- package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -1
- package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts +81 -0
- package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts.map +1 -0
- package/dist/javascript/recipes/upgrade-transitive-dependency-version.js +388 -0
- package/dist/javascript/recipes/upgrade-transitive-dependency-version.js.map +1 -0
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/javascript/dependency-manager.ts +292 -0
- package/src/javascript/index.ts +1 -0
- package/src/javascript/package-manager.ts +111 -0
- package/src/javascript/recipes/index.ts +1 -0
- package/src/javascript/recipes/upgrade-dependency-version.ts +67 -105
- package/src/javascript/recipes/upgrade-transitive-dependency-version.ts +445 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 the original author or authors.
|
|
3
|
+
* <p>
|
|
4
|
+
* Licensed under the Moderne Source Available License (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
* <p>
|
|
8
|
+
* https://docs.moderne.io/licensing/moderne-source-available-license
|
|
9
|
+
* <p>
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {PackageManager} from "./node-resolution-result";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parsed dependency path for scoped overrides.
|
|
21
|
+
* Segments represent the chain of dependencies, e.g., "express>accepts" becomes
|
|
22
|
+
* [{name: "express"}, {name: "accepts"}]
|
|
23
|
+
*/
|
|
24
|
+
export interface DependencyPathSegment {
|
|
25
|
+
name: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses a dependency path string into segments.
|
|
31
|
+
* Accepts both '>' (pnpm style) and '/' (yarn style) as separators.
|
|
32
|
+
* Examples:
|
|
33
|
+
* "express>accepts" -> [{name: "express"}, {name: "accepts"}]
|
|
34
|
+
* "express@4.0.0>accepts" -> [{name: "express", version: "4.0.0"}, {name: "accepts"}]
|
|
35
|
+
* "@scope/pkg>dep" -> [{name: "@scope/pkg"}, {name: "dep"}]
|
|
36
|
+
*/
|
|
37
|
+
export function parseDependencyPath(path: string): DependencyPathSegment[] {
|
|
38
|
+
// We can't just replace all '/' with '>' because scoped packages contain '/'
|
|
39
|
+
// Strategy: Split on '>' first, then for each part that contains '/' and doesn't
|
|
40
|
+
// start with '@', treat it as a '/'-separated path (yarn style)
|
|
41
|
+
const segments: DependencyPathSegment[] = [];
|
|
42
|
+
|
|
43
|
+
// Split on '>' (pnpm style separator)
|
|
44
|
+
const gtParts = path.split('>');
|
|
45
|
+
|
|
46
|
+
for (const gtPart of gtParts) {
|
|
47
|
+
// Check if this part needs further splitting by '/'
|
|
48
|
+
// Only split if it contains '/' AND either:
|
|
49
|
+
// - doesn't start with '@' (not a scoped package), OR
|
|
50
|
+
// - contains multiple '/' (e.g., "@scope/pkg/dep" is yarn-style path)
|
|
51
|
+
if (gtPart.includes('/')) {
|
|
52
|
+
if (gtPart.startsWith('@')) {
|
|
53
|
+
// Scoped package: @scope/pkg or @scope/pkg@version or @scope/pkg/dep (yarn path)
|
|
54
|
+
// Find the first '/' which is part of the scope
|
|
55
|
+
const firstSlash = gtPart.indexOf('/');
|
|
56
|
+
const afterFirstSlash = gtPart.substring(firstSlash + 1);
|
|
57
|
+
|
|
58
|
+
// Check if there's another '/' after the scope (yarn-style nesting)
|
|
59
|
+
const secondSlash = afterFirstSlash.indexOf('/');
|
|
60
|
+
if (secondSlash !== -1) {
|
|
61
|
+
// yarn-style: @scope/pkg/dep - split further
|
|
62
|
+
// First get the scoped package part
|
|
63
|
+
const scopedPart = gtPart.substring(0, firstSlash + 1 + secondSlash);
|
|
64
|
+
segments.push(parseSegment(scopedPart));
|
|
65
|
+
|
|
66
|
+
// Then handle the rest as separate segments
|
|
67
|
+
const rest = afterFirstSlash.substring(secondSlash + 1);
|
|
68
|
+
for (const subPart of rest.split('/')) {
|
|
69
|
+
if (subPart) {
|
|
70
|
+
segments.push(parseSegment(subPart));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// Simple scoped package: @scope/pkg or @scope/pkg@version
|
|
75
|
+
segments.push(parseSegment(gtPart));
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
// Non-scoped with '/': yarn-style path like "express/accepts"
|
|
79
|
+
for (const slashPart of gtPart.split('/')) {
|
|
80
|
+
if (slashPart) {
|
|
81
|
+
segments.push(parseSegment(slashPart));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// No '/', just parse the segment directly
|
|
87
|
+
segments.push(parseSegment(gtPart));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return segments;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parses a single segment (package name, possibly with version).
|
|
96
|
+
*/
|
|
97
|
+
function parseSegment(part: string): DependencyPathSegment {
|
|
98
|
+
// Handle scoped packages: @scope/name or @scope/name@version
|
|
99
|
+
if (part.startsWith('@')) {
|
|
100
|
+
// Find the version separator (last @ that's not the scope prefix)
|
|
101
|
+
const slashIndex = part.indexOf('/');
|
|
102
|
+
if (slashIndex === -1) {
|
|
103
|
+
return {name: part};
|
|
104
|
+
}
|
|
105
|
+
const afterSlash = part.substring(slashIndex + 1);
|
|
106
|
+
const atIndex = afterSlash.lastIndexOf('@');
|
|
107
|
+
if (atIndex > 0) {
|
|
108
|
+
return {
|
|
109
|
+
name: part.substring(0, slashIndex + 1 + atIndex),
|
|
110
|
+
version: afterSlash.substring(atIndex + 1)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {name: part};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Non-scoped package: name or name@version
|
|
117
|
+
const atIndex = part.lastIndexOf('@');
|
|
118
|
+
if (atIndex > 0) {
|
|
119
|
+
return {
|
|
120
|
+
name: part.substring(0, atIndex),
|
|
121
|
+
version: part.substring(atIndex + 1)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {name: part};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generates an npm-style override entry (nested objects).
|
|
129
|
+
* npm uses nested objects for scoped overrides:
|
|
130
|
+
* { "express": { "accepts": "^2.0.0" } }
|
|
131
|
+
* or for global overrides:
|
|
132
|
+
* { "lodash": "^4.17.21" }
|
|
133
|
+
*/
|
|
134
|
+
function generateNpmOverride(
|
|
135
|
+
packageName: string,
|
|
136
|
+
newVersion: string,
|
|
137
|
+
pathSegments?: DependencyPathSegment[]
|
|
138
|
+
): Record<string, any> {
|
|
139
|
+
if (!pathSegments || pathSegments.length === 0) {
|
|
140
|
+
// Global override
|
|
141
|
+
return {[packageName]: newVersion};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build nested structure from outside in
|
|
145
|
+
let result: Record<string, any> = {[packageName]: newVersion};
|
|
146
|
+
for (let i = pathSegments.length - 1; i >= 0; i--) {
|
|
147
|
+
const segment = pathSegments[i];
|
|
148
|
+
const key = segment.version ? `${segment.name}@${segment.version}` : segment.name;
|
|
149
|
+
result = {[key]: result};
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generates a Yarn-style resolution entry (path with / separator).
|
|
156
|
+
* Yarn uses a flat object with path keys:
|
|
157
|
+
* { "express/accepts": "^2.0.0" }
|
|
158
|
+
* or for global:
|
|
159
|
+
* { "lodash": "^4.17.21" }
|
|
160
|
+
*/
|
|
161
|
+
function generateYarnResolution(
|
|
162
|
+
packageName: string,
|
|
163
|
+
newVersion: string,
|
|
164
|
+
pathSegments?: DependencyPathSegment[]
|
|
165
|
+
): Record<string, string> {
|
|
166
|
+
if (!pathSegments || pathSegments.length === 0) {
|
|
167
|
+
// Global resolution
|
|
168
|
+
return {[packageName]: newVersion};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Yarn uses / separator: "express/accepts"
|
|
172
|
+
// Note: Yarn only supports one level of nesting, so we use the last segment
|
|
173
|
+
const parentSegment = pathSegments[pathSegments.length - 1];
|
|
174
|
+
const parentKey = parentSegment.version
|
|
175
|
+
? `${parentSegment.name}@${parentSegment.version}`
|
|
176
|
+
: parentSegment.name;
|
|
177
|
+
const key = `${parentKey}/${packageName}`;
|
|
178
|
+
return {[key]: newVersion};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Generates a pnpm-style override entry (path with > separator).
|
|
183
|
+
* pnpm uses a flat object with > path keys:
|
|
184
|
+
* { "express>accepts": "^2.0.0" }
|
|
185
|
+
* or for global:
|
|
186
|
+
* { "lodash": "^4.17.21" }
|
|
187
|
+
*/
|
|
188
|
+
function generatePnpmOverride(
|
|
189
|
+
packageName: string,
|
|
190
|
+
newVersion: string,
|
|
191
|
+
pathSegments?: DependencyPathSegment[]
|
|
192
|
+
): Record<string, string> {
|
|
193
|
+
if (!pathSegments || pathSegments.length === 0) {
|
|
194
|
+
// Global override
|
|
195
|
+
return {[packageName]: newVersion};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// pnpm uses > separator: "express@1>accepts"
|
|
199
|
+
const pathParts = pathSegments.map(seg =>
|
|
200
|
+
seg.version ? `${seg.name}@${seg.version}` : seg.name
|
|
201
|
+
);
|
|
202
|
+
const key = `${pathParts.join('>')}>${packageName}`;
|
|
203
|
+
return {[key]: newVersion};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Merges a new override entry into an existing overrides object.
|
|
208
|
+
* Handles npm's nested structure by deep merging.
|
|
209
|
+
*/
|
|
210
|
+
function mergeNpmOverride(
|
|
211
|
+
existing: Record<string, any>,
|
|
212
|
+
newOverride: Record<string, any>
|
|
213
|
+
): Record<string, any> {
|
|
214
|
+
const result = {...existing};
|
|
215
|
+
|
|
216
|
+
for (const [key, value] of Object.entries(newOverride)) {
|
|
217
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
218
|
+
// Deep merge for nested objects
|
|
219
|
+
if (typeof result[key] === 'object' && result[key] !== null) {
|
|
220
|
+
result[key] = mergeNpmOverride(result[key], value);
|
|
221
|
+
} else {
|
|
222
|
+
result[key] = value;
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// Simple value, just overwrite
|
|
226
|
+
result[key] = value;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Merges a new resolution/override entry into an existing flat object.
|
|
235
|
+
* Used for Yarn resolutions and pnpm overrides.
|
|
236
|
+
*/
|
|
237
|
+
function mergeFlatOverride(
|
|
238
|
+
existing: Record<string, string>,
|
|
239
|
+
newOverride: Record<string, string>
|
|
240
|
+
): Record<string, string> {
|
|
241
|
+
return {...existing, ...newOverride};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Applies an override to a package.json object based on the package manager.
|
|
246
|
+
*
|
|
247
|
+
* @param packageJson The parsed package.json object
|
|
248
|
+
* @param packageManager The package manager in use
|
|
249
|
+
* @param packageName The target package to override
|
|
250
|
+
* @param newVersion The version to set
|
|
251
|
+
* @param pathSegments Optional path segments for scoped override
|
|
252
|
+
* @returns The modified package.json object
|
|
253
|
+
*/
|
|
254
|
+
export function applyOverrideToPackageJson(
|
|
255
|
+
packageJson: Record<string, any>,
|
|
256
|
+
packageManager: PackageManager,
|
|
257
|
+
packageName: string,
|
|
258
|
+
newVersion: string,
|
|
259
|
+
pathSegments?: DependencyPathSegment[]
|
|
260
|
+
): Record<string, any> {
|
|
261
|
+
const result = {...packageJson};
|
|
262
|
+
|
|
263
|
+
switch (packageManager) {
|
|
264
|
+
case PackageManager.Npm:
|
|
265
|
+
case PackageManager.Bun: {
|
|
266
|
+
// npm and Bun use "overrides" with nested objects
|
|
267
|
+
const newOverride = generateNpmOverride(packageName, newVersion, pathSegments);
|
|
268
|
+
result.overrides = mergeNpmOverride(result.overrides || {}, newOverride);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case PackageManager.YarnClassic:
|
|
273
|
+
case PackageManager.YarnBerry: {
|
|
274
|
+
// Yarn uses "resolutions" with flat path keys
|
|
275
|
+
const newResolution = generateYarnResolution(packageName, newVersion, pathSegments);
|
|
276
|
+
result.resolutions = mergeFlatOverride(result.resolutions || {}, newResolution);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
case PackageManager.Pnpm: {
|
|
281
|
+
// pnpm uses "pnpm.overrides" with > path keys
|
|
282
|
+
const newOverride = generatePnpmOverride(packageName, newVersion, pathSegments);
|
|
283
|
+
if (!result.pnpm) {
|
|
284
|
+
result.pnpm = {};
|
|
285
|
+
}
|
|
286
|
+
result.pnpm.overrides = mergeFlatOverride(result.pnpm?.overrides || {}, newOverride);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return result;
|
|
292
|
+
}
|
package/src/javascript/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * from "./markers";
|
|
|
22
22
|
export * from "./node-resolution-result";
|
|
23
23
|
export * from "./package-json-parser";
|
|
24
24
|
export * from "./package-manager";
|
|
25
|
+
export * from "./dependency-manager";
|
|
25
26
|
export * from "./preconditions";
|
|
26
27
|
export * from "./templating/index";
|
|
27
28
|
export * from "./method-matcher";
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
import {PackageManager} from "./node-resolution-result";
|
|
18
18
|
import * as fs from "fs";
|
|
19
|
+
import * as fsp from "fs/promises";
|
|
19
20
|
import * as path from "path";
|
|
21
|
+
import * as os from "os";
|
|
20
22
|
import {spawnSync} from "child_process";
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -426,3 +428,112 @@ export function getPackageManagerDisplayName(pm: PackageManager): string {
|
|
|
426
428
|
return 'Bun';
|
|
427
429
|
}
|
|
428
430
|
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Result of running install in a temporary directory.
|
|
434
|
+
*/
|
|
435
|
+
export interface TempInstallResult {
|
|
436
|
+
/** Whether the install succeeded */
|
|
437
|
+
success: boolean;
|
|
438
|
+
/** The updated lock file content (if successful and lock file exists) */
|
|
439
|
+
lockFileContent?: string;
|
|
440
|
+
/** Error message (if failed) */
|
|
441
|
+
error?: string;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Options for running install in a temporary directory.
|
|
446
|
+
*/
|
|
447
|
+
export interface TempInstallOptions {
|
|
448
|
+
/** Timeout in milliseconds (default: 120000 = 2 minutes) */
|
|
449
|
+
timeout?: number;
|
|
450
|
+
/**
|
|
451
|
+
* If true, only update the lock file without installing node_modules.
|
|
452
|
+
* If false, perform a full install which creates node_modules in the temp dir.
|
|
453
|
+
* Default: true (lock-only is faster and sufficient for most cases)
|
|
454
|
+
*/
|
|
455
|
+
lockOnly?: boolean;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Runs package manager install in a temporary directory.
|
|
460
|
+
*
|
|
461
|
+
* This function:
|
|
462
|
+
* 1. Creates a temp directory
|
|
463
|
+
* 2. Writes the provided package.json content
|
|
464
|
+
* 3. Copies the existing lock file (if present)
|
|
465
|
+
* 4. Copies config files (.npmrc, .yarnrc, etc.)
|
|
466
|
+
* 5. Runs the package manager install
|
|
467
|
+
* 6. Returns the updated lock file content
|
|
468
|
+
* 7. Cleans up the temp directory
|
|
469
|
+
*
|
|
470
|
+
* @param projectDir The original project directory (for copying lock file and configs)
|
|
471
|
+
* @param pm The package manager to use
|
|
472
|
+
* @param modifiedPackageJson The modified package.json content to use
|
|
473
|
+
* @param options Optional settings for timeout and lock-only mode
|
|
474
|
+
* @returns Result containing success status and lock file content or error
|
|
475
|
+
*/
|
|
476
|
+
export async function runInstallInTempDir(
|
|
477
|
+
projectDir: string,
|
|
478
|
+
pm: PackageManager,
|
|
479
|
+
modifiedPackageJson: string,
|
|
480
|
+
options: TempInstallOptions = {}
|
|
481
|
+
): Promise<TempInstallResult> {
|
|
482
|
+
const {timeout = 120000, lockOnly = true} = options;
|
|
483
|
+
const lockFileName = getLockFileName(pm);
|
|
484
|
+
const tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'openrewrite-pm-'));
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
// Write modified package.json to temp directory
|
|
488
|
+
await fsp.writeFile(path.join(tempDir, 'package.json'), modifiedPackageJson);
|
|
489
|
+
|
|
490
|
+
// Copy existing lock file if present
|
|
491
|
+
const originalLockPath = path.join(projectDir, lockFileName);
|
|
492
|
+
if (fs.existsSync(originalLockPath)) {
|
|
493
|
+
await fsp.copyFile(originalLockPath, path.join(tempDir, lockFileName));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Copy config files if present (for registry configuration and workspace setup)
|
|
497
|
+
const configFiles = ['.npmrc', '.yarnrc', '.yarnrc.yml', '.pnpmfile.cjs', 'pnpm-workspace.yaml'];
|
|
498
|
+
for (const configFile of configFiles) {
|
|
499
|
+
const configPath = path.join(projectDir, configFile);
|
|
500
|
+
if (fs.existsSync(configPath)) {
|
|
501
|
+
await fsp.copyFile(configPath, path.join(tempDir, configFile));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Run package manager install
|
|
506
|
+
const result = runInstall(pm, {
|
|
507
|
+
cwd: tempDir,
|
|
508
|
+
lockOnly,
|
|
509
|
+
timeout
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
if (!result.success) {
|
|
513
|
+
return {
|
|
514
|
+
success: false,
|
|
515
|
+
error: result.error || result.stderr || 'Unknown error'
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Read back the updated lock file
|
|
520
|
+
const updatedLockPath = path.join(tempDir, lockFileName);
|
|
521
|
+
let lockFileContent: string | undefined;
|
|
522
|
+
if (fs.existsSync(updatedLockPath)) {
|
|
523
|
+
lockFileContent = await fsp.readFile(updatedLockPath, 'utf-8');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
success: true,
|
|
528
|
+
lockFileContent
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
} finally {
|
|
532
|
+
// Cleanup temp directory
|
|
533
|
+
try {
|
|
534
|
+
await fsp.rm(tempDir, {recursive: true, force: true});
|
|
535
|
+
} catch {
|
|
536
|
+
// Ignore cleanup errors
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
@@ -17,5 +17,6 @@
|
|
|
17
17
|
export * from "./async-callback-in-sync-array-method";
|
|
18
18
|
export * from "./auto-format";
|
|
19
19
|
export * from "./upgrade-dependency-version";
|
|
20
|
+
export * from "./upgrade-transitive-dependency-version";
|
|
20
21
|
export * from "./order-imports";
|
|
21
22
|
export * from "./change-import";
|
|
@@ -26,18 +26,11 @@ import {
|
|
|
26
26
|
PackageManager,
|
|
27
27
|
readNpmrcConfigs
|
|
28
28
|
} from "../node-resolution-result";
|
|
29
|
-
import * as fs from "fs";
|
|
30
|
-
import * as fsp from "fs/promises";
|
|
31
29
|
import * as path from "path";
|
|
32
|
-
import * as os from "os";
|
|
33
30
|
import * as semver from "semver";
|
|
34
31
|
import {markupWarn, replaceMarkerByKind} from "../../markers";
|
|
35
32
|
import {TreePrinters} from "../../print";
|
|
36
|
-
import {
|
|
37
|
-
getAllLockFileNames,
|
|
38
|
-
getLockFileName,
|
|
39
|
-
runInstall
|
|
40
|
-
} from "../package-manager";
|
|
33
|
+
import {getAllLockFileNames, getLockFileName, runInstallInTempDir} from "../package-manager";
|
|
41
34
|
|
|
42
35
|
/**
|
|
43
36
|
* Represents a dependency scope in package.json
|
|
@@ -90,7 +83,7 @@ interface Accumulator {
|
|
|
90
83
|
}
|
|
91
84
|
|
|
92
85
|
/**
|
|
93
|
-
* Upgrades the version of a dependency in package.json and updates the lock file.
|
|
86
|
+
* Upgrades the version of a direct dependency in package.json and updates the lock file.
|
|
94
87
|
*
|
|
95
88
|
* This recipe:
|
|
96
89
|
* 1. Finds package.json files containing the specified dependency
|
|
@@ -98,15 +91,15 @@ interface Accumulator {
|
|
|
98
91
|
* 3. Runs the package manager to update the lock file
|
|
99
92
|
* 4. Updates the NodeResolutionResult marker with new dependency info
|
|
100
93
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
94
|
+
* For upgrading transitive dependencies (those pulled in indirectly by your direct
|
|
95
|
+
* dependencies), use `UpgradeTransitiveDependencyVersion` instead.
|
|
96
|
+
*
|
|
97
|
+
* @see UpgradeTransitiveDependencyVersion for transitive dependencies
|
|
105
98
|
*/
|
|
106
99
|
export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
|
|
107
100
|
readonly name = "org.openrewrite.javascript.dependencies.upgrade-dependency-version";
|
|
108
101
|
readonly displayName = "Upgrade npm dependency version";
|
|
109
|
-
readonly description = "Upgrades the version of a dependency in `package.json` and updates the lock file by running the package manager.";
|
|
102
|
+
readonly description = "Upgrades the version of a direct dependency in `package.json` and updates the lock file by running the package manager.";
|
|
110
103
|
|
|
111
104
|
@Option({
|
|
112
105
|
displayName: "Package name",
|
|
@@ -169,7 +162,7 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
|
|
|
169
162
|
const recipe = this;
|
|
170
163
|
|
|
171
164
|
return new class extends JsonVisitor<ExecutionContext> {
|
|
172
|
-
protected async visitDocument(doc: Json.Document,
|
|
165
|
+
protected async visitDocument(doc: Json.Document, _ctx: ExecutionContext): Promise<Json | undefined> {
|
|
173
166
|
// Only process package.json files
|
|
174
167
|
if (!doc.sourcePath.endsWith('package.json')) {
|
|
175
168
|
return doc;
|
|
@@ -180,48 +173,54 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
|
|
|
180
173
|
return doc;
|
|
181
174
|
}
|
|
182
175
|
|
|
176
|
+
// Get the project directory and package manager
|
|
177
|
+
const projectDir = path.dirname(path.resolve(doc.sourcePath));
|
|
178
|
+
const pm = marker.packageManager ?? PackageManager.Npm;
|
|
179
|
+
|
|
183
180
|
// Check each dependency scope for the target package
|
|
184
181
|
const scopes: DependencyScope[] = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
182
|
+
let foundScope: DependencyScope | undefined;
|
|
183
|
+
let currentVersion: string | undefined;
|
|
185
184
|
|
|
186
185
|
for (const scope of scopes) {
|
|
187
186
|
const deps = marker[scope];
|
|
188
187
|
const dep = deps?.find(d => d.name === recipe.packageName);
|
|
189
188
|
|
|
190
189
|
if (dep) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Only upgrade if the new version is strictly newer than current
|
|
195
|
-
if (recipe.shouldUpgrade(currentVersion, recipe.newVersion)) {
|
|
196
|
-
// Get the project directory from the marker path
|
|
197
|
-
const projectDir = path.dirname(path.resolve(doc.sourcePath));
|
|
198
|
-
|
|
199
|
-
// Use package manager from marker (set during parsing), default to npm
|
|
200
|
-
const pm = marker.packageManager ?? PackageManager.Npm;
|
|
201
|
-
|
|
202
|
-
// Check if the resolved version already satisfies the new constraint.
|
|
203
|
-
// If so, we can skip running the package manager entirely.
|
|
204
|
-
const resolvedDep = marker.resolvedDependencies?.find(
|
|
205
|
-
rd => rd.name === recipe.packageName
|
|
206
|
-
);
|
|
207
|
-
const skipInstall = resolvedDep !== undefined &&
|
|
208
|
-
semver.satisfies(resolvedDep.version, recipe.newVersion);
|
|
209
|
-
|
|
210
|
-
acc.projectsToUpdate.set(doc.sourcePath, {
|
|
211
|
-
projectDir,
|
|
212
|
-
packageJsonPath: doc.sourcePath,
|
|
213
|
-
originalPackageJson: await this.printDocument(doc),
|
|
214
|
-
dependencyScope: scope,
|
|
215
|
-
currentVersion,
|
|
216
|
-
newVersion: recipe.newVersion,
|
|
217
|
-
packageManager: pm,
|
|
218
|
-
skipInstall
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
break; // Found the dependency, no need to check other scopes
|
|
190
|
+
foundScope = scope;
|
|
191
|
+
currentVersion = dep.versionConstraint;
|
|
192
|
+
break;
|
|
222
193
|
}
|
|
223
194
|
}
|
|
224
195
|
|
|
196
|
+
if (!foundScope || !currentVersion) {
|
|
197
|
+
return doc; // Dependency not found in any scope
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check if upgrade is needed
|
|
201
|
+
if (!recipe.shouldUpgrade(currentVersion, recipe.newVersion)) {
|
|
202
|
+
return doc; // Already at target version or newer
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if we can skip running the package manager
|
|
206
|
+
// (resolved version already satisfies the new constraint)
|
|
207
|
+
const resolvedDep = marker.resolvedDependencies?.find(
|
|
208
|
+
rd => rd.name === recipe.packageName
|
|
209
|
+
);
|
|
210
|
+
const skipInstall = resolvedDep !== undefined &&
|
|
211
|
+
semver.satisfies(resolvedDep.version, recipe.newVersion);
|
|
212
|
+
|
|
213
|
+
acc.projectsToUpdate.set(doc.sourcePath, {
|
|
214
|
+
projectDir,
|
|
215
|
+
packageJsonPath: doc.sourcePath,
|
|
216
|
+
originalPackageJson: await this.printDocument(doc),
|
|
217
|
+
dependencyScope: foundScope,
|
|
218
|
+
currentVersion,
|
|
219
|
+
newVersion: recipe.newVersion,
|
|
220
|
+
packageManager: pm,
|
|
221
|
+
skipInstall
|
|
222
|
+
});
|
|
223
|
+
|
|
225
224
|
return doc;
|
|
226
225
|
}
|
|
227
226
|
|
|
@@ -331,69 +330,32 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
|
|
|
331
330
|
updateInfo: ProjectUpdateInfo,
|
|
332
331
|
_ctx: ExecutionContext
|
|
333
332
|
): Promise<void> {
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
// Create modified package.json with the new version constraint
|
|
342
|
-
const modifiedPackageJson = this.createModifiedPackageJson(
|
|
343
|
-
updateInfo.originalPackageJson,
|
|
344
|
-
updateInfo.dependencyScope,
|
|
345
|
-
updateInfo.newVersion
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
// Write modified package.json to temp directory
|
|
349
|
-
await fsp.writeFile(path.join(tempDir, 'package.json'), modifiedPackageJson);
|
|
350
|
-
|
|
351
|
-
// Copy existing lock file if present
|
|
352
|
-
const originalLockPath = path.join(updateInfo.projectDir, lockFileName);
|
|
353
|
-
if (fs.existsSync(originalLockPath)) {
|
|
354
|
-
await fsp.copyFile(originalLockPath, path.join(tempDir, lockFileName));
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Copy config files if present (for registry configuration and workspace setup)
|
|
358
|
-
const configFiles = ['.npmrc', '.yarnrc', '.yarnrc.yml', '.pnpmfile.cjs', 'pnpm-workspace.yaml'];
|
|
359
|
-
for (const configFile of configFiles) {
|
|
360
|
-
const configPath = path.join(updateInfo.projectDir, configFile);
|
|
361
|
-
if (fs.existsSync(configPath)) {
|
|
362
|
-
await fsp.copyFile(configPath, path.join(tempDir, configFile));
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Run package manager install to validate the version and update lock file
|
|
367
|
-
const result = runInstall(pm, {
|
|
368
|
-
cwd: tempDir,
|
|
369
|
-
lockOnly: true,
|
|
370
|
-
timeout: 120000 // 2 minute timeout
|
|
371
|
-
});
|
|
333
|
+
// Create modified package.json with the new version constraint
|
|
334
|
+
const modifiedPackageJson = this.createModifiedPackageJson(
|
|
335
|
+
updateInfo.originalPackageJson,
|
|
336
|
+
updateInfo.dependencyScope,
|
|
337
|
+
updateInfo.newVersion
|
|
338
|
+
);
|
|
372
339
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
340
|
+
const result = await runInstallInTempDir(
|
|
341
|
+
updateInfo.projectDir,
|
|
342
|
+
updateInfo.packageManager,
|
|
343
|
+
modifiedPackageJson
|
|
344
|
+
);
|
|
376
345
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const updatedLockContent = await fsp.readFile(updatedLockPath, 'utf-8');
|
|
381
|
-
const lockFilePath = updateInfo.packageJsonPath.replace('package.json', lockFileName);
|
|
382
|
-
acc.updatedLockFiles.set(lockFilePath, updatedLockContent);
|
|
383
|
-
}
|
|
384
|
-
} else {
|
|
385
|
-
// Track the failure - don't update package.json, the version likely doesn't exist
|
|
386
|
-
const errorMessage = result.error || result.stderr || 'Unknown error';
|
|
387
|
-
acc.failedProjects.set(updateInfo.packageJsonPath, errorMessage);
|
|
388
|
-
}
|
|
346
|
+
if (result.success) {
|
|
347
|
+
// Store the modified package.json (we'll use our visitor for actual output)
|
|
348
|
+
acc.updatedPackageJsons.set(updateInfo.packageJsonPath, modifiedPackageJson);
|
|
389
349
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// Ignore cleanup errors
|
|
350
|
+
// Store the updated lock file content
|
|
351
|
+
if (result.lockFileContent) {
|
|
352
|
+
const lockFileName = getLockFileName(updateInfo.packageManager);
|
|
353
|
+
const lockFilePath = updateInfo.packageJsonPath.replace('package.json', lockFileName);
|
|
354
|
+
acc.updatedLockFiles.set(lockFilePath, result.lockFileContent);
|
|
396
355
|
}
|
|
356
|
+
} else {
|
|
357
|
+
// Track the failure - don't update package.json, the version likely doesn't exist
|
|
358
|
+
acc.failedProjects.set(updateInfo.packageJsonPath, result.error || 'Unknown error');
|
|
397
359
|
}
|
|
398
360
|
}
|
|
399
361
|
|