@openrewrite/rewrite 8.77.0 → 8.77.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.
@@ -30,6 +30,7 @@ import {
30
30
  } from "../node-resolution-result";
31
31
  import * as path from "path";
32
32
  import * as semver from "semver";
33
+ import * as picomatch from "picomatch";
33
34
  import {markupWarn, replaceMarkerByKind} from "../../markers";
34
35
  import {TreePrinters} from "../../print";
35
36
  import {
@@ -45,28 +46,19 @@ import {
45
46
  updateNodeResolutionMarker
46
47
  } from "../package-manager";
47
48
 
48
- /**
49
- * Information about a project that needs updating
50
- */
49
+ interface MatchedDependency {
50
+ packageName: string;
51
+ dependencyScope: DependencyScope;
52
+ currentVersion: string;
53
+ }
54
+
51
55
  interface ProjectUpdateInfo {
52
- /** Relative path to package.json (from source root) */
53
56
  packageJsonPath: string;
54
- /** Original package.json content */
55
57
  originalPackageJson: string;
56
- /** The scope where the dependency was found */
57
- dependencyScope: DependencyScope;
58
- /** Current version constraint */
59
- currentVersion: string;
60
- /** New version constraint to apply */
58
+ matchedDependencies: MatchedDependency[];
61
59
  newVersion: string;
62
- /** The package manager used by this project */
63
60
  packageManager: PackageManager;
64
- /**
65
- * If true, skip running the package manager because the resolved version
66
- * already satisfies the new constraint. Only package.json needs updating.
67
- */
68
61
  skipInstall: boolean;
69
- /** Config file contents extracted from the project (e.g., .npmrc) */
70
62
  configFiles?: Record<string, string>;
71
63
  }
72
64
 
@@ -92,14 +84,23 @@ interface Accumulator extends DependencyRecipeAccumulator<ProjectUpdateInfo> {
92
84
  export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
93
85
  readonly name = "org.openrewrite.javascript.dependencies.upgrade-dependency-version";
94
86
  readonly displayName = "Upgrade npm dependency version";
95
- readonly description = "Upgrades the version of a direct dependency in `package.json` and updates the lock file by running the package manager.";
87
+ readonly description = "Upgrades the version of a direct dependency in `package.json` and updates the lock file by running the package manager. Either `packageName` or `packagePattern` must be specified.";
96
88
 
97
89
  @Option({
98
90
  displayName: "Package name",
99
- description: "The name of the npm package to upgrade (e.g., `lodash`, `@types/node`)",
91
+ description: "The exact name of the npm package to upgrade (e.g., `lodash`, `@types/node`). Either this or `packagePattern` must be specified.",
92
+ required: false,
100
93
  example: "lodash"
101
94
  })
102
- packageName!: string;
95
+ packageName?: string;
96
+
97
+ @Option({
98
+ displayName: "Package pattern",
99
+ description: "A glob expression to match package names (e.g., `@angular/*`, `@types/*`). Either this or `packageName` must be specified.",
100
+ required: false,
101
+ example: "@angular/*"
102
+ })
103
+ packagePattern?: string;
103
104
 
104
105
  @Option({
105
106
  displayName: "Version",
@@ -108,7 +109,31 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
108
109
  })
109
110
  newVersion!: string;
110
111
 
112
+ private _matcher?: picomatch.Matcher;
113
+
114
+ private get matcher(): picomatch.Matcher | undefined {
115
+ if (this.packagePattern && !this._matcher) {
116
+ this._matcher = picomatch.default
117
+ ? picomatch.default(this.packagePattern)
118
+ : (picomatch as any)(this.packagePattern);
119
+ }
120
+ return this._matcher;
121
+ }
122
+
123
+ matchesPackage(name: string): boolean {
124
+ if (this.packageName && name === this.packageName) {
125
+ return true;
126
+ }
127
+ if (this.matcher) {
128
+ return this.matcher(name);
129
+ }
130
+ return false;
131
+ }
132
+
111
133
  initialValue(_ctx: ExecutionContext): Accumulator {
134
+ if (!this.packageName && !this.packagePattern) {
135
+ throw new Error("Either packageName or packagePattern must be specified");
136
+ }
112
137
  return {
113
138
  ...createDependencyRecipeAccumulator<ProjectUpdateInfo>(),
114
139
  originalLockFiles: new Map()
@@ -193,40 +218,30 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
193
218
 
194
219
  const pm = marker.packageManager ?? PackageManager.Npm;
195
220
 
196
- // Check each dependency scope for the target package
197
- const scopes = allDependencyScopes;
198
- let foundScope: DependencyScope | undefined;
199
- let currentVersion: string | undefined;
200
-
201
- for (const scope of scopes) {
221
+ const matchedDeps: MatchedDependency[] = [];
222
+ for (const scope of allDependencyScopes) {
202
223
  const deps = marker[scope];
203
- const dep = deps?.find(d => d.name === recipe.packageName);
204
-
205
- if (dep) {
206
- foundScope = scope;
207
- currentVersion = dep.versionConstraint;
208
- break;
224
+ if (!deps) continue;
225
+ for (const dep of deps) {
226
+ if (recipe.matchesPackage(dep.name) && recipe.shouldUpgrade(dep.versionConstraint, recipe.newVersion)) {
227
+ matchedDeps.push({
228
+ packageName: dep.name,
229
+ dependencyScope: scope,
230
+ currentVersion: dep.versionConstraint
231
+ });
232
+ }
209
233
  }
210
234
  }
211
235
 
212
- if (!foundScope || !currentVersion) {
213
- return doc; // Dependency not found in any scope
214
- }
215
-
216
- // Check if upgrade is needed
217
- if (!recipe.shouldUpgrade(currentVersion, recipe.newVersion)) {
218
- return doc; // Already at target version or newer
236
+ if (matchedDeps.length === 0) {
237
+ return doc;
219
238
  }
220
239
 
221
- // Check if we can skip running the package manager
222
- // (resolved version already satisfies the new constraint)
223
- const resolvedDep = marker.resolvedDependencies?.find(
224
- rd => rd.name === recipe.packageName
225
- );
226
- const skipInstall = resolvedDep !== undefined &&
227
- semver.satisfies(resolvedDep.version, recipe.newVersion);
240
+ const skipInstall = matchedDeps.every(md => {
241
+ const resolvedDep = marker.resolvedDependencies?.find(rd => rd.name === md.packageName);
242
+ return resolvedDep !== undefined && semver.satisfies(resolvedDep.version, recipe.newVersion);
243
+ });
228
244
 
229
- // Serialize npmrc configs from marker using requested scopes
230
245
  const configFiles: Record<string, string> = {};
231
246
  const npmrcContent = serializeNpmrcConfigs(marker.npmrcConfigs);
232
247
  if (npmrcContent) {
@@ -236,8 +251,7 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
236
251
  acc.projectsToUpdate.set(doc.sourcePath, {
237
252
  packageJsonPath: doc.sourcePath,
238
253
  originalPackageJson: await TreePrinters.print(doc),
239
- dependencyScope: foundScope,
240
- currentVersion,
254
+ matchedDependencies: matchedDeps,
241
255
  newVersion: recipe.newVersion,
242
256
  packageManager: pm,
243
257
  skipInstall,
@@ -280,32 +294,31 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
280
294
  return doc; // This package.json doesn't need updating
281
295
  }
282
296
 
283
- // Run package manager install if needed, check for failure
284
- // Skip if the resolved version already satisfies the new constraint
285
297
  const failureMessage = updateInfo.skipInstall
286
298
  ? undefined
287
299
  : await runInstallIfNeeded(sourcePath, acc, () =>
288
300
  recipe.runPackageManagerInstall(acc, updateInfo, ctx)
289
301
  );
290
302
  if (failureMessage) {
303
+ const names = updateInfo.matchedDependencies.map(d => d.packageName).join(', ');
291
304
  return markupWarn(
292
305
  doc,
293
- `Failed to upgrade ${recipe.packageName} to ${recipe.newVersion}`,
306
+ `Failed to upgrade ${names} to ${recipe.newVersion}`,
294
307
  failureMessage
295
308
  );
296
309
  }
297
310
 
298
- // Update the dependency version in the JSON AST (preserves formatting)
299
- const visitor = new UpdateVersionVisitor(
300
- recipe.packageName,
301
- updateInfo.newVersion,
302
- updateInfo.dependencyScope
303
- );
304
- const modifiedDoc = await visitor.visit(doc, undefined) as Json.Document;
311
+ let modifiedDoc = doc;
312
+ for (const md of updateInfo.matchedDependencies) {
313
+ const visitor = new UpdateVersionVisitor(
314
+ md.packageName,
315
+ updateInfo.newVersion,
316
+ md.dependencyScope
317
+ );
318
+ modifiedDoc = await visitor.visit(modifiedDoc, undefined) as Json.Document;
319
+ }
305
320
 
306
- // Update the NodeResolutionResult marker
307
321
  if (updateInfo.skipInstall) {
308
- // Just update the versionConstraint in the marker - resolved version is unchanged
309
322
  return recipe.updateMarkerVersionConstraint(modifiedDoc, updateInfo);
310
323
  }
311
324
  return updateNodeResolutionMarker(modifiedDoc, updateInfo, acc);
@@ -344,10 +357,9 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
344
357
  updateInfo: ProjectUpdateInfo,
345
358
  _ctx: ExecutionContext
346
359
  ): Promise<void> {
347
- // Create modified package.json with the new version constraint
348
360
  const modifiedPackageJson = this.createModifiedPackageJson(
349
361
  updateInfo.originalPackageJson,
350
- updateInfo.dependencyScope,
362
+ updateInfo.matchedDependencies,
351
363
  updateInfo.newVersion
352
364
  );
353
365
 
@@ -373,28 +385,22 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
373
385
  storeInstallResult(result, acc, updateInfo, modifiedPackageJson);
374
386
  }
375
387
 
376
- /**
377
- * Creates a modified package.json with the updated dependency version.
378
- * Used for the temp directory to validate the version exists.
379
- */
380
388
  private createModifiedPackageJson(
381
389
  originalContent: string,
382
- scope: DependencyScope,
390
+ matchedDependencies: MatchedDependency[],
383
391
  newVersion: string
384
392
  ): string {
385
393
  const packageJson = JSON.parse(originalContent);
386
394
 
387
- if (packageJson[scope] && packageJson[scope][this.packageName]) {
388
- packageJson[scope][this.packageName] = newVersion;
395
+ for (const md of matchedDependencies) {
396
+ if (packageJson[md.dependencyScope]?.[md.packageName]) {
397
+ packageJson[md.dependencyScope][md.packageName] = newVersion;
398
+ }
389
399
  }
390
400
 
391
401
  return JSON.stringify(packageJson, null, 2);
392
402
  }
393
403
 
394
- /**
395
- * Updates just the versionConstraint in the marker for the target dependency.
396
- * Used when skipInstall is true - the resolved version is unchanged.
397
- */
398
404
  private updateMarkerVersionConstraint(
399
405
  doc: Json.Document,
400
406
  updateInfo: ProjectUpdateInfo
@@ -404,18 +410,19 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
404
410
  return doc;
405
411
  }
406
412
 
407
- // Update the versionConstraint for the target dependency
408
- const deps = existingMarker[updateInfo.dependencyScope];
409
- const updatedDeps = deps?.map(dep =>
410
- dep.name === this.packageName
411
- ? {...dep, versionConstraint: updateInfo.newVersion}
412
- : dep
413
- );
414
-
415
- const newMarker = {
416
- ...existingMarker,
417
- [updateInfo.dependencyScope]: updatedDeps
418
- };
413
+ const matchedNames = new Set(updateInfo.matchedDependencies.map(md => md.packageName));
414
+ let newMarker = {...existingMarker};
415
+ for (const md of updateInfo.matchedDependencies) {
416
+ const deps = newMarker[md.dependencyScope];
417
+ newMarker = {
418
+ ...newMarker,
419
+ [md.dependencyScope]: deps?.map(dep =>
420
+ matchedNames.has(dep.name)
421
+ ? {...dep, versionConstraint: updateInfo.newVersion}
422
+ : dep
423
+ )
424
+ };
425
+ }
419
426
 
420
427
  return {
421
428
  ...doc,
@@ -14,6 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import ts from "typescript";
17
+ import * as path from "path";
17
18
  import {Type} from "../java";
18
19
  import FUNCTION_TYPE_NAME = Type.FUNCTION_TYPE_NAME;
19
20
 
@@ -30,7 +31,8 @@ export class JavaScriptTypeMapping {
30
31
 
31
32
 
32
33
  constructor(
33
- private readonly checker: ts.TypeChecker
34
+ private readonly checker: ts.TypeChecker,
35
+ private readonly sourceRoot?: string
34
36
  ) {
35
37
  this.regExpSymbol = checker.resolveName(
36
38
  "RegExp",
@@ -69,11 +71,14 @@ export class JavaScriptTypeMapping {
69
71
  // Fall through to regular type checking if not a variable
70
72
  }
71
73
 
74
+ // TypeNode needs getTypeFromTypeNode (resolves the annotation itself).
75
+ // Everything else — expressions, declarations, specifiers, bindings, etc. —
76
+ // uses getTypeAtLocation which works for virtually all node kinds.
72
77
  let type: ts.Type | undefined;
73
- if (ts.isExpression(node)) {
74
- type = this.checker.getTypeAtLocation(node);
75
- } else if (ts.isTypeNode(node)) {
78
+ if (ts.isTypeNode(node)) {
76
79
  type = this.checker.getTypeFromTypeNode(node);
80
+ } else {
81
+ type = this.checker.getTypeAtLocation(node);
77
82
  }
78
83
  return type && this.getType(type);
79
84
  }
@@ -565,7 +570,7 @@ export class JavaScriptTypeMapping {
565
570
  */
566
571
  private createMethodType(
567
572
  signature: ts.Signature,
568
- node: ts.MethodSignature | ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.CallExpression | ts.NewExpression,
573
+ node: ts.MethodSignature | ts.FunctionDeclaration | ts.MethodDeclaration | ts.ConstructorDeclaration | ts.FunctionExpression | ts.CallExpression | ts.NewExpression,
569
574
  declaringType: Type.FullyQualified,
570
575
  name: string,
571
576
  declaredFormalTypeNames: string[] = []
@@ -915,6 +920,22 @@ export class JavaScriptTypeMapping {
915
920
  declaredFormalTypeNames.push(tp.name.getText());
916
921
  }
917
922
  }
923
+ } else if (ts.isConstructorDeclaration(node)) {
924
+ // For constructor declarations
925
+ signature = this.checker.getSignatureFromDeclaration(node);
926
+ if (!signature) {
927
+ return undefined;
928
+ }
929
+
930
+ methodName = "<constructor>";
931
+
932
+ const parent = node.parent;
933
+ if (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) {
934
+ const parentType = this.checker.getTypeAtLocation(parent);
935
+ declaringType = this.getType(parentType) as Type.FullyQualified;
936
+ } else {
937
+ declaringType = Type.unknownType as Type.FullyQualified;
938
+ }
918
939
  } else if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)) {
919
940
  // For function declarations/expressions
920
941
  signature = this.checker.getSignatureFromDeclaration(node);
@@ -1032,6 +1053,14 @@ export class JavaScriptTypeMapping {
1032
1053
  cleanedName = packageName;
1033
1054
  }
1034
1055
  }
1056
+ } else if (path.isAbsolute(cleanedName)) {
1057
+ // TypeScript returns absolute file paths as module names for project source files
1058
+ // that are ES modules (have imports/exports). Relativize using sourceRoot.
1059
+ // Example: /var/moderne/.../BookStack/resources/js/foo.MyClass
1060
+ // Should become: resources/js/foo.MyClass
1061
+ if (this.sourceRoot) {
1062
+ cleanedName = path.relative(this.sourceRoot, cleanedName);
1063
+ }
1035
1064
  }
1036
1065
 
1037
1066
  return cleanedName.endsWith('Constructor') ?