@openrewrite/rewrite 8.69.0-20251210-214835 → 8.69.0-20251211-110844

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 (70) hide show
  1. package/dist/cli/cli-utils.d.ts.map +1 -1
  2. package/dist/cli/cli-utils.js +6 -0
  3. package/dist/cli/cli-utils.js.map +1 -1
  4. package/dist/cli/rewrite.d.ts.map +1 -1
  5. package/dist/cli/rewrite.js +55 -23
  6. package/dist/cli/rewrite.js.map +1 -1
  7. package/dist/data-table.d.ts +23 -0
  8. package/dist/data-table.d.ts.map +1 -1
  9. package/dist/data-table.js +125 -6
  10. package/dist/data-table.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/javascript/node-resolution-result.d.ts +9 -0
  15. package/dist/javascript/node-resolution-result.d.ts.map +1 -1
  16. package/dist/javascript/node-resolution-result.js +10 -1
  17. package/dist/javascript/node-resolution-result.js.map +1 -1
  18. package/dist/javascript/package-manager.d.ts +76 -89
  19. package/dist/javascript/package-manager.d.ts.map +1 -1
  20. package/dist/javascript/package-manager.js +114 -139
  21. package/dist/javascript/package-manager.js.map +1 -1
  22. package/dist/javascript/recipes/add-dependency.d.ts +57 -0
  23. package/dist/javascript/recipes/add-dependency.d.ts.map +1 -0
  24. package/dist/javascript/recipes/add-dependency.js +404 -0
  25. package/dist/javascript/recipes/add-dependency.js.map +1 -0
  26. package/dist/javascript/recipes/index.d.ts +1 -0
  27. package/dist/javascript/recipes/index.d.ts.map +1 -1
  28. package/dist/javascript/recipes/index.js +1 -0
  29. package/dist/javascript/recipes/index.js.map +1 -1
  30. package/dist/javascript/recipes/upgrade-dependency-version.d.ts +3 -24
  31. package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -1
  32. package/dist/javascript/recipes/upgrade-dependency-version.js +34 -157
  33. package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -1
  34. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts +2 -19
  35. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts.map +1 -1
  36. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js +21 -137
  37. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js.map +1 -1
  38. package/dist/javascript/search/find-dependency.d.ts.map +1 -1
  39. package/dist/javascript/search/find-dependency.js +8 -47
  40. package/dist/javascript/search/find-dependency.js.map +1 -1
  41. package/dist/json/tree.d.ts +30 -0
  42. package/dist/json/tree.d.ts.map +1 -1
  43. package/dist/json/tree.js +113 -0
  44. package/dist/json/tree.js.map +1 -1
  45. package/dist/parser.d.ts +9 -0
  46. package/dist/parser.d.ts.map +1 -1
  47. package/dist/parser.js +27 -0
  48. package/dist/parser.js.map +1 -1
  49. package/dist/reference.d.ts.map +1 -1
  50. package/dist/reference.js +1 -1
  51. package/dist/reference.js.map +1 -1
  52. package/dist/version.txt +1 -1
  53. package/dist/visitor.js +1 -1
  54. package/dist/visitor.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/cli/cli-utils.ts +6 -0
  57. package/src/cli/rewrite.ts +53 -17
  58. package/src/data-table.ts +83 -2
  59. package/src/index.ts +2 -1
  60. package/src/javascript/node-resolution-result.ts +16 -0
  61. package/src/javascript/package-manager.ts +197 -174
  62. package/src/javascript/recipes/add-dependency.ts +467 -0
  63. package/src/javascript/recipes/index.ts +1 -0
  64. package/src/javascript/recipes/upgrade-dependency-version.ts +52 -199
  65. package/src/javascript/recipes/upgrade-transitive-dependency-version.ts +39 -165
  66. package/src/javascript/search/find-dependency.ts +13 -52
  67. package/src/json/tree.ts +98 -1
  68. package/src/parser.ts +17 -0
  69. package/src/reference.ts +1 -1
  70. package/src/visitor.ts +1 -1
