@openrewrite/rewrite 8.70.2 → 8.70.4

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 (60) hide show
  1. package/dist/javascript/add-import.d.ts +5 -0
  2. package/dist/javascript/add-import.d.ts.map +1 -1
  3. package/dist/javascript/add-import.js +22 -9
  4. package/dist/javascript/add-import.js.map +1 -1
  5. package/dist/javascript/assertions.d.ts.map +1 -1
  6. package/dist/javascript/assertions.js +45 -13
  7. package/dist/javascript/assertions.js.map +1 -1
  8. package/dist/javascript/dependency-workspace.d.ts +5 -0
  9. package/dist/javascript/dependency-workspace.d.ts.map +1 -1
  10. package/dist/javascript/dependency-workspace.js +47 -13
  11. package/dist/javascript/dependency-workspace.js.map +1 -1
  12. package/dist/javascript/package-json-parser.d.ts +24 -0
  13. package/dist/javascript/package-json-parser.d.ts.map +1 -1
  14. package/dist/javascript/package-json-parser.js +147 -34
  15. package/dist/javascript/package-json-parser.js.map +1 -1
  16. package/dist/javascript/package-manager.d.ts +45 -7
  17. package/dist/javascript/package-manager.d.ts.map +1 -1
  18. package/dist/javascript/package-manager.js +83 -45
  19. package/dist/javascript/package-manager.js.map +1 -1
  20. package/dist/javascript/project-parser.d.ts +7 -0
  21. package/dist/javascript/project-parser.d.ts.map +1 -1
  22. package/dist/javascript/project-parser.js +10 -9
  23. package/dist/javascript/project-parser.js.map +1 -1
  24. package/dist/javascript/recipes/add-dependency.d.ts +7 -3
  25. package/dist/javascript/recipes/add-dependency.d.ts.map +1 -1
  26. package/dist/javascript/recipes/add-dependency.js +71 -13
  27. package/dist/javascript/recipes/add-dependency.js.map +1 -1
  28. package/dist/javascript/recipes/upgrade-dependency-version.d.ts +7 -3
  29. package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -1
  30. package/dist/javascript/recipes/upgrade-dependency-version.js +71 -13
  31. package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -1
  32. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts +7 -3
  33. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts.map +1 -1
  34. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js +71 -13
  35. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js.map +1 -1
  36. package/dist/path-utils.d.ts +45 -0
  37. package/dist/path-utils.d.ts.map +1 -0
  38. package/dist/path-utils.js +210 -0
  39. package/dist/path-utils.js.map +1 -0
  40. package/dist/rpc/request/parse-project.d.ts +13 -1
  41. package/dist/rpc/request/parse-project.d.ts.map +1 -1
  42. package/dist/rpc/request/parse-project.js +18 -9
  43. package/dist/rpc/request/parse-project.js.map +1 -1
  44. package/dist/run.d.ts.map +1 -1
  45. package/dist/run.js +4 -0
  46. package/dist/run.js.map +1 -1
  47. package/dist/version.txt +1 -1
  48. package/package.json +1 -1
  49. package/src/javascript/add-import.ts +28 -7
  50. package/src/javascript/assertions.ts +48 -6
  51. package/src/javascript/dependency-workspace.ts +66 -13
  52. package/src/javascript/package-json-parser.ts +181 -42
  53. package/src/javascript/package-manager.ts +120 -52
  54. package/src/javascript/project-parser.ts +18 -9
  55. package/src/javascript/recipes/add-dependency.ts +89 -17
  56. package/src/javascript/recipes/upgrade-dependency-version.ts +89 -17
  57. package/src/javascript/recipes/upgrade-transitive-dependency-version.ts +89 -17
  58. package/src/path-utils.ts +208 -0
  59. package/src/rpc/request/parse-project.ts +17 -9
  60. package/src/run.ts +4 -0
