@openrewrite/rewrite 8.69.0-20251211-160325 → 8.69.0-20251212-112414

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 (80) hide show
  1. package/dist/cli/cli-utils.d.ts.map +1 -1
  2. package/dist/cli/cli-utils.js +110 -71
  3. package/dist/cli/cli-utils.js.map +1 -1
  4. package/dist/javascript/package-json-parser.d.ts +0 -5
  5. package/dist/javascript/package-json-parser.d.ts.map +1 -1
  6. package/dist/javascript/package-json-parser.js +2 -12
  7. package/dist/javascript/package-json-parser.js.map +1 -1
  8. package/dist/javascript/package-manager.d.ts +72 -1
  9. package/dist/javascript/package-manager.d.ts.map +1 -1
  10. package/dist/javascript/package-manager.js +173 -2
  11. package/dist/javascript/package-manager.js.map +1 -1
  12. package/dist/javascript/recipes/add-dependency.d.ts.map +1 -1
  13. package/dist/javascript/recipes/add-dependency.js +11 -8
  14. package/dist/javascript/recipes/add-dependency.js.map +1 -1
  15. package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -1
  16. package/dist/javascript/recipes/upgrade-dependency-version.js +11 -8
  17. package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -1
  18. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts.map +1 -1
  19. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js +11 -8
  20. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js.map +1 -1
  21. package/dist/json/parser.d.ts.map +1 -1
  22. package/dist/json/parser.js +355 -123
  23. package/dist/json/parser.js.map +1 -1
  24. package/dist/json/tree.d.ts +5 -1
  25. package/dist/json/tree.d.ts.map +1 -1
  26. package/dist/json/tree.js +157 -7
  27. package/dist/json/tree.js.map +1 -1
  28. package/dist/print.d.ts.map +1 -1
  29. package/dist/print.js +5 -1
  30. package/dist/print.js.map +1 -1
  31. package/dist/rpc/request/get-languages.d.ts.map +1 -1
  32. package/dist/rpc/request/get-languages.js +1 -0
  33. package/dist/rpc/request/get-languages.js.map +1 -1
  34. package/dist/rpc/server.d.ts +1 -0
  35. package/dist/rpc/server.d.ts.map +1 -1
  36. package/dist/rpc/server.js +1 -0
  37. package/dist/rpc/server.js.map +1 -1
  38. package/dist/version.txt +1 -1
  39. package/dist/yaml/index.d.ts +6 -0
  40. package/dist/yaml/index.d.ts.map +1 -0
  41. package/dist/yaml/index.js +37 -0
  42. package/dist/yaml/index.js.map +1 -0
  43. package/dist/yaml/parser.d.ts +6 -0
  44. package/dist/yaml/parser.d.ts.map +1 -0
  45. package/dist/yaml/parser.js +803 -0
  46. package/dist/yaml/parser.js.map +1 -0
  47. package/dist/yaml/print.d.ts +2 -0
  48. package/dist/yaml/print.d.ts.map +1 -0
  49. package/dist/yaml/print.js +234 -0
  50. package/dist/yaml/print.js.map +1 -0
  51. package/dist/yaml/rpc.d.ts +2 -0
  52. package/dist/yaml/rpc.d.ts.map +1 -0
  53. package/dist/yaml/rpc.js +264 -0
  54. package/dist/yaml/rpc.js.map +1 -0
  55. package/dist/yaml/tree.d.ts +188 -0
  56. package/dist/yaml/tree.d.ts.map +1 -0
  57. package/dist/yaml/tree.js +117 -0
  58. package/dist/yaml/tree.js.map +1 -0
  59. package/dist/yaml/visitor.d.ts +19 -0
  60. package/dist/yaml/visitor.d.ts.map +1 -0
  61. package/dist/yaml/visitor.js +170 -0
  62. package/dist/yaml/visitor.js.map +1 -0
  63. package/package.json +6 -1
  64. package/src/cli/cli-utils.ts +112 -35
  65. package/src/javascript/package-json-parser.ts +2 -12
  66. package/src/javascript/package-manager.ts +179 -3
  67. package/src/javascript/recipes/add-dependency.ts +16 -10
  68. package/src/javascript/recipes/upgrade-dependency-version.ts +16 -10
  69. package/src/javascript/recipes/upgrade-transitive-dependency-version.ts +15 -9
  70. package/src/json/parser.ts +443 -146
  71. package/src/json/tree.ts +159 -7
  72. package/src/print.ts +6 -2
  73. package/src/rpc/request/get-languages.ts +1 -0
  74. package/src/rpc/server.ts +1 -0
  75. package/src/yaml/index.ts +21 -0
  76. package/src/yaml/parser.ts +850 -0
  77. package/src/yaml/print.ts +212 -0
  78. package/src/yaml/rpc.ts +248 -0
  79. package/src/yaml/tree.ts +281 -0
  80. package/src/yaml/visitor.ts +146 -0
