@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
@@ -19,8 +19,7 @@ import {
19
19
  findNodeResolutionResult,
20
20
  PackageJsonContent,
21
21
  PackageLockContent,
22
- PackageManager,
23
- readNpmrcConfigs
22
+ PackageManager
24
23
  } from "./node-resolution-result";
25
24
  import {replaceMarkerByKind} from "../markers";
26
25
  import {Json, JsonParser, JsonVisitor} from "../json";
@@ -59,35 +58,37 @@ interface PackageManagerConfig {
59
58
  const PACKAGE_MANAGER_CONFIGS: Record<PackageManager, PackageManagerConfig> = {
60
59
  [PackageManager.Npm]: {
61
60
  lockFile: 'package-lock.json',
62
- installLockOnlyCommand: ['npm', 'install', '--package-lock-only'],
63
- installCommand: ['npm', 'install'],
61
+ // --ignore-scripts prevents prepublish/prepare scripts from running in temp directory
62
+ installLockOnlyCommand: ['npm', 'install', '--package-lock-only', '--ignore-scripts'],
63
+ installCommand: ['npm', 'install', '--ignore-scripts'],
64
64
  listCommand: ['npm', 'list', '--json', '--all'],
65
65
  },
66
66
  [PackageManager.YarnClassic]: {
67
67
  lockFile: 'yarn.lock',
68
- // Yarn Classic doesn't have a lock-only mode
68
+ // Yarn Classic doesn't have a lock-only mode; --ignore-scripts prevents lifecycle scripts
69
69
  installLockOnlyCommand: ['yarn', 'install', '--ignore-scripts'],
70
- installCommand: ['yarn', 'install'],
70
+ installCommand: ['yarn', 'install', '--ignore-scripts'],
71
71
  listCommand: ['yarn', 'list', '--json'],
72
72
  },
73
73
  [PackageManager.YarnBerry]: {
74
74
  lockFile: 'yarn.lock',
75
- // Yarn Berry's mode skip-build skips post-install scripts
75
+ // --mode skip-build skips post-install scripts in Yarn Berry
76
76
  installLockOnlyCommand: ['yarn', 'install', '--mode', 'skip-build'],
77
- installCommand: ['yarn', 'install'],
77
+ installCommand: ['yarn', 'install', '--mode', 'skip-build'],
78
78
  listCommand: ['yarn', 'info', '--all', '--json'],
79
79
  },
80
80
  [PackageManager.Pnpm]: {
81
81
  lockFile: 'pnpm-lock.yaml',
82
- installLockOnlyCommand: ['pnpm', 'install', '--lockfile-only'],
83
- installCommand: ['pnpm', 'install'],
82
+ // --ignore-scripts prevents lifecycle scripts from running in temp directory
83
+ installLockOnlyCommand: ['pnpm', 'install', '--lockfile-only', '--ignore-scripts'],
84
+ installCommand: ['pnpm', 'install', '--ignore-scripts'],
84
85
  listCommand: ['pnpm', 'list', '--json', '--depth=Infinity'],
85
86
  },
86
87
  [PackageManager.Bun]: {
87
88
  lockFile: 'bun.lock',
88
- // Bun doesn't have a lock-only mode, but is very fast anyway
89
+ // Bun doesn't have a lock-only mode; --ignore-scripts prevents lifecycle scripts
89
90
  installLockOnlyCommand: ['bun', 'install', '--ignore-scripts'],
90
- installCommand: ['bun', 'install'],
91
+ installCommand: ['bun', 'install', '--ignore-scripts'],
91
92
  },
92
93
  };
93
94
 
@@ -447,8 +448,6 @@ export async function parseLockFileContent(
447
448
  * Recipes extend this with additional fields specific to their needs.
448
449
  */
449
450
  export interface BaseProjectUpdateInfo {
450
- /** Absolute path to the project directory */
451
- projectDir: string;
452
451
  /** Relative path to package.json (from source root) */
453
452
  packageJsonPath: string;
454
453
  /** The package manager used by this project */
@@ -559,17 +558,15 @@ export async function updateNodeResolutionMarker<T extends BaseProjectUpdateInfo
559
558
  }
560
559
  }
561
560
 
562
- // Read npmrc configs from the project directory
563
- const npmrcConfigs = await readNpmrcConfigs(updateInfo.projectDir);
564
-
565
- // Create new marker
561
+ // Create new marker, preserving existing npmrc configs from the parser
562
+ // (recipes don't have filesystem access to re-read them)
566
563
  const newMarker = createNodeResolutionResultMarker(
567
564
  existingMarker.path,
568
565
  packageJsonContent,
569
566
  lockContent,
570
567
  existingMarker.workspacePackagePaths,
571
568
  existingMarker.packageManager,
572
- npmrcConfigs.length > 0 ? npmrcConfigs : undefined
569
+ existingMarker.npmrcConfigs
573
570
  );
574
571
 
575
572
  // Replace the marker in the document
@@ -591,52 +588,72 @@ export interface TempInstallOptions {
591
588
  * Default: true (lock-only is faster and sufficient for most cases)
592
589
  */
593
590
  lockOnly?: boolean;
591
+ /**
592
+ * Original lock file content to use. If provided, this content will be written
593
+ * to the temp directory instead of copying from projectDir.
594
+ * This allows recipes to work with in-memory SourceFiles without filesystem access.
595
+ */
596
+ originalLockFileContent?: string;
597
+ /**
598
+ * Config file contents to use. Keys are filenames (e.g., '.npmrc'), values are content.
599
+ * If provided, these will be written to the temp directory instead of copying from projectDir.
600
+ */
601
+ configFiles?: Record<string, string>;
594
602
  }
595
603
 
596
604
  /**
597
- * Runs package manager install in a temporary directory.
598
- *
599
- * This function:
600
- * 1. Creates a temp directory
601
- * 2. Writes the provided package.json content
602
- * 3. Copies the existing lock file (if present)
603
- * 4. Copies config files (.npmrc, .yarnrc, etc.)
604
- * 5. Runs the package manager install
605
- * 6. Returns the updated lock file content
606
- * 7. Cleans up the temp directory
607
- *
608
- * @param projectDir The original project directory (for copying lock file and configs)
609
- * @param pm The package manager to use
610
- * @param modifiedPackageJson The modified package.json content to use
611
- * @param options Optional settings for timeout and lock-only mode
612
- * @returns Result containing success status and lock file content or error
605
+ * Options for running install in a temporary directory with workspace support.
613
606
  */
614
- export async function runInstallInTempDir(
615
- projectDir: string,
607
+ export interface WorkspaceTempInstallOptions extends TempInstallOptions {
608
+ /**
609
+ * Workspace package.json files. Keys are relative paths from the project root
610
+ * (e.g., "packages/foo/package.json"), values are the package.json content.
611
+ * The root package.json should have a "workspaces" field pointing to these packages.
612
+ */
613
+ workspacePackages?: Record<string, string>;
614
+ }
615
+
616
+ /**
617
+ * Internal implementation for running package manager install in a temporary directory.
618
+ * Supports both simple projects and workspaces.
619
+ */
620
+ async function runInstallInTempDirCore(
616
621
  pm: PackageManager,
617
- modifiedPackageJson: string,
618
- options: TempInstallOptions = {}
622
+ rootPackageJson: string,
623
+ options: WorkspaceTempInstallOptions = {}
619
624
  ): Promise<TempInstallResult> {
620
- const {timeout = 120000, lockOnly = true} = options;
625
+ const {
626
+ timeout = 120000,
627
+ lockOnly = true,
628
+ originalLockFileContent,
629
+ configFiles: configFileContents,
630
+ workspacePackages
631
+ } = options;
621
632
  const lockFileName = getLockFileName(pm);
622
633
  const tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'openrewrite-pm-'));
623
634
 
624
635
  try {
625
- // Write modified package.json to temp directory
626
- await fsp.writeFile(path.join(tempDir, 'package.json'), modifiedPackageJson);
636
+ // Write root package.json to temp directory
637
+ await fsp.writeFile(path.join(tempDir, 'package.json'), rootPackageJson);
638
+
639
+ // Write workspace package.json files (creating subdirectories as needed)
640
+ if (workspacePackages) {
641
+ for (const [relativePath, content] of Object.entries(workspacePackages)) {
642
+ const fullPath = path.join(tempDir, relativePath);
643
+ await fsp.mkdir(path.dirname(fullPath), {recursive: true});
644
+ await fsp.writeFile(fullPath, content);
645
+ }
646
+ }
627
647
 
628
- // Copy existing lock file if present
629
- const originalLockPath = path.join(projectDir, lockFileName);
630
- if (fs.existsSync(originalLockPath)) {
631
- await fsp.copyFile(originalLockPath, path.join(tempDir, lockFileName));
648
+ // Write lock file if provided
649
+ if (originalLockFileContent !== undefined) {
650
+ await fsp.writeFile(path.join(tempDir, lockFileName), originalLockFileContent);
632
651
  }
633
652
 
634
- // Copy config files if present (for registry configuration and workspace setup)
635
- const configFiles = ['.npmrc', '.yarnrc', '.yarnrc.yml', '.pnpmfile.cjs', 'pnpm-workspace.yaml'];
636
- for (const configFile of configFiles) {
637
- const configPath = path.join(projectDir, configFile);
638
- if (fs.existsSync(configPath)) {
639
- await fsp.copyFile(configPath, path.join(tempDir, configFile));
653
+ // Write config files if provided
654
+ if (configFileContents) {
655
+ for (const [configFile, content] of Object.entries(configFileContents)) {
656
+ await fsp.writeFile(path.join(tempDir, configFile), content);
640
657
  }
641
658
  }
642
659
 
@@ -688,6 +705,57 @@ export async function runInstallInTempDir(
688
705
  }
689
706
  }
690
707
 
708
+ /**
709
+ * Runs package manager install in a temporary directory.
710
+ *
711
+ * This function:
712
+ * 1. Creates a temp directory
713
+ * 2. Writes the provided package.json content
714
+ * 3. Writes the lock file content (if provided)
715
+ * 4. Writes config files (if provided)
716
+ * 5. Runs the package manager install
717
+ * 6. Returns the updated lock file content
718
+ * 7. Cleans up the temp directory
719
+ *
720
+ * @param pm The package manager to use
721
+ * @param modifiedPackageJson The modified package.json content to use
722
+ * @param options Optional settings for timeout, lock-only mode, and file contents
723
+ * @returns Result containing success status and lock file content or error
724
+ */
725
+ export async function runInstallInTempDir(
726
+ pm: PackageManager,
727
+ modifiedPackageJson: string,
728
+ options: TempInstallOptions = {}
729
+ ): Promise<TempInstallResult> {
730
+ return runInstallInTempDirCore(pm, modifiedPackageJson, options);
731
+ }
732
+
733
+ /**
734
+ * Runs package manager install in a temporary directory with workspace support.
735
+ *
736
+ * This function:
737
+ * 1. Creates a temp directory
738
+ * 2. Writes the root package.json content
739
+ * 3. Writes workspace package.json files (creating subdirectories as needed)
740
+ * 4. Writes the lock file content (if provided)
741
+ * 5. Writes config files (if provided)
742
+ * 6. Runs the package manager install at the root
743
+ * 7. Returns the updated lock file content
744
+ * 8. Cleans up the temp directory
745
+ *
746
+ * @param pm The package manager to use
747
+ * @param rootPackageJson The root package.json content (should contain "workspaces" field)
748
+ * @param options Optional settings including workspace packages, timeout, lock-only mode, and file contents
749
+ * @returns Result containing success status and lock file content or error
750
+ */
751
+ export async function runWorkspaceInstallInTempDir(
752
+ pm: PackageManager,
753
+ rootPackageJson: string,
754
+ options: WorkspaceTempInstallOptions = {}
755
+ ): Promise<TempInstallResult> {
756
+ return runInstallInTempDirCore(pm, rootPackageJson, options);
757
+ }
758
+
691
759
  /**
692
760
  * Creates a lock file visitor that handles updating YAML lock files (pnpm-lock.yaml).
693
761
  * This is a reusable component for dependency recipes.
@@ -77,6 +77,13 @@ export interface ProjectParserOptions {
77
77
  * If not provided, all discovered files are parsed.
78
78
  */
79
79
  fileFilter?: (absolutePath: string) => boolean;
80
+
81
+ /**
82
+ * Optional path to make source file paths relative to.
83
+ * If not specified, paths are relative to projectPath.
84
+ * Use this when parsing a subdirectory but wanting paths relative to the repository root.
85
+ */
86
+ relativeTo?: string;
80
87
  }
81
88
 
82
89
  /**
@@ -153,6 +160,7 @@ const TEXT_CONFIG_FILES = new Set([
153
160
  */
154
161
  export class ProjectParser {
155
162
  private readonly projectPath: string;
163
+ private readonly relativeTo: string;
156
164
  private readonly exclusions: string[];
157
165
  private readonly ctx: ExecutionContext;
158
166
  private readonly useGit: boolean;
@@ -162,6 +170,7 @@ export class ProjectParser {
162
170
 
163
171
  constructor(projectPath: string, options: ProjectParserOptions = {}) {
164
172
  this.projectPath = path.resolve(projectPath);
173
+ this.relativeTo = options.relativeTo ? path.resolve(options.relativeTo) : this.projectPath;
165
174
  this.exclusions = options.exclusions ?? DEFAULT_EXCLUSIONS;
166
175
  this.ctx = options.ctx ?? new ExecutionContext();
167
176
  this.useGit = options.useGit ?? this.isGitRepository();
@@ -230,7 +239,7 @@ export class ProjectParser {
230
239
  this.log(`Parsing ${discovered.packageJsonFiles.length} package.json files...`);
231
240
  const parser = Parsers.createParser("packageJson", {
232
241
  ctx: this.ctx,
233
- relativeTo: this.projectPath
242
+ relativeTo: this.relativeTo
234
243
  });
235
244
  for await (const sf of parser.parse(...discovered.packageJsonFiles)) {
236
245
  current++;
@@ -244,7 +253,7 @@ export class ProjectParser {
244
253
  this.log(`Parsing ${discovered.lockFiles.json.length} JSON lock files...`);
245
254
  const parser = Parsers.createParser("json", {
246
255
  ctx: this.ctx,
247
- relativeTo: this.projectPath
256
+ relativeTo: this.relativeTo
248
257
  });
249
258
  for await (const sf of parser.parse(...discovered.lockFiles.json)) {
250
259
  current++;
@@ -258,7 +267,7 @@ export class ProjectParser {
258
267
  this.log(`Parsing ${discovered.lockFiles.yaml.length} YAML lock files...`);
259
268
  const parser = Parsers.createParser("yaml", {
260
269
  ctx: this.ctx,
261
- relativeTo: this.projectPath
270
+ relativeTo: this.relativeTo
262
271
  });
263
272
  for await (const sf of parser.parse(...discovered.lockFiles.yaml)) {
264
273
  current++;
@@ -272,7 +281,7 @@ export class ProjectParser {
272
281
  this.log(`Parsing ${discovered.lockFiles.text.length} text lock files...`);
273
282
  const parser = Parsers.createParser("plainText", {
274
283
  ctx: this.ctx,
275
- relativeTo: this.projectPath
284
+ relativeTo: this.relativeTo
276
285
  });
277
286
  for await (const sf of parser.parse(...discovered.lockFiles.text)) {
278
287
  current++;
@@ -286,7 +295,7 @@ export class ProjectParser {
286
295
  this.log(`Parsing ${discovered.jsFiles.length} JavaScript/TypeScript files...`);
287
296
  const parser = Parsers.createParser("javascript", {
288
297
  ctx: this.ctx,
289
- relativeTo: this.projectPath
298
+ relativeTo: this.relativeTo
290
299
  });
291
300
 
292
301
  // Check if Prettier is available
@@ -299,7 +308,7 @@ export class ProjectParser {
299
308
  this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
300
309
 
301
310
  const prettierMarker = await prettierLoader.getConfigMarker(
302
- path.join(this.projectPath, sf.sourcePath)
311
+ path.join(this.relativeTo, sf.sourcePath)
303
312
  );
304
313
  if (prettierMarker) {
305
314
  yield produce(sf, draft => {
@@ -352,7 +361,7 @@ export class ProjectParser {
352
361
  this.log(`Parsing ${discovered.yamlFiles.length} YAML files...`);
353
362
  const parser = Parsers.createParser("yaml", {
354
363
  ctx: this.ctx,
355
- relativeTo: this.projectPath
364
+ relativeTo: this.relativeTo
356
365
  });
357
366
  for await (const sf of parser.parse(...discovered.yamlFiles)) {
358
367
  current++;
@@ -366,7 +375,7 @@ export class ProjectParser {
366
375
  this.log(`Parsing ${discovered.jsonFiles.length} JSON files...`);
367
376
  const parser = Parsers.createParser("json", {
368
377
  ctx: this.ctx,
369
- relativeTo: this.projectPath
378
+ relativeTo: this.relativeTo
370
379
  });
371
380
  for await (const sf of parser.parse(...discovered.jsonFiles)) {
372
381
  current++;
@@ -380,7 +389,7 @@ export class ProjectParser {
380
389
  this.log(`Parsing ${discovered.textFiles.length} text config files...`);
381
390
  const parser = Parsers.createParser("plainText", {
382
391
  ctx: this.ctx,
383
- relativeTo: this.projectPath
392
+ relativeTo: this.relativeTo
384
393
  });
385
394
  for await (const sf of parser.parse(...discovered.textFiles)) {
386
395
  current++;
@@ -17,11 +17,15 @@
17
17
  import {Option, ScanningRecipe} from "../../recipe";
18
18
  import {ExecutionContext} from "../../execution";
19
19
  import {TreeVisitor} from "../../visitor";
20
- import {detectIndent, getMemberKeyName, isObject, Json, JsonVisitor, rightPadded, space} from "../../json";
20
+ import {Tree} from "../../tree";
21
+ import {detectIndent, getMemberKeyName, isJson, isObject, Json, JsonVisitor, rightPadded, space} from "../../json";
22
+ import {isDocuments, isYaml, Yaml} from "../../yaml";
23
+ import {isPlainText, PlainText} from "../../text";
21
24
  import {
22
25
  allDependencyScopes,
23
26
  DependencyScope,
24
27
  findNodeResolutionResult,
28
+ NpmrcScope,
25
29
  PackageManager
26
30
  } from "../node-resolution-result";
27
31
  import {emptyMarkers, markupWarn} from "../../markers";
@@ -31,6 +35,7 @@ import {
31
35
  createLockFileEditor,
32
36
  DependencyRecipeAccumulator,
33
37
  getAllLockFileNames,
38
+ getLockFileName,
34
39
  parseLockFileContent,
35
40
  runInstallIfNeeded,
36
41
  runInstallInTempDir,
@@ -44,8 +49,6 @@ import * as path from "path";
44
49
  * Information about a project that needs updating
45
50
  */
46
51
  interface ProjectUpdateInfo {
47
- /** Absolute path to the project directory */
48
- projectDir: string;
49
52
  /** Relative path to package.json (from source root) */
50
53
  packageJsonPath: string;
51
54
  /** Original package.json content */
@@ -56,9 +59,14 @@ interface ProjectUpdateInfo {
56
59
  newVersion: string;
57
60
  /** The package manager used by this project */
58
61
  packageManager: PackageManager;
62
+ /** Config file contents extracted from the project (e.g., .npmrc) */
63
+ configFiles?: Record<string, string>;
59
64
  }
60
65
 
61
- type Accumulator = DependencyRecipeAccumulator<ProjectUpdateInfo>;
66
+ interface Accumulator extends DependencyRecipeAccumulator<ProjectUpdateInfo> {
67
+ /** Original lock file content, keyed by lock file path */
68
+ originalLockFiles: Map<string, string>;
69
+ }
62
70
 
63
71
  /**
64
72
  * Adds a new dependency to package.json and updates the lock file.
@@ -100,7 +108,10 @@ export class AddDependency extends ScanningRecipe<Accumulator> {
100
108
  scope?: DependencyScope;
101
109
 
102
110
  initialValue(_ctx: ExecutionContext): Accumulator {
103
- return createDependencyRecipeAccumulator();
111
+ return {
112
+ ...createDependencyRecipeAccumulator<ProjectUpdateInfo>(),
113
+ originalLockFiles: new Map()
114
+ };
104
115
  }
105
116
 
106
117
  private getTargetScope(): DependencyScope {
@@ -109,10 +120,38 @@ export class AddDependency extends ScanningRecipe<Accumulator> {
109
120
 
110
121
  async scanner(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
111
122
  const recipe = this;
123
+ const LOCK_FILE_NAMES = getAllLockFileNames();
124
+
125
+ return new class extends TreeVisitor<Tree, ExecutionContext> {
126
+ protected async accept(tree: Tree, ctx: ExecutionContext): Promise<Tree | undefined> {
127
+ // Handle JSON documents (package.json and JSON lock files)
128
+ if (isJson(tree) && tree.kind === Json.Kind.Document) {
129
+ return this.handleJsonDocument(tree as Json.Document, ctx);
130
+ }
131
+
132
+ // Handle YAML documents (pnpm-lock.yaml)
133
+ if (isYaml(tree) && isDocuments(tree)) {
134
+ return this.handleYamlDocument(tree, ctx);
135
+ }
136
+
137
+ // Handle PlainText files (yarn.lock for Yarn Classic)
138
+ if (isPlainText(tree)) {
139
+ return this.handlePlainTextDocument(tree as PlainText, ctx);
140
+ }
141
+
142
+ return tree;
143
+ }
144
+
145
+ private async handleJsonDocument(doc: Json.Document, _ctx: ExecutionContext): Promise<Json | undefined> {
146
+ const basename = path.basename(doc.sourcePath);
112
147
 
113
- return new class extends JsonVisitor<ExecutionContext> {
114
- protected async visitDocument(doc: Json.Document, _ctx: ExecutionContext): Promise<Json | undefined> {
115
- // Only process package.json files
148
+ // Capture JSON lock file content (package-lock.json, bun.lock)
149
+ if (LOCK_FILE_NAMES.includes(basename)) {
150
+ acc.originalLockFiles.set(doc.sourcePath, await TreePrinters.print(doc));
151
+ return doc;
152
+ }
153
+
154
+ // Only process package.json files for dependency analysis
116
155
  if (!doc.sourcePath.endsWith('package.json')) {
117
156
  return doc;
118
157
  }
@@ -131,24 +170,43 @@ export class AddDependency extends ScanningRecipe<Accumulator> {
131
170
  }
132
171
  }
133
172
 
134
- // Get the project directory and package manager
135
- const projectDir = path.dirname(path.resolve(doc.sourcePath));
136
173
  const pm = marker.packageManager ?? PackageManager.Npm;
137
174
 
175
+ // Extract project-level .npmrc config from marker
176
+ const configFiles: Record<string, string> = {};
177
+ const projectNpmrc = marker.npmrcConfigs?.find(c => c.scope === NpmrcScope.Project);
178
+ if (projectNpmrc) {
179
+ const lines = Object.entries(projectNpmrc.properties)
180
+ .map(([key, value]) => `${key}=${value}`);
181
+ configFiles['.npmrc'] = lines.join('\n');
182
+ }
183
+
138
184
  acc.projectsToUpdate.set(doc.sourcePath, {
139
- projectDir,
140
185
  packageJsonPath: doc.sourcePath,
141
- originalPackageJson: await this.printDocument(doc),
186
+ originalPackageJson: await TreePrinters.print(doc),
142
187
  dependencyScope: recipe.getTargetScope(),
143
188
  newVersion: recipe.version,
144
- packageManager: pm
189
+ packageManager: pm,
190
+ configFiles: Object.keys(configFiles).length > 0 ? configFiles : undefined
145
191
  });
146
192
 
147
193
  return doc;
148
194
  }
149
195
 
150
- private async printDocument(doc: Json.Document): Promise<string> {
151
- return TreePrinters.print(doc);
196
+ private async handleYamlDocument(docs: Yaml.Documents, _ctx: ExecutionContext): Promise<Yaml.Documents | undefined> {
197
+ const basename = path.basename(docs.sourcePath);
198
+ if (LOCK_FILE_NAMES.includes(basename)) {
199
+ acc.originalLockFiles.set(docs.sourcePath, await TreePrinters.print(docs));
200
+ }
201
+ return docs;
202
+ }
203
+
204
+ private async handlePlainTextDocument(text: PlainText, _ctx: ExecutionContext): Promise<PlainText | undefined> {
205
+ const basename = path.basename(text.sourcePath);
206
+ if (LOCK_FILE_NAMES.includes(basename)) {
207
+ acc.originalLockFiles.set(text.sourcePath, await TreePrinters.print(text));
208
+ }
209
+ return text;
152
210
  }
153
211
  };
154
212
  }
@@ -217,6 +275,7 @@ export class AddDependency extends ScanningRecipe<Accumulator> {
217
275
 
218
276
  /**
219
277
  * Runs the package manager in a temporary directory to update the lock file.
278
+ * All file contents are provided from in-memory sources (SourceFiles), not read from disk.
220
279
  */
221
280
  private async runPackageManagerInstall(
222
281
  acc: Accumulator,
@@ -229,10 +288,23 @@ export class AddDependency extends ScanningRecipe<Accumulator> {
229
288
  updateInfo.dependencyScope
230
289
  );
231
290
 
291
+ // Get the lock file path based on package manager
292
+ const lockFileName = getLockFileName(updateInfo.packageManager);
293
+ const packageJsonDir = path.dirname(updateInfo.packageJsonPath);
294
+ const lockFilePath = packageJsonDir === '.'
295
+ ? lockFileName
296
+ : path.join(packageJsonDir, lockFileName);
297
+
298
+ // Look up the original lock file content from captured SourceFiles
299
+ const originalLockFileContent = acc.originalLockFiles.get(lockFilePath);
300
+
232
301
  const result = await runInstallInTempDir(
233
- updateInfo.projectDir,
234
302
  updateInfo.packageManager,
235
- modifiedPackageJson
303
+ modifiedPackageJson,
304
+ {
305
+ originalLockFileContent,
306
+ configFiles: updateInfo.configFiles
307
+ }
236
308
  );
237
309
 
238
310
  storeInstallResult(result, acc, updateInfo, modifiedPackageJson);