@openrewrite/rewrite 8.68.0-20251204-054843 → 8.68.0-20251204-145030

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 (76) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +4 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/javascript/index.d.ts +3 -0
  5. package/dist/javascript/index.d.ts.map +1 -1
  6. package/dist/javascript/index.js +3 -0
  7. package/dist/javascript/index.js.map +1 -1
  8. package/dist/javascript/package-json-parser.d.ts +0 -5
  9. package/dist/javascript/package-json-parser.d.ts.map +1 -1
  10. package/dist/javascript/package-json-parser.js +13 -25
  11. package/dist/javascript/package-json-parser.js.map +1 -1
  12. package/dist/javascript/package-manager.d.ts +131 -0
  13. package/dist/javascript/package-manager.d.ts.map +1 -0
  14. package/dist/javascript/package-manager.js +372 -0
  15. package/dist/javascript/package-manager.js.map +1 -0
  16. package/dist/javascript/recipes/index.d.ts +2 -0
  17. package/dist/javascript/recipes/index.d.ts.map +1 -0
  18. package/dist/javascript/recipes/index.js +33 -0
  19. package/dist/javascript/recipes/index.js.map +1 -0
  20. package/dist/javascript/recipes/upgrade-dependency-version.d.ts +105 -0
  21. package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -0
  22. package/dist/javascript/recipes/upgrade-dependency-version.js +493 -0
  23. package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -0
  24. package/dist/javascript/search/find-dependency.d.ts +32 -0
  25. package/dist/javascript/search/find-dependency.d.ts.map +1 -0
  26. package/dist/javascript/search/find-dependency.js +312 -0
  27. package/dist/javascript/search/find-dependency.js.map +1 -0
  28. package/dist/javascript/search/index.d.ts +1 -0
  29. package/dist/javascript/search/index.d.ts.map +1 -1
  30. package/dist/javascript/search/index.js +1 -0
  31. package/dist/javascript/search/index.js.map +1 -1
  32. package/dist/json/print.js +1 -1
  33. package/dist/json/print.js.map +1 -1
  34. package/dist/markers.d.ts +67 -0
  35. package/dist/markers.d.ts.map +1 -1
  36. package/dist/markers.js +101 -0
  37. package/dist/markers.js.map +1 -1
  38. package/dist/print.d.ts.map +1 -1
  39. package/dist/print.js +0 -1
  40. package/dist/print.js.map +1 -1
  41. package/dist/recipe.js +3 -3
  42. package/dist/recipe.js.map +1 -1
  43. package/dist/rpc/index.js +72 -0
  44. package/dist/rpc/index.js.map +1 -1
  45. package/dist/rpc/request/generate.js +1 -1
  46. package/dist/rpc/request/generate.js.map +1 -1
  47. package/dist/rpc/request/get-languages.d.ts.map +1 -1
  48. package/dist/rpc/request/get-languages.js +2 -1
  49. package/dist/rpc/request/get-languages.js.map +1 -1
  50. package/dist/rpc/request/visit.d.ts.map +1 -1
  51. package/dist/rpc/request/visit.js +27 -0
  52. package/dist/rpc/request/visit.js.map +1 -1
  53. package/dist/run.js +2 -2
  54. package/dist/run.js.map +1 -1
  55. package/dist/test/rewrite-test.js +1 -1
  56. package/dist/test/rewrite-test.js.map +1 -1
  57. package/dist/version.txt +1 -1
  58. package/package.json +1 -1
  59. package/src/index.ts +4 -0
  60. package/src/javascript/index.ts +3 -0
  61. package/src/javascript/package-json-parser.ts +14 -33
  62. package/src/javascript/package-manager.ts +428 -0
  63. package/src/javascript/recipes/index.ts +17 -0
  64. package/src/javascript/recipes/upgrade-dependency-version.ts +586 -0
  65. package/src/javascript/search/find-dependency.ts +303 -0
  66. package/src/javascript/search/index.ts +1 -0
  67. package/src/json/print.ts +1 -1
  68. package/src/markers.ts +146 -0
  69. package/src/print.ts +0 -1
  70. package/src/recipe.ts +3 -3
  71. package/src/rpc/index.ts +65 -1
  72. package/src/rpc/request/generate.ts +1 -1
  73. package/src/rpc/request/get-languages.ts +2 -1
  74. package/src/rpc/request/visit.ts +32 -1
  75. package/src/run.ts +2 -2
  76. package/src/test/rewrite-test.ts +1 -1