@@ -19,8 +19,17 @@ import * as path from 'path';
19
19
  import {spawn, spawnSync} from 'child_process';
20
20
  import {Recipe, RecipeRegistry} from '../recipe';
21
21
  import {SourceFile} from '../tree';
22
- import {JavaScriptParser, PackageJsonParser} from '../javascript';
22
+ import {
23
+ isYarnBerryLockFile,
24
+ JavaScriptParser,
25
+ JSON_LOCK_FILE_NAMES,
26
+ PackageJsonParser,
27
+ TEXT_LOCK_FILE_NAMES,
28
+ YAML_LOCK_FILE_NAMES
29
+ } from '../javascript';
23
30
  import {JsonParser} from '../json';
31
+ import {PlainTextParser} from '../text';
32
+ import {YamlParser} from '../yaml';
24
33
 
25
34
  // ANSI color codes
26
35
  const colors = {
@@ -440,11 +449,17 @@ export async function walkDirectory(
440
449
  }
441
450
  }
442
451
 
452
+ /**
453
+ * All lock file names (typed as string[] for easier comparison)
454
+ */
455
+ const ALL_LOCK_FILE_NAMES: readonly string[] = [...JSON_LOCK_FILE_NAMES, ...YAML_LOCK_FILE_NAMES, ...TEXT_LOCK_FILE_NAMES];
456
+
443
457
  /**
444
458
  * Check if a file is accepted for parsing based on its extension
445
459
  */
446
460
  export function isAcceptedFile(filePath: string): boolean {
447
461
  const ext = path.extname(filePath).toLowerCase();
462
+ const basename = path.basename(filePath);
448
463
 
449
464
  // JavaScript/TypeScript files
450
465
  if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts'].includes(ext)) {
@@ -456,6 +471,16 @@ export function isAcceptedFile(filePath: string): boolean {
456
471
  return true;
457
472
  }
458
473
 
474
+ // YAML files
475
+ if (['.yaml', '.yml'].includes(ext)) {
476
+ return true;
477
+ }
478
+
479
+ // Lock files (some have non-standard extensions like yarn.lock)
480
+ if (ALL_LOCK_FILE_NAMES.includes(basename)) {
481
+ return true;
482
+ }
483
+
459
484
  return false;
460
485
  }
461
486
 
@@ -466,6 +491,56 @@ export interface ParseFilesOptions {
466
491
  onProgress?: ProgressCallback;
467
492
  }
468
493
 