@@ -49,6 +49,11 @@ interface PackageJsonWorkspaceOptions extends BaseWorkspaceOptions {
49
49
  * - `npm ci` is used instead of `npm install` (faster, deterministic)
50
50
  */
51
51
  packageLockContent?: string;
52
+ /**
53
+ * Optional workspace member package.json files.
54
+ * Keys are relative paths (e.g., "packages/foo/package.json"), values are content.
55
+ */
56
+ workspacePackages?: Record<string, string>;
52
57
  }
53
58
 
54
59
  /**
@@ -77,20 +82,44 @@ export class DependencyWorkspace {
77
82
  // Extract dependencies from package.json content if provided
78
83
  let dependencies: Record<string, string> | undefined = options.dependencies;
79
84
  let parsedPackageJson: Record<string, any> | undefined;
85
+ let workspacePackages: Record<string, string> | undefined;
86
+
80
87
  if (options.packageJsonContent) {
81
88
  parsedPackageJson = JSON.parse(options.packageJsonContent);
82
89
  dependencies = {
83
90
  ...parsedPackageJson?.dependencies,
84
91
  ...parsedPackageJson?.devDependencies
85
92
  };
93
+ workspacePackages = options.workspacePackages;
94
+
95
+ // For workspaces, also collect dependencies from workspace members
96
+ if (workspacePackages) {
97
+ for (const content of Object.values(workspacePackages)) {
98
+ const memberPkg = JSON.parse(content);
99
+ dependencies = {
100
+ ...dependencies,
101
+ ...memberPkg?.dependencies,
102
+ ...memberPkg?.devDependencies
103
+ };
104
+ }
105
+ }
86
106
  }
87
107
 
88
- if (!dependencies || Object.keys(dependencies).length === 0) {
108
+ // For workspaces without explicit dependencies in root, we still need to run install
109
+ const hasWorkspaces = parsedPackageJson?.workspaces && Array.isArray(parsedPackageJson.workspaces);
110
+ if ((!dependencies || Object.keys(dependencies).length === 0) && !hasWorkspaces) {
89
111
  throw new Error('No dependencies provided');
90
112
  }
91
113
 
92
114
  // Use the refactored internal method
93
- return this.createWorkspace(dependencies, parsedPackageJson, options.packageJsonContent, options.packageLockContent, options.targetDir);
115
+ return this.createWorkspace(
116
+ dependencies || {},
117
+ parsedPackageJson,
118
+ options.packageJsonContent,
119
+ options.packageLockContent,
120
+ options.targetDir,
121
+ workspacePackages
122
+ );
94
123
  }
95
124
 
96
125
  /**
@@ -101,14 +130,23 @@ export class DependencyWorkspace {
101
130
  parsedPackageJson: Record<string, any> | undefined,
102
131
  packageJsonContent: string | undefined,
103
132
  packageLockContent: string | undefined,
104
- targetDir: string | undefined
133
+ targetDir: string | undefined,
134
+ workspacePackages?: Record<string, string>
105
135
  ): Promise<string> {
106
136
  // Determine hash based on lock file (most precise) or dependencies
107
137
  // Note: We always hash dependencies (not packageJsonContent) because whitespace/formatting
108
138
  // differences in package.json shouldn't create different workspaces
109
- const hash = packageLockContent
110
- ? this.hashContent(packageLockContent)
111
- : this.hashDependencies(dependencies);
139
+ // For workspaces, include workspace package paths in the hash
140
+ let hash: string;
141
+ if (packageLockContent) {
142
+ hash = this.hashContent(packageLockContent);
143
+ } else if (workspacePackages) {
144
+ // Include workspace package paths in hash for workspace setups
145
+ const workspacePaths = Object.keys(workspacePackages).sort().join(',');
146
+ hash = this.hashContent(this.hashDependencies(dependencies) + ':' + workspacePaths);
147
+ } else {
148
+ hash = this.hashDependencies(dependencies);
149
+ }
112
150
 
113
151
  // Determine npm command: use `npm ci` when lock file is provided (faster, deterministic)
114
152
  const npmCommand = packageLockContent ? 'npm ci --silent' : 'npm install --silent';
@@ -134,11 +172,26 @@ export class DependencyWorkspace {
134
172
  if (packageLockContent) {
135
173
  fs.writeFileSync(path.join(dir, 'package-lock.json'), packageLockContent);
136
174
  }
175
+
176
+ // Write workspace member package.json files
177
+ if (workspacePackages) {
178
+ for (const [relativePath, content] of Object.entries(workspacePackages)) {
179
+ const fullPath = path.join(dir, relativePath);
180
+ const memberDir = path.dirname(fullPath);
181
+ if (!fs.existsSync(memberDir)) {
182
+ fs.mkdirSync(memberDir, {recursive: true});
183
+ }
184
+ fs.writeFileSync(fullPath, content);
185
+ }
186
+ }
137
187
  };
138
188
 
189
+ // For workspaces, skip dependency validation (combined deps don't match root package.json)
190
+ const depsForValidation = workspacePackages ? undefined : dependencies;
191
+
139
192
  if (targetDir) {
140
193
  // Use provided directory - check if it's already valid
141
- if (this.isWorkspaceValid(targetDir, dependencies)) {
194
+ if (this.isWorkspaceValid(targetDir, depsForValidation)) {
142
195
  return targetDir;
143
196
  }
144
197
 
@@ -149,7 +202,7 @@ export class DependencyWorkspace {
149
202
  const cachedWorkspaceDir = path.join(this.WORKSPACE_BASE, hash);
150
203
  const cachedNodeModules = path.join(cachedWorkspaceDir, 'node_modules');
151
204
 
152
- if (fs.existsSync(cachedNodeModules) && this.isWorkspaceValid(cachedWorkspaceDir, dependencies)) {
205
+ if (fs.existsSync(cachedNodeModules) && this.isWorkspaceValid(cachedWorkspaceDir, depsForValidation)) {
153
206
  // Symlink node_modules from cached workspace
154
207
  try {
155
208
  const targetNodeModules = path.join(targetDir, 'node_modules');
@@ -190,7 +243,7 @@ export class DependencyWorkspace {
190
243
 
191
244
  // Check cache
192
245
  const cached = this.cache.get(hash);
193
- if (cached && fs.existsSync(cached) && this.isWorkspaceValid(cached, dependencies)) {
246
+ if (cached && fs.existsSync(cached) && this.isWorkspaceValid(cached, depsForValidation)) {
194
247
  return cached;
195
248
  }
196
249
 
@@ -198,7 +251,7 @@ export class DependencyWorkspace {
198
251
  const workspaceDir = path.join(this.WORKSPACE_BASE, hash);
199
252
 
200
253
  // Check if valid workspace already exists on disk (cross-VM reuse)
201
- if (fs.existsSync(workspaceDir) && this.isWorkspaceValid(workspaceDir, dependencies)) {
254
+ if (fs.existsSync(workspaceDir) && this.isWorkspaceValid(workspaceDir, depsForValidation)) {
202
255
  this.cache.set(hash, workspaceDir);
203
256
  return workspaceDir;
204
257
  }
@@ -241,7 +294,7 @@ export class DependencyWorkspace {
241
294
  if (error.code === 'EEXIST' || error.code === 'ENOTEMPTY' || error.code === 'EISDIR' ||
242
295
  (error.code === 'EPERM' && fs.existsSync(workspaceDir))) {
243
296
  // Target exists - check if it's valid
244
- if (this.isWorkspaceValid(workspaceDir, dependencies)) {
297
+ if (this.isWorkspaceValid(workspaceDir, depsForValidation)) {
245
298
  // Another process created a valid workspace - use theirs
246
299
  moved = true; // Don't try again
247
300
  } else {
@@ -261,7 +314,7 @@ export class DependencyWorkspace {
261
314
  moved = true;
262
315
  } catch (copyError) {
263
316
  // Check if another process created it while we were copying
264
- if (this.isWorkspaceValid(workspaceDir, dependencies)) {
317
+ if (this.isWorkspaceValid(workspaceDir, depsForValidation)) {
265
318
  moved = true;
266
319
  } else {
267
320
  throw error;
@@ -284,7 +337,7 @@ export class DependencyWorkspace {
284
337
  }
285
338
 
286
339
  // Verify final workspace is valid (might be from another process)
287
- if (!this.isWorkspaceValid(workspaceDir, dependencies)) {
340
+ if (!this.isWorkspaceValid(workspaceDir, depsForValidation)) {
288
341
  throw new Error('Failed to create valid workspace due to concurrent modifications');
289
342
  }
290
343
 
@@ -29,6 +29,8 @@ import * as fsp from "fs/promises";
29
29
  import * as path from "path";
30
30
  import * as YAML from "yaml";
31
31
  import {getLockFileDetectionConfig, runList} from "./package-manager";
32
+ import picomatch from "picomatch";
33
+ import {DEFAULT_DIR_EXCLUSIONS, walkDirs} from "../path-utils";
32
34
 
33
35
  /**
34
36
  * Bun.lock package entry metadata.
@@ -140,52 +142,42 @@ export class PackageJsonParser extends Parser {
140
142
  }
141
143
 
142
144
  async *parse(...inputs: ParserInput[]): AsyncGenerator<SourceFile> {
143
- // Group inputs by directory to share NodeResolutionResult markers
144
- const inputsByDir = new Map<string, ParserInput[]>();
145
+ // Cache markers by directory to share NodeResolutionResult markers
146
+ // but maintain input order for output
147
+ const markersByDir = new Map<string, NodeResolutionResult | null>();
145
148
 
149
+ // Process each input in order, caching markers per directory
146
150
  for (const input of inputs) {
147
151
  const filePath = parserInputFile(input);
148
152
  const dir = path.dirname(filePath);
149
153
 
150
- if (!inputsByDir.has(dir)) {
151
- inputsByDir.set(dir, []);
152
- }
153
- inputsByDir.get(dir)!.push(input);
154
- }
155
-
156
- // Process each directory's package.json files
157
- for (const [dir, dirInputs] of inputsByDir) {
158
- // Create a shared marker for this directory
159
- let marker: NodeResolutionResult | null = null;
160
-
161
- for (const input of dirInputs) {
162
- // Parse as JSON first
163
- const jsonGenerator = this.jsonParser.parse(input);
164
- const jsonResult = await jsonGenerator.next();
165
-
166
- if (jsonResult.done || !jsonResult.value) {
167
- continue;
168
- }
154
+ // Parse as JSON first
155
+ const jsonGenerator = this.jsonParser.parse(input);
156
+ const jsonResult = await jsonGenerator.next();
169
157
 
170
- const jsonDoc = jsonResult.value as Json.Document;
158
+ if (jsonResult.done || !jsonResult.value) {
159
+ continue;
160
+ }
171
161
 
172
- // Create NodeResolutionResult marker if not already created for this directory
173
- if (!marker) {
174
- marker = await this.createMarker(input, dir);
175
- }
162
+ const jsonDoc = jsonResult.value as Json.Document;
176
163
 
177
- // Attach the marker to the JSON document
178
- if (marker) {
179
- yield {
180
- ...jsonDoc,
181
- markers: {
182
- ...jsonDoc.markers,
183
- markers: [...jsonDoc.markers.markers, marker]
184
- }
185
- };
186
- } else {
187
- yield jsonDoc;
188
- }
164
+ // Create NodeResolutionResult marker if not already created for this directory
165
+ if (!markersByDir.has(dir)) {
166
+ markersByDir.set(dir, await this.createMarker(input, dir));
167
+ }
168
+ const marker = markersByDir.get(dir)!;
169
+
170
+ // Attach the marker to the JSON document
171
+ if (marker) {
172
+ yield {
173
+ ...jsonDoc,
174
+ markers: {
175
+ ...jsonDoc.markers,
176
+ markers: [...jsonDoc.markers.markers, marker]
177
+ }
178
+ };
179
+ } else {
180
+ yield jsonDoc;
189
181
  }
190
182
  }
191
183
  }
@@ -222,11 +214,24 @@ export class PackageJsonParser extends Parser {
222
214
  const projectDir = this.relativeTo || dir;
223
215
  const npmrcConfigs = await readNpmrcConfigs(projectDir);
224
216
 
217
+ // Detect workspace member paths if this is a workspace root
218
+ let workspacePackagePaths: string[] | undefined;
219
+ if (packageJson.workspaces) {
220
+ const absoluteDir = this.relativeTo && !path.isAbsolute(dir)
221
+ ? path.resolve(this.relativeTo, dir)
222
+ : dir;
223
+ workspacePackagePaths = await this.resolveWorkspacePackagePaths(
224
+ packageJson.workspaces,
225
+ absoluteDir,
226
+ this.relativeTo
227
+ );
228
+ }
229
+
225
230
  return createNodeResolutionResultMarker(
226
231
  relativePath,
227
232
  packageJson,
228
233
  lockContent,
229
- undefined,
234
+ workspacePackagePaths,
230
235
  packageManager,
231
236
  npmrcConfigs.length > 0 ? npmrcConfigs : undefined
232
237
  );
@@ -236,6 +241,131 @@ export class PackageJsonParser extends Parser {
236
241
  }
237
242
  }
238
243
 
244
+ /**
245
+ * Resolves workspace glob patterns to actual package.json paths.
246
+ *
247
+ * Workspaces can be specified as:
248
+ * - Array of globs: ["packages/*", "apps/*", "packages/**", "{apps,libs}/*"]
249
+ * - Object with packages array: { packages: ["packages/*"] }
250
+ * - Negation patterns: ["packages/*", "!packages/internal"]
251
+ *
252
+ * @param workspaces The workspaces field from package.json
253
+ * @param projectDir The absolute path to the project directory
254
+ * @param relativeTo Optional base path for creating relative paths
255
+ * @returns Array of relative paths to workspace member package.json files
256
+ */
257
+ private async resolveWorkspacePackagePaths(
258
+ workspaces: string[] | { packages?: string[] },
259
+ projectDir: string,
260
+ relativeTo?: string
261
+ ): Promise<string[] | undefined> {
262
+ // Normalize workspaces to array format
263
+ const patterns = Array.isArray(workspaces)
264
+ ? workspaces
265
+ : workspaces.packages;
266
+
267
+ if (!patterns || patterns.length === 0) {
268
+ return undefined;
269
+ }
270
+
271
+ // Separate include and exclude patterns
272
+ const includePatterns = patterns.filter(p => !p.startsWith('!'));
273
+ const excludePatterns = patterns.filter(p => p.startsWith('!')).map(p => p.slice(1));
274
+
275
+ // Create picomatch matchers
276
+ const isIncluded = includePatterns.length > 0
277
+ ? picomatch(includePatterns, { dot: false })
278
+ : () => false;
279
+ const isExcluded = excludePatterns.length > 0
280
+ ? picomatch(excludePatterns, { dot: false })
281
+ : () => false;
282
+
283
+ // Collect all candidate directories by walking the project
284
+ const candidateDirs = await this.collectCandidateWorkspaceDirs(projectDir, includePatterns);
285
+
286
+ const memberPaths: string[] = [];
287
+ const basePath = relativeTo || projectDir;
288
+
289
+ for (const candidateDir of candidateDirs) {
290
+ // Get relative path from project root for pattern matching
291
+ const relativeDir = path.relative(projectDir, candidateDir);
292
+
293
+ // Check if directory matches include patterns and not exclude patterns
294
+ if (isIncluded(relativeDir) && !isExcluded(relativeDir)) {
295
+ const packageJsonPath = path.join(candidateDir, 'package.json');
296
+ if (fs.existsSync(packageJsonPath)) {
297
+ const relativePath = path.relative(basePath, packageJsonPath);
298
+ memberPaths.push(relativePath);
299
+ }
300
+ }
301
+ }
302
+
303
+ return memberPaths.length > 0 ? memberPaths : undefined;
304
+ }
305
+
306
+ /**
307
+ * Collects candidate directories that might match workspace patterns.
308
+ * Uses the patterns to determine how deep to scan.
309
+ */
310
+ private async collectCandidateWorkspaceDirs(
311
+ projectDir: string,
312
+ patterns: string[]
313
+ ): Promise<string[]> {
314
+ const candidates: string[] = [];
315
+
316
+ // Determine the maximum depth we need to scan based on patterns
317
+ // "packages/*" -> depth 1 under packages/
318
+ // "packages/**" -> unlimited depth under packages/
319
+ // "{apps,libs}/*" -> depth 1 under apps/ and libs/
320
+
321
+ for (const pattern of patterns) {
322
+ // Extract base directories from pattern (before any wildcards)
323
+ const baseDirs = this.extractBaseDirs(pattern);
324
+ const hasRecursive = pattern.includes('**');
325
+
326
+ for (const baseDir of baseDirs) {
327
+ const absoluteBaseDir = path.join(projectDir, baseDir);
328
+
329
+ if (!fs.existsSync(absoluteBaseDir)) {
330
+ continue;
331
+ }
332
+
333
+ // Use walkDirs with appropriate depth limit
334
+ const dirs = await walkDirs(absoluteBaseDir, {
335
+ maxDepth: hasRecursive ? undefined : 0,
336
+ excludeDirs: DEFAULT_DIR_EXCLUSIONS
337
+ });
338
+
339
+ candidates.push(...dirs);
340
+ }
341
+ }
342
+
343
+ return candidates;
344
+ }
345
+
346
+ /**
347
+ * Extracts base directory paths from a glob pattern.
348
+ * Handles brace expansion like "{apps,libs}/*" -> ["apps", "libs"]
349
+ */
350
+ private extractBaseDirs(pattern: string): string[] {
351
+ // Find the first wildcard character
352
+ const wildcardIndex = pattern.search(/[*?]/);
353
+ const beforeWildcard = wildcardIndex >= 0 ? pattern.slice(0, wildcardIndex) : pattern;
354
+
355
+ // Remove trailing slash if present
356
+ const basePath = beforeWildcard.replace(/\/$/, '');
357
+
358
+ // Handle brace expansion at the end of basePath: "dir/{a,b}" or "{a,b}"
359
+ const braceMatch = basePath.match(/^(.*?)(?:\{([^}]+)\})?$/);
360
+ if (braceMatch && braceMatch[2]) {
361
+ const prefix = braceMatch[1];
362
+ const options = braceMatch[2].split(',').map(s => s.trim());
363
+ return options.map(opt => prefix + opt);
364
+ }
365
+
366
+ return basePath ? [basePath] : ['.'];
367
+ }
368
+
239
369
  /**
240
370
  * Attempts to find and read a lock file by walking up the directory tree.
241
371
  * Starts from the directory containing the package.json and walks up toward
@@ -412,12 +542,21 @@ export class PackageJsonParser extends Parser {
412
542
  }
413
543
 
414
544
  // Parse name@version from directory name
545
+ // pnpm directory format: <name>@<version> or <name>@<version>_<peer-deps-context>
415
546
  // Handle scoped packages: @scope+name@version
416
- const atIndex = entry.name.lastIndexOf('@');
547
+ // Example: @babel+helper-module-transforms@7.28.3_@babel+core@7.28.5
548
+ // -> name: @babel/helper-module-transforms, version: 7.28.3
549
+
550
+ // First, strip peer dependency context (everything after first _)
551
+ const underscoreIndex = entry.name.indexOf('_');
552
+ const mainPart = underscoreIndex > 0 ? entry.name.substring(0, underscoreIndex) : entry.name;
553
+
554
+ // Now parse name@version from the main part
555
+ const atIndex = mainPart.lastIndexOf('@');
417
556
  if (atIndex <= 0) return;
418
557
 
419
- let name = entry.name.substring(0, atIndex);
420
- const version = entry.name.substring(atIndex + 1);
558
+ let name = mainPart.substring(0, atIndex);
559
+ const version = mainPart.substring(atIndex + 1);
421
560
 
422
561
  // pnpm encodes @ as + in scoped packages: @scope+name -> @scope/name
423
562
  if (name.startsWith('@') && name.includes('+')) {