@@ -0,0 +1,586 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import {Option, ScanningRecipe} from "../../recipe";
18
+ import {ExecutionContext} from "../../execution";
19
+ import {TreeVisitor} from "../../visitor";
20
+ import {Json, JsonParser, JsonVisitor} from "../../json";
21
+ import {
22
+ createNodeResolutionResultMarker,
23
+ findNodeResolutionResult,
24
+ PackageJsonContent,
25
+ PackageLockContent,
26
+ PackageManager,
27
+ readNpmrcConfigs
28
+ } from "../node-resolution-result";
29
+ import * as fs from "fs";
30
+ import * as fsp from "fs/promises";
31
+ import * as path from "path";
32
+ import * as os from "os";
33
+ import * as semver from "semver";
34
+ import {markupWarn, replaceMarkerByKind} from "../../markers";
35
+ import {TreePrinters} from "../../print";
36
+ import {
37
+ getAllLockFileNames,
38
+ getLockFileName,
39
+ runInstall
40
+ } from "../package-manager";
41
+
42
+ /**
43
+ * Represents a dependency scope in package.json
44
+ */
45
+ type DependencyScope = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';
46
+
47
+ /**
48
+ * Information about a project that needs updating
49
+ */
50
+ interface ProjectUpdateInfo {
51
+ /** Absolute path to the project directory */
52
+ projectDir: string;
53
+ /** Relative path to package.json (from source root) */
54
+ packageJsonPath: string;
55
+ /** Original package.json content */
56
+ originalPackageJson: string;
57
+ /** The scope where the dependency was found */
58
+ dependencyScope: DependencyScope;
59
+ /** Current version constraint */
60
+ currentVersion: string;
61
+ /** New version constraint to apply */
62
+ newVersion: string;
63
+ /** The package manager used by this project */
64
+ packageManager: PackageManager;
65
+ /**
66
+ * If true, skip running the package manager because the resolved version
67
+ * already satisfies the new constraint. Only package.json needs updating.
68
+ */
69
+ skipInstall: boolean;
70
+ }
71
+
72
+ /**
73
+ * Accumulator for tracking state across scanning and editing phases
74
+ */
75
+ interface Accumulator {
76
+ /** Projects that need updating: packageJsonPath -> update info */
77
+ projectsToUpdate: Map<string, ProjectUpdateInfo>;
78
+
79
+ /** After running package manager, store the updated lock file content */
80
+ updatedLockFiles: Map<string, string>;
81
+
82
+ /** Updated package.json content (after npm install may have modified it) */
83
+ updatedPackageJsons: Map<string, string>;
84
+
85
+ /** Track which projects have been processed (npm install has run) */
86
+ processedProjects: Set<string>;
87
+
88
+ /** Track projects where npm install failed: packageJsonPath -> error message */
89
+ failedProjects: Map<string, string>;
90
+ }
91
+
92
+ /**
93
+ * Upgrades the version of a dependency in package.json and updates the lock file.
94
+ *
95
+ * This recipe:
96
+ * 1. Finds package.json files containing the specified dependency
97
+ * 2. Updates the version constraint to the new version
98
+ * 3. Runs the package manager to update the lock file
99
+ * 4. Updates the NodeResolutionResult marker with new dependency info
100
+ *
101
+ * TODO: Consider adding a `resolveToLatestMatching` option that would use `npm install <pkg>@<version>`
102
+ * to let the package manager resolve the constraint to the latest matching version.
103
+ * For example, `^22.0.0` would become `^22.19.1` (the latest version satisfying ^22.0.0).
104
+ * This would be similar to how Maven's UpgradeDependencyVersion works with version selectors.
105
+ */
106
+ export class UpgradeDependencyVersion extends ScanningRecipe<Accumulator> {
107
+ readonly name = "org.openrewrite.javascript.dependencies.upgrade-dependency-version";
108
+ readonly displayName = "Upgrade npm dependency version";
109
+ readonly description = "Upgrades the version of a dependency in `package.json` and updates the lock file by running the package manager.";
110
+
111
+ @Option({
112
+ displayName: "Package name",
113
+ description: "The name of the npm package to upgrade (e.g., 'lodash', '@types/node')",
114
+ example: "lodash"
115
+ })
116
+ packageName!: string;
117
+
118
+ @Option({
119
+ displayName: "New version",
120
+ description: "The new version constraint to set (e.g., '^5.0.0', '~2.1.0', '3.0.0')",
121
+ example: "^5.0.0"
122
+ })
123
+ newVersion!: string;
124
+
125
+ initialValue(_ctx: ExecutionContext): Accumulator {
126
+ return {
127
+ projectsToUpdate: new Map(),
128
+ updatedLockFiles: new Map(),
129
+ updatedPackageJsons: new Map(),
130
+ processedProjects: new Set(),
131
+ failedProjects: new Map()
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Determines if the dependency should be upgraded from currentVersion to newVersion.
137
+ * Returns true only if the new version constraint represents a strictly newer version.
138
+ *
139
+ * This prevents:
140
+ * - Re-applying the same version (idempotency)
141
+ * - Downgrading to an older version
142
+ *
143
+ * @param currentVersion Current version constraint (e.g., "^4.17.20")
144
+ * @param newVersion New version constraint to apply (e.g., "^4.17.21")
145
+ * @returns true if upgrade should proceed, false otherwise
146
+ */
147
+ shouldUpgrade(currentVersion: string, newVersion: string): boolean {
148
+ // If they're identical strings, no upgrade needed
149
+ if (currentVersion === newVersion) {
150
+ return false;
151
+ }
152
+
153
+ // Extract the minimum version from each constraint
154
+ // semver.minVersion returns the lowest version that could match the range
155
+ const currentMin = semver.minVersion(currentVersion);
156
+ const newMin = semver.minVersion(newVersion);
157
+
158
+ // If either constraint is invalid, fall back to string comparison
159
+ // (will upgrade if strings differ)
160
+ if (!currentMin || !newMin) {
161
+ return currentVersion !== newVersion;
162
+ }
163
+
164
+ // Only upgrade if new minimum version is strictly greater than current
165
+ return semver.gt(newMin, currentMin);
166
+ }
167
+
168
+ async scanner(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
169
+ const recipe = this;
170
+
171
+ return new class extends JsonVisitor<ExecutionContext> {
172
+ protected async visitDocument(doc: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
173
+ // Only process package.json files
174
+ if (!doc.sourcePath.endsWith('package.json')) {
175
+ return doc;
176
+ }
177
+
178
+ const marker = findNodeResolutionResult(doc);
179
+ if (!marker) {
180
+ return doc;
181
+ }
182
+
183
+ // Check each dependency scope for the target package
184
+ const scopes: DependencyScope[] = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
185
+
186
+ for (const scope of scopes) {
187
+ const deps = marker[scope];
188
+ const dep = deps?.find(d => d.name === recipe.packageName);
189
+
190
+ if (dep) {
191
+ const currentVersion = dep.versionConstraint;
192
+
193
+ // Check if version needs updating using semver comparison
194
+ // Only upgrade if the new version is strictly newer than current
195
+ if (recipe.shouldUpgrade(currentVersion, recipe.newVersion)) {
196
+ // Get the project directory from the marker path
197
+ const projectDir = path.dirname(path.resolve(doc.sourcePath));
198
+
199
+ // Use package manager from marker (set during parsing), default to npm
200
+ const pm = marker.packageManager ?? PackageManager.Npm;
201
+
202
+ // Check if the resolved version already satisfies the new constraint.
203
+ // If so, we can skip running the package manager entirely.
204
+ const resolvedDep = marker.resolvedDependencies?.find(
205
+ rd => rd.name === recipe.packageName
206
+ );
207
+ const skipInstall = resolvedDep !== undefined &&
208
+ semver.satisfies(resolvedDep.version, recipe.newVersion);
209
+
210
+ acc.projectsToUpdate.set(doc.sourcePath, {
211
+ projectDir,
212
+ packageJsonPath: doc.sourcePath,
213
+ originalPackageJson: await this.printDocument(doc),
214
+ dependencyScope: scope,
215
+ currentVersion,
216
+ newVersion: recipe.newVersion,
217
+ packageManager: pm,
218
+ skipInstall
219
+ });
220
+ }
221
+ break; // Found the dependency, no need to check other scopes
222
+ }
223
+ }
224
+
225
+ return doc;
226
+ }
227
+
228
+ private async printDocument(doc: Json.Document): Promise<string> {
229
+ return TreePrinters.print(doc);
230
+ }
231
+ };
232
+ }
233
+
234
+ async editorWithData(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
235
+ const recipe = this;
236
+
237
+ return new class extends JsonVisitor<ExecutionContext> {
238
+ protected async visitDocument(doc: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
239
+ const sourcePath = doc.sourcePath;
240
+
241
+ // Handle package.json files
242
+ if (sourcePath.endsWith('package.json')) {
243
+ const updateInfo = acc.projectsToUpdate.get(sourcePath);
244
+ if (!updateInfo) {
245
+ return doc; // This package.json doesn't need updating
246
+ }
247
+
248
+ // Run package manager install if we haven't processed this project yet
249
+ // Skip if the resolved version already satisfies the new constraint
250
+ if (!updateInfo.skipInstall && !acc.processedProjects.has(sourcePath)) {
251
+ await recipe.runPackageManagerInstall(acc, updateInfo, ctx);
252
+ acc.processedProjects.add(sourcePath);
253
+ }
254
+
255
+ // Check if the install failed - if so, don't update, just add warning
256
+ const failureMessage = acc.failedProjects.get(sourcePath);
257
+ if (failureMessage) {
258
+ return markupWarn(
259
+ doc,
260
+ `Failed to upgrade ${recipe.packageName} to ${recipe.newVersion}`,
261
+ failureMessage
262
+ );
263
+ }
264
+
265
+ // Update the dependency version in the JSON AST (preserves formatting)
266
+ const visitor = new UpdateVersionVisitor(
267
+ recipe.packageName,
268
+ updateInfo.newVersion,
269
+ updateInfo.dependencyScope
270
+ );
271
+ const modifiedDoc = await visitor.visit(doc, undefined) as Json.Document;
272
+
273
+ // Update the NodeResolutionResult marker
274
+ return recipe.updateMarker(modifiedDoc, updateInfo, acc);
275
+ }
276
+
277
+ // Handle lock files for all package managers
278
+ for (const lockFileName of getAllLockFileNames()) {
279
+ if (sourcePath.endsWith(lockFileName)) {
280
+ // Find the corresponding package.json path
281
+ const packageJsonPath = sourcePath.replace(lockFileName, 'package.json');
282
+ const updateInfo = acc.projectsToUpdate.get(packageJsonPath);
283
+
284
+ if (updateInfo && acc.updatedLockFiles.has(sourcePath)) {
285
+ // Parse the updated lock file content and return it
286
+ const updatedContent = acc.updatedLockFiles.get(sourcePath)!;
287
+ return this.parseUpdatedLockFile(doc, updatedContent);
288
+ }
289
+ break;
290
+ }
291
+ }
292
+
293
+ return doc;
294
+ }
295
+
296
+ /**
297
+ * Parses updated lock file content and creates a new document.
298
+ */
299
+ private async parseUpdatedLockFile(
300
+ originalDoc: Json.Document,
301
+ updatedContent: string
302
+ ): Promise<Json.Document> {
303
+ // Parse the updated content using JsonParser
304
+ const parser = new JsonParser({});
305
+ const parsed: Json.Document[] = [];
306
+
307
+ for await (const sf of parser.parse({text: updatedContent, sourcePath: originalDoc.sourcePath})) {
308
+ parsed.push(sf as Json.Document);
309
+ }
310
+
311
+ if (parsed.length > 0) {
312
+ // Preserve the original source path and markers
313
+ return {
314
+ ...parsed[0],
315
+ sourcePath: originalDoc.sourcePath,
316
+ markers: originalDoc.markers
317
+ };
318
+ }
319
+
320
+ return originalDoc;
321
+ }
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Runs the package manager in a temporary directory to update the lock file.
327
+ * Writes a modified package.json with the new version, then runs install to update the lock file.
328
+ */
329
+ private async runPackageManagerInstall(
330
+ acc: Accumulator,
331
+ updateInfo: ProjectUpdateInfo,
332
+ _ctx: ExecutionContext
333
+ ): Promise<void> {
334
+ const pm = updateInfo.packageManager;
335
+ const lockFileName = getLockFileName(pm);
336
+
337
+ // Create temp directory
338
+ const tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'openrewrite-pm-'));
339
+
340
+ try {
341
+ // Create modified package.json with the new version constraint
342
+ const modifiedPackageJson = this.createModifiedPackageJson(
343
+ updateInfo.originalPackageJson,
344
+ updateInfo.dependencyScope,
345
+ updateInfo.newVersion
346
+ );
347
+
348
+ // Write modified package.json to temp directory
349
+ await fsp.writeFile(path.join(tempDir, 'package.json'), modifiedPackageJson);
350
+
351
+ // Copy existing lock file if present
352
+ const originalLockPath = path.join(updateInfo.projectDir, lockFileName);
353
+ if (fs.existsSync(originalLockPath)) {
354
+ await fsp.copyFile(originalLockPath, path.join(tempDir, lockFileName));
355
+ }
356
+
357
+ // Copy config files if present (for registry configuration and workspace setup)
358
+ const configFiles = ['.npmrc', '.yarnrc', '.yarnrc.yml', '.pnpmfile.cjs', 'pnpm-workspace.yaml'];
359
+ for (const configFile of configFiles) {
360
+ const configPath = path.join(updateInfo.projectDir, configFile);
361
+ if (fs.existsSync(configPath)) {
362
+ await fsp.copyFile(configPath, path.join(tempDir, configFile));
363
+ }
364
+ }
365
+
366
+ // Run package manager install to validate the version and update lock file
367
+ const result = runInstall(pm, {
368
+ cwd: tempDir,
369
+ lockOnly: true,
370
+ timeout: 120000 // 2 minute timeout
371
+ });
372
+
373
+ if (result.success) {
374
+ // Store the modified package.json (we'll use our visitor for actual output)
375
+ acc.updatedPackageJsons.set(updateInfo.packageJsonPath, modifiedPackageJson);
376
+
377
+ // Read back the updated lock file
378
+ const updatedLockPath = path.join(tempDir, lockFileName);
379
+ if (fs.existsSync(updatedLockPath)) {
380
+ const updatedLockContent = await fsp.readFile(updatedLockPath, 'utf-8');
381
+ const lockFilePath = updateInfo.packageJsonPath.replace('package.json', lockFileName);
382
+ acc.updatedLockFiles.set(lockFilePath, updatedLockContent);
383
+ }
384
+ } else {
385
+ // Track the failure - don't update package.json, the version likely doesn't exist
386
+ const errorMessage = result.error || result.stderr || 'Unknown error';
387
+ acc.failedProjects.set(updateInfo.packageJsonPath, errorMessage);
388
+ }
389
+
390
+ } finally {
391
+ // Cleanup temp directory
392
+ try {
393
+ await fsp.rm(tempDir, {recursive: true, force: true});
394
+ } catch {
395
+ // Ignore cleanup errors
396
+ }
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Creates a modified package.json with the updated dependency version.
402
+ * Used for the temp directory to validate the version exists.
403
+ */
404
+ private createModifiedPackageJson(
405
+ originalContent: string,
406
+ scope: DependencyScope,
407
+ newVersion: string
408
+ ): string {
409
+ const packageJson = JSON.parse(originalContent);
410
+
411
+ if (packageJson[scope] && packageJson[scope][this.packageName]) {
412
+ packageJson[scope][this.packageName] = newVersion;
413
+ }
414
+
415
+ return JSON.stringify(packageJson, null, 2);
416
+ }
417
+
418
+ /**
419
+ * Updates the NodeResolutionResult marker with new dependency information.
420
+ */
421
+ private async updateMarker(
422
+ doc: Json.Document,
423
+ updateInfo: ProjectUpdateInfo,
424
+ acc: Accumulator
425
+ ): Promise<Json.Document> {
426
+ const existingMarker = findNodeResolutionResult(doc);
427
+ if (!existingMarker) {
428
+ return doc;
429
+ }
430
+
431
+ // If we skipped install, just update the versionConstraint in the marker
432
+ // The resolved version is already correct, we only changed the constraint
433
+ if (updateInfo.skipInstall) {
434
+ return this.updateMarkerVersionConstraint(doc, existingMarker, updateInfo);
435
+ }
436
+
437
+ // Parse the updated package.json and lock file to create new marker
438
+ const updatedPackageJson = acc.updatedPackageJsons.get(updateInfo.packageJsonPath);
439
+ const lockFileName = getLockFileName(updateInfo.packageManager);
440
+ const updatedLockFile = acc.updatedLockFiles.get(
441
+ updateInfo.packageJsonPath.replace('package.json', lockFileName)
442
+ );
443
+
444
+ let packageJsonContent: PackageJsonContent;
445
+ let lockContent: PackageLockContent | undefined;
446
+
447
+ try {
448
+ packageJsonContent = JSON.parse(updatedPackageJson || updateInfo.originalPackageJson);
449
+ } catch {
450
+ return doc; // Failed to parse, keep original marker
451
+ }
452
+
453
+ if (updatedLockFile) {
454
+ try {
455
+ lockContent = JSON.parse(updatedLockFile);
456
+ } catch {
457
+ // Continue without lock file content
458
+ }
459
+ }
460
+
461
+ // Read npmrc configs from the project directory
462
+ const npmrcConfigs = await readNpmrcConfigs(updateInfo.projectDir);
463
+
464
+ // Create new marker
465
+ const newMarker = createNodeResolutionResultMarker(
466
+ existingMarker.path,
467
+ packageJsonContent,
468
+ lockContent,
469
+ existingMarker.workspacePackagePaths,
470
+ existingMarker.packageManager,
471
+ npmrcConfigs.length > 0 ? npmrcConfigs : undefined
472
+ );
473
+
474
+ // Replace the marker in the document
475
+ return {
476
+ ...doc,
477
+ markers: replaceMarkerByKind(doc.markers, newMarker)
478
+ };
479
+ }
480
+
481
+ /**
482
+ * Updates just the versionConstraint in the marker for the target dependency.
483
+ * Used when skipInstall is true - the resolved version is unchanged.
484
+ */
485
+ private updateMarkerVersionConstraint(
486
+ doc: Json.Document,
487
+ existingMarker: any,
488
+ updateInfo: ProjectUpdateInfo
489
+ ): Json.Document {
490
+ // Create updated dependency lists with the new versionConstraint
491
+ const updateDeps = (deps: any[] | undefined) => {
492
+ if (!deps) return deps;
493
+ return deps.map(dep => {
494
+ if (dep.name === this.packageName) {
495
+ return {...dep, versionConstraint: updateInfo.newVersion};
496
+ }
497
+ return dep;
498
+ });
499
+ };
500
+
501
+ const newMarker = {
502
+ ...existingMarker,
503
+ [updateInfo.dependencyScope]: updateDeps(existingMarker[updateInfo.dependencyScope])
504
+ };
505
+
506
+ return {
507
+ ...doc,
508
+ markers: replaceMarkerByKind(doc.markers, newMarker)
509
+ };
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Visitor that updates the version of a specific dependency in a specific scope.
515
+ */
516
+ class UpdateVersionVisitor extends JsonVisitor<void> {
517
+ private readonly packageName: string;
518
+ private readonly newVersion: string;
519
+ private readonly targetScope: DependencyScope;
520
+ private inTargetScope = false;
521
+
522
+ constructor(packageName: string, newVersion: string, targetScope: DependencyScope) {
523
+ super();
524
+ this.packageName = packageName;
525
+ this.newVersion = newVersion;
526
+ this.targetScope = targetScope;
527
+ }
528
+
529
+ protected async visitMember(member: Json.Member, p: void): Promise<Json | undefined> {
530
+ // Check if we're entering the target scope
531
+ const keyName = this.getMemberKeyName(member);
532
+
533
+ if (keyName === this.targetScope) {
534
+ // We're entering the dependencies scope
535
+ this.inTargetScope = true;
536
+ const result = await super.visitMember(member, p);
537
+ this.inTargetScope = false;
538
+ return result;
539
+ }
540
+
541
+ // Check if this is the dependency we're looking for
542
+ if (this.inTargetScope && keyName === this.packageName) {
543
+ // Update the version value
544
+ return this.updateVersion(member);
545
+ }
546
+
547
+ return super.visitMember(member, p);
548
+ }
549
+
550
+ private getMemberKeyName(member: Json.Member): string | undefined {
551
+ const key = member.key.element;
552
+ if (key.kind === Json.Kind.Literal) {
553
+ // Remove quotes from string literal
554
+ const source = (key as Json.Literal).source;
555
+ if (source.startsWith('"') && source.endsWith('"')) {
556
+ return source.slice(1, -1);
557
+ }
558
+ return source;
559
+ } else if (key.kind === Json.Kind.Identifier) {
560
+ return (key as Json.Identifier).name;
561
+ }
562
+ return undefined;
563
+ }
564
+
565
+ private updateVersion(member: Json.Member): Json.Member {
566
+ const value = member.value;
567
+
568
+ if (value.kind !== Json.Kind.Literal) {
569
+ return member; // Not a literal value, can't update
570
+ }
571
+
572
+ const literal = value as Json.Literal;
573
+
574
+ // Create new literal with updated version
575
+ const newLiteral: Json.Literal = {
576
+ ...literal,
577
+ source: `"${this.newVersion}"`,
578
+ value: this.newVersion
579
+ };
580
+
581
+ return {
582
+ ...member,
583
+ value: newLiteral
584
+ };
585
+ }
586
+ }