494
+ /**
495
+ * Internal context for file parsing progress tracking.
496
+ */
497
+ interface ParseContext {
498
+ current: number;
499
+ total: number;
500
+ verbose: boolean;
501
+ onProgress?: ProgressCallback;
502
+ }
503
+
504
+ /**
505
+ * Helper to parse files with a given parser, handling verbose logging and progress.
506
+ */
507
+ async function* parseWithParser(
508
+ files: string[],
509
+ parser: { parse(...files: string[]): AsyncGenerator<SourceFile> },
510
+ fileType: string,
511
+ ctx: ParseContext
512
+ ): AsyncGenerator<SourceFile, ParseContext> {
513
+ if (files.length === 0) {
514
+ return ctx;
515
+ }
516
+
517
+ if (ctx.verbose) {
518
+ console.log(`Parsing ${files.length} ${fileType} files...`);
519
+ }
520
+
521
+ for await (const sf of parser.parse(...files)) {
522
+ ctx.current++;
523
+ ctx.onProgress?.(ctx.current, ctx.total, sf.sourcePath);
524
+ yield sf;
525
+ }
526
+
527
+ return ctx;
528
+ }
529
+
530
+ /**
531
+ * Classifies a yarn.lock file as YAML (Berry) or text (Classic) based on its content.
532
+ * Returns 'yaml' for Yarn Berry (v2+) and 'text' for Yarn Classic (v1).
533
+ */
534
+ async function classifyYarnLockFile(filePath: string): Promise<'yaml' | 'text'> {
535
+ try {
536
+ const content = await fsp.readFile(filePath, 'utf-8');
537
+ return isYarnBerryLockFile(content) ? 'yaml' : 'text';
538
+ } catch {
539
+ // Default to text format if we can't read the file
540
+ return 'text';
541
+ }
542
+ }
543
+
469
544
  /**
470
545
  * Parse source files using appropriate parsers (streaming version).
471
546
  * Yields source files as they are parsed, allowing immediate processing.
@@ -483,6 +558,13 @@ export async function* parseFilesStreaming(
483
558
  const jsFiles: string[] = [];
484
559
  const packageJsonFiles: string[] = [];
485
560
  const jsonFiles: string[] = [];
561
+ const jsonLockFiles: string[] = [];
562
+ const yamlLockFiles: string[] = [];
563
+ const yamlFiles: string[] = [];
564
+ const textLockFiles: string[] = [];
565
+
566
+ // Collect yarn.lock files for content-based classification
567
+ const yarnLockFiles: string[] = [];
486
568
 
487
569
  for (const filePath of filePaths) {
488
570
  const basename = path.basename(filePath);
@@ -492,49 +574,44 @@ export async function* parseFilesStreaming(
492
574
  packageJsonFiles.push(filePath);
493
575
  } else if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts'].includes(ext)) {
494
576
  jsFiles.push(filePath);
577
+ } else if ((JSON_LOCK_FILE_NAMES as readonly string[]).includes(basename)) {
578
+ jsonLockFiles.push(filePath);
579
+ } else if ((YAML_LOCK_FILE_NAMES as readonly string[]).includes(basename)) {
580
+ yamlLockFiles.push(filePath);
581
+ } else if (basename === 'yarn.lock') {
582
+ // yarn.lock needs content-based classification
583
+ yarnLockFiles.push(filePath);
584
+ } else if ((TEXT_LOCK_FILE_NAMES as readonly string[]).includes(basename)) {
585
+ // Other text lock files (if any besides yarn.lock)
586
+ textLockFiles.push(filePath);
495
587
  } else if (ext === '.json') {
496
588
  jsonFiles.push(filePath);
589
+ } else if (['.yaml', '.yml'].includes(ext)) {
590
+ yamlFiles.push(filePath);
497
591
  }
498
592
  }
499
593
 
500
- // Parse JavaScript/TypeScript files
501
- if (jsFiles.length > 0) {
502
- if (verbose) {
503
- console.log(`Parsing ${jsFiles.length} JavaScript/TypeScript files...`);
504
- }
505
- const jsParser = new JavaScriptParser({relativeTo: projectRoot});
506
- for await (const sf of jsParser.parse(...jsFiles)) {
507
- current++;
508
- onProgress?.(current, total, sf.sourcePath);
509
- yield sf;
594
+ // Classify yarn.lock files by content (Yarn Berry uses YAML, Classic uses text)
595
+ for (const yarnLockPath of yarnLockFiles) {
596
+ const format = await classifyYarnLockFile(yarnLockPath);
597
+ if (format === 'yaml') {
598
+ yamlLockFiles.push(yarnLockPath);
599
+ } else {
600
+ textLockFiles.push(yarnLockPath);
510
601
  }
511
602
  }
512
603
 
513
- // Parse package.json files
514
- if (packageJsonFiles.length > 0) {
515
- if (verbose) {
516
- console.log(`Parsing ${packageJsonFiles.length} package.json files...`);
517
- }
518
- const pkgParser = new PackageJsonParser({relativeTo: projectRoot});
519
- for await (const sf of pkgParser.parse(...packageJsonFiles)) {
520
- current++;
521
- onProgress?.(current, total, sf.sourcePath);
522
- yield sf;
523
- }
524
- }
604
+ // Create parse context for tracking progress
605
+ const ctx: ParseContext = { current, total, verbose, onProgress };
525
606
 
526
- // Parse other JSON files
527
- if (jsonFiles.length > 0) {
528
- if (verbose) {
529
- console.log(`Parsing ${jsonFiles.length} JSON files...`);
530
- }
531
- const jsonParser = new JsonParser({relativeTo: projectRoot});
532
- for await (const sf of jsonParser.parse(...jsonFiles)) {
533
- current++;
534
- onProgress?.(current, total, sf.sourcePath);
535
- yield sf;
536
- }
537
- }
607
+ // Parse files by type using helper
608
+ yield* parseWithParser(jsFiles, new JavaScriptParser({relativeTo: projectRoot}), 'JavaScript/TypeScript', ctx);
609
+ yield* parseWithParser(packageJsonFiles, new PackageJsonParser({relativeTo: projectRoot}), 'package.json', ctx);
610
+ yield* parseWithParser(jsonLockFiles, new JsonParser({relativeTo: projectRoot}), 'JSON lock', ctx);
611
+ yield* parseWithParser(yamlLockFiles, new YamlParser({relativeTo: projectRoot}), 'YAML lock', ctx);
612
+ yield* parseWithParser(textLockFiles, new PlainTextParser({relativeTo: projectRoot}), 'text lock', ctx);
613
+ yield* parseWithParser(yamlFiles, new YamlParser({relativeTo: projectRoot}), 'YAML', ctx);
614
+ yield* parseWithParser(jsonFiles, new JsonParser({relativeTo: projectRoot}), 'JSON', ctx);
538
615
  }
539
616
 
540
617
  /**
@@ -336,20 +336,10 @@ export class PackageJsonParser extends Parser {
336
336
 
337
337
  /**
338
338
  * Parses JSONC (JSON with Comments and trailing commas) content.
339
- *
340
- * Note: This is a simple regex-based approach that works for bun.lock files but doesn't
341
- * handle edge cases like comment-like sequences inside strings (e.g., "// not a comment").
342
- * For lock files this is acceptable since they don't contain such patterns. If broader
343
- * JSONC support is needed, consider using a proper parser like `jsonc-parser`.
344
339
  */
