@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.
Files changed (36) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +2 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/javascript/dependency-manager.d.ts +31 -0
  5. package/dist/javascript/dependency-manager.d.ts.map +1 -0
  6. package/dist/javascript/dependency-manager.js +243 -0
  7. package/dist/javascript/dependency-manager.js.map +1 -0
  8. package/dist/javascript/index.d.ts +1 -0
  9. package/dist/javascript/index.d.ts.map +1 -1
  10. package/dist/javascript/index.js +1 -0
  11. package/dist/javascript/index.js.map +1 -1
  12. package/dist/javascript/package-manager.d.ts +43 -0
  13. package/dist/javascript/package-manager.d.ts.map +1 -1
  14. package/dist/javascript/package-manager.js +85 -0
  15. package/dist/javascript/package-manager.js.map +1 -1
  16. package/dist/javascript/recipes/index.d.ts +1 -0
  17. package/dist/javascript/recipes/index.d.ts.map +1 -1
  18. package/dist/javascript/recipes/index.js +1 -0
  19. package/dist/javascript/recipes/index.js.map +1 -1
  20. package/dist/javascript/recipes/upgrade-dependency-version.d.ts +6 -6
  21. package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -1
  22. package/dist/javascript/recipes/upgrade-dependency-version.js +51 -86
  23. package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -1
  24. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts +81 -0
  25. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts.map +1 -0
  26. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js +388 -0
  27. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js.map +1 -0
  28. package/dist/version.txt +1 -1
  29. package/package.json +1 -1
  30. package/src/index.ts +2 -1
  31. package/src/javascript/dependency-manager.ts +292 -0
  32. package/src/javascript/index.ts +1 -0
  33. package/src/javascript/package-manager.ts +111 -0
  34. package/src/javascript/recipes/index.ts +1 -0
  35. package/src/javascript/recipes/upgrade-dependency-version.ts +67 -105
  36. package/src/javascript/recipes/upgrade-transitive-dependency-version.ts +445 -0