@@ -21,6 +21,7 @@ import * as path from 'path';
21
21
  import {spawn} from 'child_process';
22
22
  import {Recipe, RecipeRegistry} from '../recipe';
23
23
  import {ExecutionContext} from '../execution';
24
+ import {CsvDataTableStore, DATA_TABLE_STORE} from '../data-table';
24
25
  import {ProgressCallback, scheduleRunStreaming} from '../run';
25
26
  import {TreePrinters} from '../print';
26
27
  import {activate} from '../index';
@@ -145,6 +146,8 @@ interface CliOptions {
145
146
  option: string[];
146
147
  verbose: boolean;
147
148
  debug: boolean;
149
+ dataTables?: string;
150
+ dataTablesOnly: boolean;
148
151
  }
149
152
 
150
153
  /**
@@ -184,6 +187,8 @@ async function main() {
184
187
  .option('-o, --option <option...>', 'Recipe options in format "key=value"', [])
185
188
  .option('-v, --verbose', 'Enable verbose output', false)
186
189
  .option('--debug', 'Start with Node.js debugger (--inspect-brk)', false)
190
+ .option('--data-tables <dir>', 'Export data tables as CSV files to this directory')
191
+ .option('--data-tables-only', 'Only output data tables, skip source file diffs', false)
187
192
  .parse();
188
193
 
189
194
  const recipeArg = program.args[0];
@@ -377,8 +382,18 @@ async function main() {
377
382
  // Run the recipe with streaming output - for non-scanning recipes,
378
383
  // parsing and processing happen concurrently without collecting all files first
379
384
  const ctx = new ExecutionContext();
385
+
386
+ // Set up data table store if requested
387
+ let csvDataTableStore: CsvDataTableStore | undefined;
388
+ if (opts.dataTables) {
389
+ csvDataTableStore = new CsvDataTableStore(opts.dataTables);
390
+ csvDataTableStore.acceptRows(true);
391
+ ctx.messages[DATA_TABLE_STORE] = csvDataTableStore;
392
+ }
393
+
380
394
  let changeCount = 0;
381
395
  let processedCount = 0;
396
+ const skipSourceOutput = opts.dataTablesOnly;
382
397
 
383
398
  // Progress callback for spinner updates during all phases (disabled in verbose mode)
384
399
  const onProgress: ProgressCallback | undefined = opts.verbose ? undefined : (phase, current, total, sourcePath) => {
@@ -414,23 +429,27 @@ async function main() {
414
429
  await fsp.writeFile(filePath, content);
415
430
 
416
431
  changeCount++;
417
- if (!opts.verbose) spinner.stop();
418
- const displayPath = toCwdRelative(result.after.sourcePath, projectRoot);
419
- if (result.before) {
420
- console.log(` Modified: ${displayPath}`);
421
- } else {
422
- console.log(` Created: ${displayPath}`);
432
+ if (!skipSourceOutput) {
433
+ if (!opts.verbose) spinner.stop();
434
+ const displayPath = toCwdRelative(result.after.sourcePath, projectRoot);
435
+ if (result.before) {
436
+ console.log(` Modified: ${displayPath}`);
437
+ } else {
438
+ console.log(` Created: ${displayPath}`);
439
+ }
440
+ if (!opts.verbose) spinner.start(statusMsg);
423
441
  }
424
- if (!opts.verbose) spinner.start(statusMsg);
425
442
  } else if (result.before) {
426
443
  const filePath = path.join(projectRoot, result.before.sourcePath);
427
444
  await fsp.unlink(filePath);
428
445
  changeCount++;
429
- if (!opts.verbose) spinner.stop();
430
- console.log(` Deleted: ${toCwdRelative(result.before.sourcePath, projectRoot)}`);
431
- if (!opts.verbose) spinner.start(statusMsg);
446
+ if (!skipSourceOutput) {
447
+ if (!opts.verbose) spinner.stop();
448
+ console.log(` Deleted: ${toCwdRelative(result.before.sourcePath, projectRoot)}`);
449
+ if (!opts.verbose) spinner.start(statusMsg);
450
+ }
432
451
  }
433
- } else {
452
+ } else if (!skipSourceOutput) {
434
453
  // Dry-run mode: show diff or just list paths
435
454
  const diff = await result.diff();
436
455
  const hasChanges = diff.split('\n').some(line =>
@@ -466,12 +485,29 @@ async function main() {
466
485
  process.exit(recipe.hasErrors ? 1 : 0);
467
486
  }
468
487
 
469
- if (changeCount === 0) {
470
- console.log('No changes to make.');
471
- } else if (opts.apply) {
472
- console.log(`\n${changeCount} file(s) changed.`);
473
- } else if (!opts.list) {
474
- console.log(`\n${changeCount} file(s) would be changed. Run with --apply to apply changes.`);
488
+ // Report on data tables if any were written
489
+ if (csvDataTableStore) {
490
+ const tableNames = csvDataTableStore.tableNames;
491
+ const rowCounts = csvDataTableStore.rowCounts;
492
+ if (tableNames.length > 0) {
493
+ console.log(`\nData tables written to ${opts.dataTables}:`);
494
+ for (const name of tableNames) {
495
+ console.log(` ${name}.csv (${rowCounts[name]} rows)`);
496
+ }
497
+ } else if (opts.dataTablesOnly) {
498
+ console.log('No data tables produced.');
499
+ }
500
+ }
501
+
502
+ // Report on source file changes (unless --data-tables-only)
503
+ if (!skipSourceOutput) {
504
+ if (changeCount === 0) {
505
+ console.log('No changes to make.');
506
+ } else if (opts.apply) {
507
+ console.log(`\n${changeCount} file(s) changed.`);
508
+ } else if (!opts.list) {
509
+ console.log(`\n${changeCount} file(s) would be changed. Run with --apply to apply changes.`);
510
+ }
475
511
  }
476
512
  }
477
513
 
package/src/data-table.ts CHANGED
@@ -13,11 +13,13 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
16
18
  import {ExecutionContext} from "./execution";
17
19
  import {OptionDescriptor} from "./recipe";
18
20
 
19
- const DATA_TABLE_STORE = Symbol("org.openrewrite.dataTables.store");
20
- const COLUMNS_KEY = Symbol("org.openrewrite.dataTables.columns");
21
+ export const DATA_TABLE_STORE = Symbol.for("org.openrewrite.dataTables.store");
22
+ const COLUMNS_KEY = Symbol.for("org.openrewrite.dataTables.columns");
21
23
 
22
24
  export function Column(descriptor: ColumnDescriptor) {
23
25
  return function (target: any, propertyKey: string) {
@@ -103,3 +105,82 @@ export interface ColumnDescriptor {
103
105
  displayName: string,
104
106
  description: string
105
107
  }
108
+
109
+ /**
110
+ * Escape a value for CSV output following RFC 4180.
111
+ * Quotes the value if it contains commas, quotes, or newlines.
112
+ */
113
+ function escapeCsv(value: unknown): string {
114
+ if (value === null || value === undefined) {
115
+ return '""';
116
+ }
117
+ const str = String(value);
118
+ // If the value contains comma, quote, or newline, wrap in quotes and escape internal quotes
119
+ if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
120
+ return '"' + str.replace(/"/g, '""') + '"';
121
+ }
122
+ return str;
123
+ }
124
+
125
+ /**
126
+ * A DataTableStore that writes rows directly to CSV files as they are inserted.
127
+ */
128
+ export class CsvDataTableStore implements DataTableStore {
129
+ private _acceptRows = false;
130
+ private readonly _initializedTables = new Set<string>();
131
+ private readonly _rowCounts: { [dataTable: string]: number } = {};
132
+
133
+ constructor(private readonly outputDir: string) {
134
+ // Ensure output directory exists
135
+ fs.mkdirSync(outputDir, {recursive: true});
136
+ }
137
+
138
+ insertRow<Row>(dataTable: DataTable<Row>, _ctx: ExecutionContext, row: Row): void {
139
+ if (!this._acceptRows) {
140
+ return;
141
+ }
142
+
143
+ const descriptor = dataTable.descriptor;
144
+ const tableName = descriptor.name;
145
+ const csvPath = path.join(this.outputDir, tableName + '.csv');
146
+
147
+ // Write header rows on first insert for this table
148
+ if (!this._initializedTables.has(tableName)) {
149
+ this._initializedTables.add(tableName);
150
+ this._rowCounts[tableName] = 0;
151
+
152
+ const columns = descriptor.columns;
153
+ const headerRow = columns.map(col => escapeCsv(col.displayName)).join(',');
154
+ const descriptionRow = columns.map(col => escapeCsv(col.description)).join(',');
155
+
156
+ fs.writeFileSync(csvPath, headerRow + '\n' + descriptionRow + '\n');
157
+ }
158
+
159
+ // Write the data row
160
+ const columns = descriptor.columns;
161
+ const rowValues = columns.map(col => {
162
+ const value = (row as any)[col.name];
163
+ return escapeCsv(value);
164
+ });
165
+ fs.appendFileSync(csvPath, rowValues.join(',') + '\n');
166
+ this._rowCounts[tableName]++;
167
+ }
168
+
169
+ acceptRows(accept: boolean): void {
170
+ this._acceptRows = accept;
171
+ }
172
+
173
+ /**
174
+ * Get the number of rows written for each data table.
175
+ */
176
+ get rowCounts(): { [dataTable: string]: number } {
177
+ return {...this._rowCounts};
178
+ }
179
+
180
+ /**
181
+ * Get the names of all data tables that have been written to.
182
+ */
183
+ get tableNames(): string[] {
184
+ return [...this._initializedTables];
185
+ }
186
+ }
package/src/index.ts CHANGED
@@ -38,9 +38,10 @@ export async function activate(registry: RecipeRegistry): Promise<void> {
38
38
  const {ModernizeOctalEscapeSequences, ModernizeOctalLiterals, RemoveDuplicateObjectKeys} = await import("./javascript/migrate/es6/index.js");
39
39
  const {ExportAssignmentToExportDefault} = await import("./javascript/migrate/typescript/index.js");
40
40
  const {UseObjectPropertyShorthand, PreferOptionalChain, AddParseIntRadix} = await import("./javascript/cleanup/index.js");
41
- const {AsyncCallbackInSyncArrayMethod, AutoFormat, UpgradeDependencyVersion, UpgradeTransitiveDependencyVersion, OrderImports, ChangeImport} = await import("./javascript/recipes/index.js");
41
+ const {AddDependency, AsyncCallbackInSyncArrayMethod, AutoFormat, UpgradeDependencyVersion, UpgradeTransitiveDependencyVersion, OrderImports, ChangeImport} = await import("./javascript/recipes/index.js");
42
42
  const {FindDependency} = await import("./javascript/search/index.js");
43
43
 
44
+ registry.register(AddDependency);
44
45
  registry.register(ExportAssignmentToExportDefault);
45
46
  registry.register(FindDependency);
46
47
  registry.register(OrderImports);
@@ -89,6 +89,22 @@ export const enum NpmrcScope {
89
89
  Project = 'Project', // .npmrc in project root
90
90
  }
91
91
 
92
+ /**
93
+ * Represents a dependency scope in package.json that uses object structure {name: version}.
94
+ * Note: `bundledDependencies` is excluded because it's a string[] of package names, not a version map.
95
+ */
96
+ export type DependencyScope = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';
97
+
98
+ /**
99
+ * All dependency scopes in package.json that use object structure {name: version}.
100
+ */
101
+ export const allDependencyScopes: readonly DependencyScope[] = [
102
+ 'dependencies',
103
+ 'devDependencies',
104
+ 'peerDependencies',
105
+ 'optionalDependencies'
106
+ ] as const;
107
+
92
108
  export const NpmrcKind = "org.openrewrite.javascript.marker.NodeResolutionResult$Npmrc" as const;
93
109
 
94
110
  /**
@@ -14,7 +14,16 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import {PackageManager} from "./node-resolution-result";
17
+ import {
18
+ createNodeResolutionResultMarker,
19
+ findNodeResolutionResult,
20
+ PackageJsonContent,
21
+ PackageLockContent,
22
+ PackageManager,
23
+ readNpmrcConfigs
24
+ } from "./node-resolution-result";
25
+ import {replaceMarkerByKind} from "../markers";
26
+ import {Json} from "../json";
18
27
  import * as fs from "fs";
19
28
  import * as fsp from "fs/promises";
20
29
  import * as path from "path";
@@ -108,7 +117,7 @@ const LOCK_FILE_DETECTION: ReadonlyArray<LockFileDetectionConfig> = [
108
117
  /**
109
118
  * Result of running a package manager command.
110
119
  */
111
- export interface PackageManagerResult {
120
+ interface PackageManagerResult {
112
121
  success: boolean;
113
122
  stdout?: string;
114
123
  stderr?: string;
@@ -118,7 +127,7 @@ export interface PackageManagerResult {
118
127
  /**
119
128
  * Options for running package manager install.
120
129
  */
121
- export interface InstallOptions {
130
+ interface InstallOptions {
122
131
  /** Working directory */
123
132
  cwd: string;
124
133
 
@@ -164,13 +173,6 @@ export function getLockFileDetectionConfig(): ReadonlyArray<LockFileDetectionCon
164
173
  return LOCK_FILE_DETECTION;
165
174
  }
166
175
 
167
- /**
168
- * Gets the configuration for a package manager.
169
- */
170
- export function getPackageManagerConfig(pm: PackageManager): PackageManagerConfig {
171
- return PACKAGE_MANAGER_CONFIGS[pm];
172
- }
173
-
174
176
  /**
175
177
  * Gets the lock file name for a package manager.
176
178
  */
@@ -185,22 +187,10 @@ export function getAllLockFileNames(): string[] {
185
187
  return LOCK_FILE_DETECTION.map(c => c.filename);
186
188
  }
187
189
 
188
- /**
189
- * Checks if a file path is a lock file.
190
- */
191
- export function isLockFile(filePath: string): boolean {
192
- const fileName = path.basename(filePath);
193
- return getAllLockFileNames().includes(fileName);
194
- }
195
-
196
190
  /**
197
191
  * Runs the package manager install command.
198
- *
199
- * @param pm The package manager to use
200
- * @param options Install options
201
- * @returns Result of the install command
202
192
  */
203
- export function runInstall(pm: PackageManager, options: InstallOptions): PackageManagerResult {
193
+ function runInstall(pm: PackageManager, options: InstallOptions): PackageManagerResult {
204
194
  const config = PACKAGE_MANAGER_CONFIGS[pm];
205
195
  const command = options.lockOnly ? config.installLockOnlyCommand : config.installCommand;
206
196
  const [cmd, ...args] = command;
@@ -244,121 +234,6 @@ export function runInstall(pm: PackageManager, options: InstallOptions): Package
244
234
  }
245
235
  }
246
236
 
247
- /**
248
- * Options for adding/upgrading a package.
249
- */
250
- export interface AddPackageOptions {
251
- /** Working directory */
252
- cwd: string;
253
-
254
- /** Package name to add/upgrade */
255
- packageName: string;
256
-
257
- /** Version constraint (e.g., "^5.0.0") */
258
- version: string;
259
-
260
- /** If true, only update lock file without installing to node_modules */
261
- lockOnly?: boolean;
262
-
263
- /** Timeout in milliseconds (default: 120000 = 2 minutes) */
264
- timeout?: number;
265
-
266
- /** Additional environment variables */
267
- env?: Record<string, string>;
268
- }
269
-
270
- /**
271
- * Runs a package manager command to add or upgrade a package.
272
- * This updates both package.json and the lock file.
273
- *
274
- * @param pm The package manager to use
275
- * @param options Add package options
276
- * @returns Result of the command
277
- */
278
- export function runAddPackage(pm: PackageManager, options: AddPackageOptions): PackageManagerResult {
279
- const packageSpec = `${options.packageName}@${options.version}`;
280
-
281
- // Build command based on package manager
282
- let cmd: string;
283
- let args: string[];
284
-
285
- switch (pm) {
286
- case PackageManager.Npm:
287
- cmd = 'npm';
288
- args = ['install', packageSpec];
289
- if (options.lockOnly) {
290
- args.push('--package-lock-only');
291
- }
292
- break;
293
- case PackageManager.YarnClassic:
294
- cmd = 'yarn';
295
- args = ['add', packageSpec];
296
- if (options.lockOnly) {
297
- args.push('--ignore-scripts');
298
- }
299
- break;
300
- case PackageManager.YarnBerry:
301
- cmd = 'yarn';
302
- args = ['add', packageSpec];
303
- if (options.lockOnly) {
304
- args.push('--mode', 'skip-build');
305
- }
306
- break;
307
- case PackageManager.Pnpm:
308
- cmd = 'pnpm';
309
- args = ['add', packageSpec];
310
- if (options.lockOnly) {
311
- args.push('--lockfile-only');
312
- }
313
- break;
314
- case PackageManager.Bun:
315
- cmd = 'bun';
316
- args = ['add', packageSpec];
317
- if (options.lockOnly) {
318
- args.push('--ignore-scripts');
319
- }
320
- break;
321
- }
322
-
323
- try {
324
- const result = spawnSync(cmd, args, {
325
- cwd: options.cwd,
326
- encoding: 'utf-8',
327
- stdio: ['pipe', 'pipe', 'pipe'],
328
- timeout: options.timeout ?? 120000,
329
- env: options.env ? {...process.env, ...options.env} : process.env,
330
- });
331
-
332
- if (result.error) {
333
- return {
334
- success: false,
335
- error: result.error.message,
336
- stderr: result.stderr,
337
- };
338
- }
339
-
340
- if (result.status !== 0) {
341
- return {
342
- success: false,
343
- stdout: result.stdout,
344
- stderr: result.stderr,
345
- error: `Command exited with code ${result.status}`,
346
- };
347
- }
348
-
349
- return {
350
- success: true,
351
- stdout: result.stdout,
352
- stderr: result.stderr,
353
- };
354
- } catch (error: any) {
355
- return {
356
- success: false,
357
- error: error.message,
358
- };
359
- }
360
- }
361
-
362
237
  /**
363
238
  * Runs a package manager list command to get dependency information.
364
239
  *
@@ -390,55 +265,203 @@ export function runList(pm: PackageManager, cwd: string, timeout: number = 30000
390
265
  }
391
266
 
392
267
  /**
393
- * Checks if a package manager is available on the system.
268
+ * Result of running install in a temporary directory.
269
+ */
270
+ export interface TempInstallResult {
271
+ /** Whether the install succeeded */
272
+ success: boolean;
273
+ /** The updated lock file content (if successful and lock file exists) */
274
+ lockFileContent?: string;
275
+ /** Error message (if failed) */
276
+ error?: string;
277
+ }
278
+
279
+ /**
280
+ * Generic accumulator for dependency recipes that run package manager operations.
281
+ * Used by scanning recipes to track state across scanning and editing phases.
394
282
  *
395
- * @param pm The package manager to check
396
- * @returns True if the package manager is available
283
+ * @typeParam T The recipe-specific project update info type
397
284
  */
398
- export function isPackageManagerAvailable(pm: PackageManager): boolean {
399
- const config = PACKAGE_MANAGER_CONFIGS[pm];
400
- const cmd = config.installCommand[0];
285
+ export interface DependencyRecipeAccumulator<T> {
286
+ /** Projects that need updating: packageJsonPath -> update info */
287
+ projectsToUpdate: Map<string, T>;
401
288
 
402
- try {
403
- const result = spawnSync(cmd, ['--version'], {
404
- encoding: 'utf-8',
405
- stdio: ['pipe', 'pipe', 'pipe'],
406
- timeout: 5000,
407
- });
408
- return result.status === 0;
409
- } catch {
410
- return false;
289
+ /** After running package manager, store the updated lock file content */
290
+ updatedLockFiles: Map<string, string>;
291
+
292
+ /** Updated package.json content (after npm install may have modified it) */
293
+ updatedPackageJsons: Map<string, string>;
294
+
295
+ /** Track which projects have been processed (npm install has run) */
296
+ processedProjects: Set<string>;
297
+
298
+ /** Track projects where npm install failed: packageJsonPath -> error message */
299
+ failedProjects: Map<string, string>;
300
+ }
301
+
302
+ /**
303
+ * Creates a new empty accumulator for dependency recipes.
304
+ */
305
+ export function createDependencyRecipeAccumulator<T>(): DependencyRecipeAccumulator<T> {
306
+ return {
307
+ projectsToUpdate: new Map(),
308
+ updatedLockFiles: new Map(),
309
+ updatedPackageJsons: new Map(),
310
+ processedProjects: new Set(),
311
+ failedProjects: new Map()
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Checks if a source path is a lock file and returns the updated content if available.
317
+ * This is a helper for dependency recipes that need to update lock files.
318
+ *
319
+ * @param sourcePath The source path to check
320
+ * @param acc The recipe accumulator containing updated lock file content
321
+ * @returns The updated lock file content if this is a lock file that was updated, undefined otherwise
322
+ */
323
+ export function getUpdatedLockFileContent<T>(
324
+ sourcePath: string,
325
+ acc: DependencyRecipeAccumulator<T>
326
+ ): string | undefined {
327
+ for (const lockFileName of getAllLockFileNames()) {
328
+ if (sourcePath.endsWith(lockFileName)) {
329
+ // Find the corresponding package.json path
330
+ const packageJsonPath = sourcePath.replace(lockFileName, 'package.json');
331
+ const updateInfo = acc.projectsToUpdate.get(packageJsonPath);
332
+
333
+ if (updateInfo && acc.updatedLockFiles.has(sourcePath)) {
334
+ return acc.updatedLockFiles.get(sourcePath);
335
+ }
336
+ break;
337
+ }
411
338
  }
339
+ return undefined;
340
+ }
341
+
342
+ /**
343
+ * Base interface for project update info used by dependency recipes.
344
+ * Recipes extend this with additional fields specific to their needs.
345
+ */
346
+ export interface BaseProjectUpdateInfo {
347
+ /** Absolute path to the project directory */
348
+ projectDir: string;
349
+ /** Relative path to package.json (from source root) */
350
+ packageJsonPath: string;
351
+ /** The package manager used by this project */
352
+ packageManager: PackageManager;
412
353
  }
413
354
 
414
355
  /**
415
- * Gets a human-readable name for a package manager.
356
+ * Stores the result of a package manager install into the accumulator.
357
+ * This handles the common pattern of storing updated lock files and tracking failures.
358
+ *
359
+ * @param result The result from runInstallInTempDir
360
+ * @param acc The recipe accumulator
361
+ * @param updateInfo The project update info (must have packageJsonPath and packageManager)
362
+ * @param modifiedPackageJson The modified package.json content that was used for install
416
363
  */
417
- export function getPackageManagerDisplayName(pm: PackageManager): string {
418
- switch (pm) {
419
- case PackageManager.Npm:
420
- return 'npm';
421
- case PackageManager.YarnClassic:
422
- return 'Yarn Classic';
423
- case PackageManager.YarnBerry:
424
- return 'Yarn Berry';
425
- case PackageManager.Pnpm:
426
- return 'pnpm';
427
- case PackageManager.Bun:
428
- return 'Bun';
364
+ export function storeInstallResult<T extends BaseProjectUpdateInfo>(
365
+ result: TempInstallResult,
366
+ acc: DependencyRecipeAccumulator<T>,
367
+ updateInfo: T,
368
+ modifiedPackageJson: string
369
+ ): void {
370
+ if (result.success) {
371
+ acc.updatedPackageJsons.set(updateInfo.packageJsonPath, modifiedPackageJson);
372
+
373
+ if (result.lockFileContent) {
374
+ const lockFileName = getLockFileName(updateInfo.packageManager);
375
+ const lockFilePath = updateInfo.packageJsonPath.replace('package.json', lockFileName);
376
+ acc.updatedLockFiles.set(lockFilePath, result.lockFileContent);
377
+ }
378
+ } else {
379
+ acc.failedProjects.set(updateInfo.packageJsonPath, result.error || 'Unknown error');
429
380
  }
430
381
  }
431
382
 
432
383
  /**
433
- * Result of running install in a temporary directory.
384
+ * Runs the package manager install for a project if it hasn't been processed yet.
385
+ * Updates the accumulator's processedProjects set after running.
386
+ *
387
+ * @param sourcePath The source path (package.json path) being processed
388
+ * @param acc The recipe accumulator
389
+ * @param runInstall Function that performs the actual install (recipe-specific)
390
+ * @returns The failure message if install failed, undefined otherwise
434
391
  */
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;
392
+ export async function runInstallIfNeeded<T>(
393
+ sourcePath: string,
394
+ acc: DependencyRecipeAccumulator<T>,
395
+ runInstall: () => Promise<void>
396
+ ): Promise<string | undefined> {
397
+ if (!acc.processedProjects.has(sourcePath)) {
398
+ await runInstall();
399
+ acc.processedProjects.add(sourcePath);
400
+ }
401
+ return acc.failedProjects.get(sourcePath);
402
+ }
403
+
404
+ /**
405
+ * Updates the NodeResolutionResult marker on a JSON document after a package manager operation.
406
+ * This recreates the marker based on the updated package.json and lock file content.
407
+ *
408
+ * @param doc The JSON document containing the marker
409
+ * @param updateInfo Project update info with paths and package manager
410
+ * @param acc The recipe accumulator containing updated content
411
+ * @returns The document with the updated marker, or unchanged if no existing marker
412
+ */
413
+ export async function updateNodeResolutionMarker<T extends BaseProjectUpdateInfo>(
414
+ doc: Json.Document,
415
+ updateInfo: T & { originalPackageJson: string },
416
+ acc: DependencyRecipeAccumulator<T>
417
+ ): Promise<Json.Document> {
418
+ const existingMarker = findNodeResolutionResult(doc);
419
+ if (!existingMarker) {
420
+ return doc;
421
+ }
422
+
423
+ // Parse the updated package.json and lock file to create new marker
424
+ const updatedPackageJson = acc.updatedPackageJsons.get(updateInfo.packageJsonPath);
425
+ const lockFileName = getLockFileName(updateInfo.packageManager);
426
+ const updatedLockFile = acc.updatedLockFiles.get(
427
+ updateInfo.packageJsonPath.replace('package.json', lockFileName)
428
+ );
429
+
430
+ let packageJsonContent: PackageJsonContent;
431
+ let lockContent: PackageLockContent | undefined;
432
+
433
+ try {
434
+ packageJsonContent = JSON.parse(updatedPackageJson || updateInfo.originalPackageJson);
435
+ } catch {
436
+ return doc; // Failed to parse, keep original marker
437
+ }
438
+
439
+ if (updatedLockFile) {
440
+ try {
441
+ lockContent = JSON.parse(updatedLockFile);
442
+ } catch {
443
+ // Continue without lock file content
444
+ }
445
+ }
446
+
447
+ // Read npmrc configs from the project directory
448
+ const npmrcConfigs = await readNpmrcConfigs(updateInfo.projectDir);
449
+
450
+ // Create new marker
451
+ const newMarker = createNodeResolutionResultMarker(
452
+ existingMarker.path,
453
+ packageJsonContent,
454
+ lockContent,
455
+ existingMarker.workspacePackagePaths,
456
+ existingMarker.packageManager,
457
+ npmrcConfigs.length > 0 ? npmrcConfigs : undefined
458
+ );
459
+
460
+ // Replace the marker in the document
461
+ return {
462
+ ...doc,
463
+ markers: replaceMarkerByKind(doc.markers, newMarker)
464
+ };
442
465
  }
443
466
 
444
467
  /**