345
340
  private parseJsonc(content: string): Record<string, any> {
346
- // Remove single-line comments (// ...)
347
- let stripped = content.replace(/\/\/.*$/gm, '');
348
- // Remove multi-line comments (/* ... */)
349
- stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '');
350
- // Remove trailing commas before ] or }
351
- stripped = stripped.replace(/,(\s*[}\]])/g, '$1');
352
- return JSON.parse(stripped);
341
+ const {parse} = require('jsonc-parser');
342
+ return parse(content);
353
343
  }
354
344
 
355
345
  /**
@@ -23,12 +23,18 @@ import {
23
23
  readNpmrcConfigs
24
24
  } from "./node-resolution-result";
25
25
  import {replaceMarkerByKind} from "../markers";
26
- import {Json} from "../json";
26
+ import {Json, JsonParser, JsonVisitor} from "../json";
27
+ import {isDocuments, Yaml, YamlParser, YamlVisitor} from "../yaml";
28
+ import {PlainTextParser} from "../text";
29
+ import {SourceFile} from "../tree";
30
+ import {TreeVisitor} from "../visitor";
31
+ import {ExecutionContext} from "../execution";
27
32
  import * as fs from "fs";
28
33
  import * as fsp from "fs/promises";
29
34
  import * as path from "path";
30
35
  import * as os from "os";
31
36
  import {spawnSync} from "child_process";
37
+ import * as YAML from "yaml";
32
38
 
33
39
  /**
34
40
  * Configuration for each package manager.
@@ -114,6 +120,34 @@ const LOCK_FILE_DETECTION: ReadonlyArray<LockFileDetectionConfig> = [
114
120
  },
115
121
  ];
116
122
 
123
+ /**
124
+ * Lock file names that should be parsed as JSON/JSONC format.
125
+ */
126
+ export const JSON_LOCK_FILE_NAMES = ['bun.lock', 'package-lock.json'] as const;
127
+
128
+ /**
129
+ * Lock file names that should be parsed as YAML format.
130
+ */
131
+ export const YAML_LOCK_FILE_NAMES = ['pnpm-lock.yaml'] as const;
132
+
133
+ /**
134
+ * Lock file names that should be parsed as plain text (custom formats like yarn.lock v1).
135
+ * Note: yarn.lock for Yarn Berry (v2+) is actually YAML format and should be parsed as such.
136
+ * Use `getLockFileFormat` with content to determine the correct format for yarn.lock.
137
+ */
138
+ export const TEXT_LOCK_FILE_NAMES = ['yarn.lock'] as const;
139
+
140
+ /**
141
+ * Detects if a yarn.lock file is Yarn Berry (v2+) format based on content.
142
+ * Yarn Berry lock files contain a `__metadata:` key which is not present in Classic.
143
+ *
144
+ * @param content The yarn.lock file content
145
+ * @returns true if this is a Yarn Berry lock file (YAML format), false for Classic
146
+ */
147
+ export function isYarnBerryLockFile(content: string): boolean {
148
+ return content.includes('__metadata:');
149
+ }
150
+
117
151
  /**
118
152
  * Result of running a package manager command.
119
153
  */