@@ -0,0 +1,445 @@
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 {Option, ScanningRecipe} from "../../recipe";
18
+ import {ExecutionContext} from "../../execution";
19
+ import {TreeVisitor} from "../../visitor";
20
+ import {Json, JsonParser, JsonVisitor} from "../../json";
21
+ import {
22
+ createNodeResolutionResultMarker,
23
+ findNodeResolutionResult,
24
+ NodeResolutionResultQueries,
25
+ PackageJsonContent,
26
+ PackageLockContent,
27
+ PackageManager,
28
+ readNpmrcConfigs
29
+ } from "../node-resolution-result";
30
+ import * as path from "path";
31
+ import * as semver from "semver";
32
+ import {markupWarn, replaceMarkerByKind} from "../../markers";
33
+ import {TreePrinters} from "../../print";
34
+ import {getAllLockFileNames, getLockFileName, runInstallInTempDir} from "../package-manager";
35
+ import {applyOverrideToPackageJson, DependencyPathSegment, parseDependencyPath} from "../dependency-manager";
36
+
37
+ /**
38
+ * Information about a project that needs updating
39
+ */
40
+ interface ProjectUpdateInfo {
41
+ /** Absolute path to the project directory */
42
+ projectDir: string;
43
+ /** Relative path to package.json (from source root) */
44
+ packageJsonPath: string;
45
+ /** Original package.json content */
46
+ originalPackageJson: string;
47
+ /** New version constraint to apply */
48
+ newVersion: string;
49
+ /** The package manager used by this project */
50
+ packageManager: PackageManager;
51
+ /**
52
+ * If true, skip running the package manager because the resolved version
53
+ * already satisfies the new constraint. Only package.json needs updating.
54
+ */
55
+ skipInstall: boolean;
56
+ /** Parsed dependency path for scoped overrides (if specified) */
57
+ dependencyPathSegments?: DependencyPathSegment[];
58
+ }
59
+
60
+ /**
61
+ * Accumulator for tracking state across scanning and editing phases
62
+ */
63
+ interface Accumulator {
64
+ /** Projects that need updating: packageJsonPath -> update info */
65
+ projectsToUpdate: Map<string, ProjectUpdateInfo>;
66
+
67
+ /** After running package manager, store the updated lock file content */
68
+ updatedLockFiles: Map<string, string>;
69
+
70
+ /** Updated package.json content (after npm install may have modified it) */
71
+ updatedPackageJsons: Map<string, string>;
72
+
73
+ /** Track which projects have been processed (npm install has run) */
74
+ processedProjects: Set<string>;
75
+
76
+ /** Track projects where npm install failed: packageJsonPath -> error message */
77
+ failedProjects: Map<string, string>;
78
+ }
79
+
80
+ /**
81
+ * Upgrades the version of a transitive dependency by adding override entries to package.json.
82
+ *
83
+ * This recipe is used when you need to upgrade a dependency that is not directly declared
84
+ * in your package.json, but is pulled in transitively by one of your direct dependencies.
85
+ * This is commonly needed for security vulnerability remediation.
86
+ *
87
+ * The recipe adds entries to:
88
+ * - `overrides` for npm and Bun
89
+ * - `resolutions` for Yarn (Classic and Berry)
90
+ * - `pnpm.overrides` for pnpm
91
+ *
92
+ * @see UpgradeDependencyVersion for upgrading direct dependencies
93
+ */
94
+ export class UpgradeTransitiveDependencyVersion extends ScanningRecipe<Accumulator> {
95
+ readonly name = "org.openrewrite.javascript.dependencies.upgrade-transitive-dependency-version";
96
+ readonly displayName = "Upgrade transitive npm dependency version";
97
+ readonly description = "Upgrades the version of a transitive dependency by adding override/resolution entries to `package.json` and updates the lock file by running the package manager.";
98
+
99
+ @Option({
100
+ displayName: "Package name",
101
+ description: "The name of the npm package to upgrade (e.g., 'lodash', '@types/node')",
102
+ example: "lodash"
103
+ })
104
+ packageName!: string;
105
+
106
+ @Option({
107
+ displayName: "New version",
108
+ description: "The new version constraint to set (e.g., '^5.0.0', '~2.1.0', '3.0.0')",
109
+ example: "^5.0.0"
110
+ })
111
+ newVersion!: string;
112
+
113
+ @Option({
114
+ displayName: "Dependency path",
115
+ description: "Optional path to scope the override to a specific dependency chain. Use '>' as separator (e.g., 'express>accepts'). When not specified, applies globally to all transitive occurrences.",
116
+ required: false,
117
+ example: "express>accepts"
118
+ })
119
+ dependencyPath?: string;
120
+
121
+ initialValue(_ctx: ExecutionContext): Accumulator {
122
+ return {
123
+ projectsToUpdate: new Map(),
124
+ updatedLockFiles: new Map(),
125
+ updatedPackageJsons: new Map(),
126
+ processedProjects: new Set(),
127
+ failedProjects: new Map()
128
+ };
129
+ }
130
+
131
+ async scanner(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
132
+ const recipe = this;
133
+
134
+ return new class extends JsonVisitor<ExecutionContext> {
135
+ protected async visitDocument(doc: Json.Document, _ctx: ExecutionContext): Promise<Json | undefined> {
136
+ // Only process package.json files
137
+ if (!doc.sourcePath.endsWith('package.json')) {
138
+ return doc;
139
+ }
140
+
141
+ const marker = findNodeResolutionResult(doc);
142
+ if (!marker) {
143
+ return doc;
144
+ }
145
+
146
+ // Get the project directory and package manager
147
+ const projectDir = path.dirname(path.resolve(doc.sourcePath));
148
+ const pm = marker.packageManager ?? PackageManager.Npm;
149
+
150
+ // Check if package is a direct dependency - if so, skip (use UpgradeDependencyVersion instead)
151
+ const scopes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const;
152
+ for (const scope of scopes) {
153
+ const deps = marker[scope];
154
+ if (deps?.find(d => d.name === recipe.packageName)) {
155
+ // Package is a direct dependency, don't add override
156
+ return doc;
157
+ }
158
+ }
159
+
160
+ // Check if package exists as a transitive dependency (in resolvedDependencies)
161
+ // Note: There may be multiple versions of the same package installed
162
+ const resolvedVersions = NodeResolutionResultQueries.getAllResolvedVersions(
163
+ marker,
164
+ recipe.packageName
165
+ );
166
+
167
+ if (resolvedVersions.length === 0) {
168
+ // Package not found in resolved dependencies at all
169
+ return doc;
170
+ }
171
+
172
+ // Check if ANY resolved version needs upgrading
173
+ // We need an override if at least one installed version doesn't satisfy the constraint
174
+ const anyVersionNeedsUpgrade = resolvedVersions.some(
175
+ rd => !semver.satisfies(rd.version, recipe.newVersion)
176
+ );
177
+
178
+ if (!anyVersionNeedsUpgrade) {
179
+ // All installed versions already satisfy the constraint
180
+ return doc;
181
+ }
182
+
183
+ // Parse dependency path if specified
184
+ const dependencyPathSegments = recipe.dependencyPath
185
+ ? parseDependencyPath(recipe.dependencyPath)
186
+ : undefined;
187
+
188
+ acc.projectsToUpdate.set(doc.sourcePath, {
189
+ projectDir,
190
+ packageJsonPath: doc.sourcePath,
191
+ originalPackageJson: await this.printDocument(doc),
192
+ newVersion: recipe.newVersion,
193
+ packageManager: pm,
194
+ skipInstall: false, // Always need to run install for overrides
195
+ dependencyPathSegments
196
+ });
197
+
198
+ return doc;
199
+ }
200
+
201
+ private async printDocument(doc: Json.Document): Promise<string> {
202
+ return TreePrinters.print(doc);
203
+ }
204
+ };
205
+ }
206
+
207
+ async editorWithData(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
208
+ const recipe = this;
209
+
210
+ return new class extends JsonVisitor<ExecutionContext> {
211
+ protected async visitDocument(doc: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
212
+ const sourcePath = doc.sourcePath;
213
+
214
+ // Handle package.json files
215
+ if (sourcePath.endsWith('package.json')) {
216
+ const updateInfo = acc.projectsToUpdate.get(sourcePath);
217
+ if (!updateInfo) {
218
+ return doc; // This package.json doesn't need updating
219
+ }
220
+
221
+ // Run package manager install if we haven't processed this project yet
222
+ if (!acc.processedProjects.has(sourcePath)) {
223
+ await recipe.runPackageManagerInstall(acc, updateInfo, ctx);
224
+ acc.processedProjects.add(sourcePath);
225
+ }
226
+
227
+ // Check if the install failed - if so, don't update, just add warning
228
+ const failureMessage = acc.failedProjects.get(sourcePath);
229
+ if (failureMessage) {
230
+ return markupWarn(
231
+ doc,
232
+ `Failed to add override for ${recipe.packageName} to ${recipe.newVersion}`,
233
+ failureMessage
234
+ );
235
+ }
236
+
237
+ // Add override entries
238
+ const modifiedDoc = await this.addOverrideEntry(doc, updateInfo);
239
+
240
+ // Update the NodeResolutionResult marker
241
+ return recipe.updateMarker(modifiedDoc, updateInfo, acc);
242
+ }
243
+
244
+ // Handle lock files for all package managers
245
+ for (const lockFileName of getAllLockFileNames()) {
246
+ if (sourcePath.endsWith(lockFileName)) {
247
+ // Find the corresponding package.json path
248
+ const packageJsonPath = sourcePath.replace(lockFileName, 'package.json');
249
+ const updateInfo = acc.projectsToUpdate.get(packageJsonPath);
250
+
251
+ if (updateInfo && acc.updatedLockFiles.has(sourcePath)) {
252
+ // Parse the updated lock file content and return it
253
+ const updatedContent = acc.updatedLockFiles.get(sourcePath)!;
254
+ return this.parseUpdatedLockFile(doc, updatedContent);
255
+ }
256
+ break;
257
+ }
258
+ }
259
+
260
+ return doc;
261
+ }
262
+
263
+ /**
264
+ * Adds override entry to package.json for transitive dependency upgrade.
265
+ */
266
+ private async addOverrideEntry(
267
+ doc: Json.Document,
268
+ updateInfo: ProjectUpdateInfo
269
+ ): Promise<Json.Document> {
270
+ // Parse current package.json content
271
+ const currentContent = await TreePrinters.print(doc);
272
+ let packageJson: Record<string, any>;
273
+ try {
274
+ packageJson = JSON.parse(currentContent);
275
+ } catch {
276
+ return doc; // Can't parse, return unchanged
277
+ }
278
+
279
+ // Apply override
280
+ const modifiedPackageJson = applyOverrideToPackageJson(
281
+ packageJson,
282
+ updateInfo.packageManager,
283
+ recipe.packageName,
284
+ updateInfo.newVersion,
285
+ updateInfo.dependencyPathSegments
286
+ );
287
+
288
+ // Serialize back to JSON, preserving indentation
289
+ const indentMatch = currentContent.match(/^(\s+)"/m);
290
+ const indent = indentMatch ? indentMatch[1].length : 2;
291
+ const newContent = JSON.stringify(modifiedPackageJson, null, indent);
292
+
293
+ // Re-parse with JsonParser to get proper AST
294
+ const parser = new JsonParser({});
295
+ const parsed: Json.Document[] = [];
296
+ for await (const sf of parser.parse({text: newContent, sourcePath: doc.sourcePath})) {
297
+ parsed.push(sf as Json.Document);
298
+ }
299
+
300
+ if (parsed.length > 0) {
301
+ return {
302
+ ...parsed[0],
303
+ sourcePath: doc.sourcePath,
304
+ markers: doc.markers
305
+ };
306
+ }
307
+
308
+ return doc;
309
+ }
310
+
311
+ /**
312
+ * Parses updated lock file content and creates a new document.
313
+ */
314
+ private async parseUpdatedLockFile(
315
+ originalDoc: Json.Document,
316
+ updatedContent: string
317
+ ): Promise<Json.Document> {
318
+ const parser = new JsonParser({});
319
+ const parsed: Json.Document[] = [];
320
+
321
+ for await (const sf of parser.parse({text: updatedContent, sourcePath: originalDoc.sourcePath})) {
322
+ parsed.push(sf as Json.Document);
323
+ }
324
+
325
+ if (parsed.length > 0) {
326
+ return {
327
+ ...parsed[0],
328
+ sourcePath: originalDoc.sourcePath,
329
+ markers: originalDoc.markers
330
+ };
331
+ }
332
+
333
+ return originalDoc;
334
+ }
335
+ };
336
+ }
337
+
338
+ /**
339
+ * Runs the package manager in a temporary directory to update the lock file.
340
+ */
341
+ private async runPackageManagerInstall(
342
+ acc: Accumulator,
343
+ updateInfo: ProjectUpdateInfo,
344
+ _ctx: ExecutionContext
345
+ ): Promise<void> {
346
+ // Create modified package.json with the override
347
+ const modifiedPackageJson = this.createModifiedPackageJson(
348
+ updateInfo.originalPackageJson,
349
+ updateInfo
350
+ );
351
+
352
+ const result = await runInstallInTempDir(
353
+ updateInfo.projectDir,
354
+ updateInfo.packageManager,
355
+ modifiedPackageJson
356
+ );
357
+
358
+ if (result.success) {
359
+ acc.updatedPackageJsons.set(updateInfo.packageJsonPath, modifiedPackageJson);
360
+
361
+ // Store the updated lock file content
362
+ if (result.lockFileContent) {
363
+ const lockFileName = getLockFileName(updateInfo.packageManager);
364
+ const lockFilePath = updateInfo.packageJsonPath.replace('package.json', lockFileName);
365
+ acc.updatedLockFiles.set(lockFilePath, result.lockFileContent);
366
+ }
367
+ } else {
368
+ acc.failedProjects.set(updateInfo.packageJsonPath, result.error || 'Unknown error');
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Creates a modified package.json with the override.
374
+ */
375
+ private createModifiedPackageJson(
376
+ originalContent: string,
377
+ updateInfo: ProjectUpdateInfo
378
+ ): string {
379
+ let packageJson = JSON.parse(originalContent);
380
+
381
+ packageJson = applyOverrideToPackageJson(
382
+ packageJson,
383
+ updateInfo.packageManager,
384
+ this.packageName,
385
+ updateInfo.newVersion,
386
+ updateInfo.dependencyPathSegments
387
+ );
388
+
389
+ return JSON.stringify(packageJson, null, 2);
390
+ }
391
+
392
+ /**
393
+ * Updates the NodeResolutionResult marker with new dependency information.
394
+ */
395
+ private async updateMarker(
396
+ doc: Json.Document,
397
+ updateInfo: ProjectUpdateInfo,
398
+ acc: Accumulator
399
+ ): Promise<Json.Document> {
400
+ const existingMarker = findNodeResolutionResult(doc);
401
+ if (!existingMarker) {
402
+ return doc;
403
+ }
404
+
405
+ // Parse the updated package.json and lock file to create new marker
406
+ const updatedPackageJson = acc.updatedPackageJsons.get(updateInfo.packageJsonPath);
407
+ const lockFileName = getLockFileName(updateInfo.packageManager);
408
+ const updatedLockFile = acc.updatedLockFiles.get(
409
+ updateInfo.packageJsonPath.replace('package.json', lockFileName)
410
+ );
411
+
412
+ let packageJsonContent: PackageJsonContent;
413
+ let lockContent: PackageLockContent | undefined;
414
+
415
+ try {
416
+ packageJsonContent = JSON.parse(updatedPackageJson || updateInfo.originalPackageJson);
417
+ } catch {
418
+ return doc;
419
+ }
420
+
421
+ if (updatedLockFile) {
422
+ try {
423
+ lockContent = JSON.parse(updatedLockFile);
424
+ } catch {
425
+ // Continue without lock file content
426
+ }
427
+ }
428
+
429
+ const npmrcConfigs = await readNpmrcConfigs(updateInfo.projectDir);
430
+
431
+ const newMarker = createNodeResolutionResultMarker(
432
+ existingMarker.path,
433
+ packageJsonContent,
434
+ lockContent,
435
+ existingMarker.workspacePackagePaths,
436
+ existingMarker.packageManager,
437
+ npmrcConfigs.length > 0 ? npmrcConfigs : undefined
438
+ );
439
+
440
+ return {
441
+ ...doc,
442
+ markers: replaceMarkerByKind(doc.markers, newMarker)
443
+ };
444
+ }
445
+ }