@@ -339,6 +373,75 @@ export function getUpdatedLockFileContent<T>(
339
373
  return undefined;
340
374
  }
341
375
 
376
+ /**
377
+ * Determines the appropriate parser for a lock file based on its filename and optionally content.
378
+ *
379
+ * For yarn.lock files, the format depends on the Yarn version:
380
+ * - Yarn Classic (v1): Custom plain text format
381
+ * - Yarn Berry (v2+): YAML format
382
+ *
383
+ * If content is provided for yarn.lock, it will be used to detect the format.
384
+ * Otherwise, defaults to 'text' (Yarn Classic).
385
+ *
386
+ * @param lockFileName The lock file name (e.g., "pnpm-lock.yaml", "package-lock.json", "yarn.lock")
387
+ * @param content Optional file content (used for yarn.lock format detection)
388
+ * @returns 'yaml' for YAML lock files, 'json' for JSON lock files, 'text' for plain text
389
+ */
390
+ export function getLockFileFormat(lockFileName: string, content?: string): 'yaml' | 'json' | 'text' {
391
+ if ((YAML_LOCK_FILE_NAMES as readonly string[]).includes(lockFileName)) {
392
+ return 'yaml';
393
+ }
394
+ if (lockFileName === 'yarn.lock') {
395
+ // Yarn Berry (v2+) uses YAML format, Classic uses custom text format
396
+ if (content && isYarnBerryLockFile(content)) {
397
+ return 'yaml';
398
+ }
399
+ return 'text';
400
+ }
401
+ if ((TEXT_LOCK_FILE_NAMES as readonly string[]).includes(lockFileName)) {
402
+ return 'text';
403
+ }
404
+ // package-lock.json, bun.lock
405
+ return 'json';
406
+ }
407
+
408
+ /**
409
+ * Re-parses updated lock file content using the appropriate parser.
410
+ * This is used by dependency recipes to create the updated lock file SourceFile.
411
+ *
412
+ * For yarn.lock files, the content is used to detect whether it's Yarn Berry (YAML)
413
+ * or Yarn Classic (plain text) format.
414
+ *
415
+ * @param content The updated lock file content
416
+ * @param sourcePath The source path of the lock file
417
+ * @param lockFileName The lock file name (e.g., "pnpm-lock.yaml", "yarn.lock")
418
+ * @returns The parsed SourceFile (Json.Document, Yaml.Documents, or PlainText)
419
+ */
420
+ export async function parseLockFileContent(
421
+ content: string,
422
+ sourcePath: string,
423
+ lockFileName: string
424
+ ): Promise<SourceFile> {
425
+ // Pass content to getLockFileFormat for yarn.lock detection
426
+ const format = getLockFileFormat(lockFileName, content);
427
+
428
+ switch (format) {
429
+ case 'yaml': {
430
+ const parser = new YamlParser({});
431
+ return await parser.parseOne({text: content, sourcePath}) as Yaml.Documents;
432
+ }
433
+ case 'text': {
434
+ const parser = new PlainTextParser({});
435
+ return await parser.parseOne({text: content, sourcePath});
436
+ }
437
+ case 'json':
438
+ default: {
439
+ const parser = new JsonParser({});
440
+ return await parser.parseOne({text: content, sourcePath}) as Json.Document;
441
+ }
442
+ }
443
+ }
444
+
342
445
  /**
343
446
  * Base interface for project update info used by dependency recipes.
344
447
  * Recipes extend this with additional fields specific to their needs.
@@ -438,7 +541,19 @@ export async function updateNodeResolutionMarker<T extends BaseProjectUpdateInfo
438
541
 
439
542
  if (updatedLockFile) {
440
543
  try {
441
- lockContent = JSON.parse(updatedLockFile);
544
+ // Parse lock file based on format
545
+ if (updateInfo.packageManager === PackageManager.Pnpm) {
546
+ // pnpm-lock.yaml is YAML format
547
+ lockContent = YAML.parse(updatedLockFile);
548
+ } else if (updateInfo.packageManager === PackageManager.YarnClassic ||
549
+ updateInfo.packageManager === PackageManager.YarnBerry) {
550
+ // yarn.lock has a custom format - skip parsing here
551
+ // The marker will still be updated with package.json info
552
+ lockContent = undefined;
553
+ } else {
554
+ // npm (package-lock.json) and bun (bun.lock) use JSON
555
+ lockContent = JSON.parse(updatedLockFile);
556
+ }
442
557
  } catch {
443
558
  // Continue without lock file content
444
559
  }
@@ -533,9 +648,21 @@ export async function runInstallInTempDir(
533
648
  });
534
649
 
535
650
  if (!result.success) {
651
+ // Combine error message with stderr for more useful diagnostics
652
+ const errorParts: string[] = [];
653
+ if (result.error) {
654
+ errorParts.push(result.error);
655
+ }
656
+ if (result.stderr) {
657
+ // Trim and limit stderr to avoid excessively long error messages
658
+ const stderr = result.stderr.trim();
659
+ if (stderr) {
660
+ errorParts.push(stderr.length > 2000 ? stderr.slice(0, 2000) + '...' : stderr);
661
+ }
662
+ }
536
663
  return {
537
664
  success: false,
538
- error: result.error || result.stderr || 'Unknown error'
665
+ error: errorParts.length > 0 ? errorParts.join('\n\n') : 'Unknown error'
539
666
  };
540
667
  }
541
668
 
@@ -560,3 +687,52 @@ export async function runInstallInTempDir(
560
687
  }
561
688
  }
562
689
  }
690
+
691
+ /**
692
+ * Creates a lock file visitor that handles updating YAML lock files (pnpm-lock.yaml).
693
+ * This is a reusable component for dependency recipes.
694
+ *
695
+ * @param acc The recipe accumulator containing updated lock file content
696
+ * @returns A YamlVisitor that updates YAML lock files
697
+ */
698
+ export function createYamlLockFileVisitor<T>(
699
+ acc: DependencyRecipeAccumulator<T>
700
+ ): YamlVisitor<ExecutionContext> {
701
+ return new class extends YamlVisitor<ExecutionContext> {
702
+ protected async visitDocuments(docs: Yaml.Documents, _ctx: ExecutionContext): Promise<Yaml | undefined> {
703
+ const sourcePath = docs.sourcePath;
704
+ const updatedLockContent = getUpdatedLockFileContent(sourcePath, acc);
705
+ if (updatedLockContent) {
706
+ const lockFileName = path.basename(sourcePath);
707
+ return await parseLockFileContent(updatedLockContent, sourcePath, lockFileName) as Yaml.Documents;
708
+ }
709
+ return docs;
710
+ }
711
+ };
712
+ }
713
+
714
+ /**
715
+ * Creates a composite visitor that delegates to the appropriate editor based on tree type.
716
+ * This handles both JSON (package-lock.json, bun.lock) and YAML (pnpm-lock.yaml) lock files.
717
+ *
718
+ * @param jsonEditor The JSON visitor for handling JSON files
719
+ * @param acc The recipe accumulator for YAML lock file handling
720
+ * @returns A TreeVisitor that handles both JSON and YAML files
721
+ */
722
+ export function createLockFileEditor<T>(
723
+ jsonEditor: JsonVisitor<ExecutionContext>,
724
+ acc: DependencyRecipeAccumulator<T>
725
+ ): TreeVisitor<any, ExecutionContext> {
726
+ const yamlEditor = createYamlLockFileVisitor(acc);
727
+
728
+ return new class extends TreeVisitor<any, ExecutionContext> {
729
+ async visit(tree: any, ctx: ExecutionContext): Promise<any> {
730
+ if (isDocuments(tree)) {
731
+ return yamlEditor.visit(tree, ctx);
732
+ } else if (tree && tree.kind === Json.Kind.Document) {
733
+ return jsonEditor.visit(tree, ctx);
734
+ }
735
+ return tree;
736
+ }
737
+ };
738
+ }
@@ -17,7 +17,7 @@
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, JsonParser, JsonVisitor, rightPadded, space} from "../../json";
20
+ import {detectIndent, getMemberKeyName, isObject, Json, JsonVisitor, rightPadded, space} from "../../json";
21
21
  import {
22
22
  allDependencyScopes,
23
23
  DependencyScope,
@@ -28,8 +28,10 @@ import {emptyMarkers, markupWarn} from "../../markers";
28
28
  import {TreePrinters} from "../../print";
29
29
  import {
30
30
  createDependencyRecipeAccumulator,
31
+ createLockFileEditor,
31
32
  DependencyRecipeAccumulator,
32
- getUpdatedLockFileContent,
33
+ getAllLockFileNames,
34
+ parseLockFileContent,
33
35
  runInstallIfNeeded,
34
36
  runInstallInTempDir,
35
37
  storeInstallResult,
@@ -154,7 +156,8 @@ export class AddDependency extends ScanningRecipe<Accumulator> {
154
156
  async editorWithData(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
155
157
  const recipe = this;
156
158
 
157
- return new class extends JsonVisitor<ExecutionContext> {
159
+ // Create JSON visitor that handles both package.json and JSON lock files
160
+ const jsonEditor = new class extends JsonVisitor<ExecutionContext> {
158
161
  protected async visitDocument(doc: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
159
162
  const sourcePath = doc.sourcePath;
160
163
 
@@ -189,18 +192,21 @@ export class AddDependency extends ScanningRecipe<Accumulator> {
189
192
  return updateNodeResolutionMarker(modifiedDoc, updateInfo, acc);
190
193
  }
191
194
 
192
- // Handle lock files for all package managers
193
- const updatedLockContent = getUpdatedLockFileContent(sourcePath, acc);
194
- if (updatedLockContent) {
195
- return await new JsonParser({}).parseOne({
196
- text: updatedLockContent,
197
- sourcePath: doc.sourcePath
198
- }) as Json.Document;
195
+ // Handle JSON lock files (package-lock.json, bun.lock)
196
+ const lockFileName = path.basename(sourcePath);
197
+ if (getAllLockFileNames().includes(lockFileName)) {
198
+ const updatedLockContent = acc.updatedLockFiles.get(sourcePath);
199
+ if (updatedLockContent) {
200
+ return await parseLockFileContent(updatedLockContent, sourcePath, lockFileName) as Json.Document;
201
+ }
199
202
  }
200
203
 
201
204
  return doc;
202
205
  }
203
206
  };
207
+
208
+ // Return composite visitor that handles both JSON and YAML lock files
209
+ return createLockFileEditor(jsonEditor, acc);
204
210
  }
205
211
 
206
212
  /**
@@ -17,7 +17,7 @@
17
17
  import {Option, ScanningRecipe} from "../../recipe";
18
18
  import {ExecutionContext} from "../../execution";
19
19
  import {TreeVisitor} from "../../visitor";
20
- import {getMemberKeyName, isLiteral, Json, JsonParser, JsonVisitor} from "../../json";
20
+ import {getMemberKeyName, isLiteral, Json, JsonVisitor} from "../../json";
21
21
  import {
22
22
  allDependencyScopes,
23
23
  DependencyScope,
@@ -30,8 +30,10 @@ import {markupWarn, replaceMarkerByKind} from "../../markers";
30
30
  import {TreePrinters} from "../../print";
31
31
  import {
32
32
  createDependencyRecipeAccumulator,
33
+ createLockFileEditor,
33
34
  DependencyRecipeAccumulator,
34
- getUpdatedLockFileContent,
35
+ getAllLockFileNames,
36
+ parseLockFileContent,
35
37
  runInstallIfNeeded,
36
38
  runInstallInTempDir,
37
39
  storeInstallResult,
@@ -210,7 +212,8 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
210
212
  async editorWithData(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
211
213
  const recipe = this;
212
214
 
213
- return new class extends JsonVisitor<ExecutionContext> {
215
+ // Create JSON visitor that handles both package.json and JSON lock files
216
+ const jsonEditor = new class extends JsonVisitor<ExecutionContext> {
214
217
  protected async visitDocument(doc: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
215
218
  const sourcePath = doc.sourcePath;
216
219
 
@@ -252,18 +255,21 @@ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
252
255
  return updateNodeResolutionMarker(modifiedDoc, updateInfo, acc);
253
256
  }
254
257
 
255
- // Handle lock files for all package managers
256
- const updatedLockContent = getUpdatedLockFileContent(sourcePath, acc);
257
- if (updatedLockContent) {
258
- return await new JsonParser({}).parseOne({
259
- text: updatedLockContent,
260
- sourcePath: doc.sourcePath
261
- }) as Json.Document;
258
+ // Handle JSON lock files (package-lock.json, bun.lock)
259
+ const lockFileName = path.basename(sourcePath);
260
+ if (getAllLockFileNames().includes(lockFileName)) {
261
+ const updatedLockContent = acc.updatedLockFiles.get(sourcePath);
262
+ if (updatedLockContent) {
263
+ return await parseLockFileContent(updatedLockContent, sourcePath, lockFileName) as Json.Document;
264
+ }
262
265
  }
263
266
 
264
267
  return doc;
265
268
  }
266
269
  };
270
+
271
+ // Return composite visitor that handles both JSON and YAML lock files
272
+ return createLockFileEditor(jsonEditor, acc);
267
273
  }
268
274
 
269
275
  /**
@@ -30,8 +30,10 @@ import {markupWarn} from "../../markers";
30
30
  import {TreePrinters} from "../../print";
31
31
  import {
32
32
  createDependencyRecipeAccumulator,
33
+ createLockFileEditor,
33
34
  DependencyRecipeAccumulator,
34
- getUpdatedLockFileContent,
35
+ getAllLockFileNames,
36
+ parseLockFileContent,
35
37
  runInstallIfNeeded,
36
38
  runInstallInTempDir,
37
39
  storeInstallResult,
@@ -187,7 +189,8 @@ export class UpgradeTransitiveDependencyVersion extends ScanningRecipe<Accumulat
187
189
  async editorWithData(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
188
190
  const recipe = this;
189
191
 
190
- return new class extends JsonVisitor<ExecutionContext> {
192
+ // Create JSON visitor that handles both package.json and JSON lock files
193
+ const jsonEditor = new class extends JsonVisitor<ExecutionContext> {
191
194
  protected async visitDocument(doc: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
192
195
  const sourcePath = doc.sourcePath;
193
196
 
@@ -217,13 +220,13 @@ export class UpgradeTransitiveDependencyVersion extends ScanningRecipe<Accumulat
217
220
  return updateNodeResolutionMarker(modifiedDoc, updateInfo, acc);
218
221
  }
219
222
 
220
- // Handle lock files for all package managers
221
- const updatedLockContent = getUpdatedLockFileContent(sourcePath, acc);
222
- if (updatedLockContent) {
223
- return await new JsonParser({}).parseOne({
224
- text: updatedLockContent,
225
- sourcePath: doc.sourcePath
226
- }) as Json.Document;
223
+ // Handle JSON lock files (package-lock.json, bun.lock)
224
+ const lockFileName = path.basename(sourcePath);
225
+ if (getAllLockFileNames().includes(lockFileName)) {
226
+ const updatedLockContent = acc.updatedLockFiles.get(sourcePath);
227
+ if (updatedLockContent) {
228
+ return await parseLockFileContent(updatedLockContent, sourcePath, lockFileName) as Json.Document;
229
+ }
227
230
  }
228
231
 
229
232
  return doc;
@@ -271,6 +274,9 @@ export class UpgradeTransitiveDependencyVersion extends ScanningRecipe<Accumulat
271
274
  };
272
275
  }
273
276
  };
277
+
278
+ // Return composite visitor that handles both JSON and YAML lock files
279
+ return createLockFileEditor(jsonEditor, acc);
274
280
  }
275
281
 
276
282
  /**