@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
@@ -0,0 +1,467 @@
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 {detectIndent, getMemberKeyName, isObject, Json, JsonParser, JsonVisitor, rightPadded, space} from "../../json";
21
+ import {
22
+ allDependencyScopes,
23
+ DependencyScope,
24
+ findNodeResolutionResult,
25
+ PackageManager
26
+ } from "../node-resolution-result";
27
+ import {emptyMarkers, markupWarn} from "../../markers";
28
+ import {TreePrinters} from "../../print";
29
+ import {
30
+ createDependencyRecipeAccumulator,
31
+ DependencyRecipeAccumulator,
32
+ getUpdatedLockFileContent,
33
+ runInstallIfNeeded,
34
+ runInstallInTempDir,
35
+ storeInstallResult,
36
+ updateNodeResolutionMarker
37
+ } from "../package-manager";
38
+ import {randomId} from "../../uuid";
39
+ import * as path from "path";
40
+
41
+ /**
42
+ * Information about a project that needs updating
43
+ */
44
+ interface ProjectUpdateInfo {
45
+ /** Absolute path to the project directory */
46
+ projectDir: string;
47
+ /** Relative path to package.json (from source root) */
48
+ packageJsonPath: string;
49
+ /** Original package.json content */
50
+ originalPackageJson: string;
51
+ /** The scope where the dependency should be added */
52
+ dependencyScope: DependencyScope;
53
+ /** Version constraint to apply */
54
+ newVersion: string;
55
+ /** The package manager used by this project */
56
+ packageManager: PackageManager;
57
+ }
58
+
59
+ type Accumulator = DependencyRecipeAccumulator<ProjectUpdateInfo>;
60
+
61
+ /**
62
+ * Adds a new dependency to package.json and updates the lock file.
63
+ *
64
+ * This recipe:
65
+ * 1. Finds package.json files that don't already have the specified dependency
66
+ * 2. Adds the dependency to the specified scope (defaults to 'dependencies')
67
+ * 3. Runs the package manager to update the lock file
68
+ * 4. Updates the NodeResolutionResult marker with new dependency info
69
+ *
70
+ * If the dependency already exists in any scope, no changes are made.
71
+ * This matches the behavior of org.openrewrite.maven.AddDependency.
72
+ */
73
+ export class AddDependency extends ScanningRecipe<Accumulator> {
74
+ readonly name = "org.openrewrite.javascript.dependencies.add-dependency";
75
+ readonly displayName = "Add npm dependency";
76
+ readonly description = "Adds a new dependency to `package.json` and updates the lock file by running the package manager.";
77
+
78
+ @Option({
79
+ displayName: "Package name",
80
+ description: "The name of the npm package to add (e.g., `lodash`, `@types/node`)",
81
+ example: "lodash"
82
+ })
83
+ packageName!: string;
84
+
85
+ @Option({
86
+ displayName: "Version",
87
+ description: "The version constraint to set (e.g., `^5.0.0`, `~2.1.0`, `3.0.0`)",
88
+ example: "^5.0.0"
89
+ })
90
+ version!: string;
91
+
92
+ @Option({
93
+ displayName: "Scope",
94
+ description: "The dependency scope: `dependencies`, `devDependencies`, `peerDependencies`, or `optionalDependencies`",
95
+ example: "dependencies",
96
+ required: false
97
+ })
98
+ scope?: DependencyScope;
99
+
100
+ initialValue(_ctx: ExecutionContext): Accumulator {
101
+ return createDependencyRecipeAccumulator();
102
+ }
103
+
104
+ private getTargetScope(): DependencyScope {
105
+ return this.scope ?? 'dependencies';
106
+ }
107
+
108
+ async scanner(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
109
+ const recipe = this;
110
+
111
+ return new class extends JsonVisitor<ExecutionContext> {
112
+ protected async visitDocument(doc: Json.Document, _ctx: ExecutionContext): Promise<Json | undefined> {
113
+ // Only process package.json files
114
+ if (!doc.sourcePath.endsWith('package.json')) {
115
+ return doc;
116
+ }
117
+
118
+ const marker = findNodeResolutionResult(doc);
119
+ if (!marker) {
120
+ return doc;
121
+ }
122
+
123
+ // Check if dependency already exists in any scope
124
+ for (const scope of allDependencyScopes) {
125
+ const deps = marker[scope];
126
+ if (deps?.some(d => d.name === recipe.packageName)) {
127
+ // Dependency already exists, don't add again
128
+ return doc;
129
+ }
130
+ }
131
+
132
+ // Get the project directory and package manager
133
+ const projectDir = path.dirname(path.resolve(doc.sourcePath));
134
+ const pm = marker.packageManager ?? PackageManager.Npm;
135
+
136
+ acc.projectsToUpdate.set(doc.sourcePath, {
137
+ projectDir,
138
+ packageJsonPath: doc.sourcePath,
139
+ originalPackageJson: await this.printDocument(doc),
140
+ dependencyScope: recipe.getTargetScope(),
141
+ newVersion: recipe.version,
142
+ packageManager: pm
143
+ });
144
+
145
+ return doc;
146
+ }
147
+
148
+ private async printDocument(doc: Json.Document): Promise<string> {
149
+ return TreePrinters.print(doc);
150
+ }
151
+ };
152
+ }
153
+
154
+ async editorWithData(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
155
+ const recipe = this;
156
+
157
+ return new class extends JsonVisitor<ExecutionContext> {
158
+ protected async visitDocument(doc: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
159
+ const sourcePath = doc.sourcePath;
160
+
161
+ // Handle package.json files
162
+ if (sourcePath.endsWith('package.json')) {
163
+ const updateInfo = acc.projectsToUpdate.get(sourcePath);
164
+ if (!updateInfo) {
165
+ return doc; // This package.json doesn't need updating
166
+ }
167
+
168
+ // Run package manager install if needed, check for failure
169
+ const failureMessage = await runInstallIfNeeded(sourcePath, acc, () =>
170
+ recipe.runPackageManagerInstall(acc, updateInfo, ctx)
171
+ );
172
+ if (failureMessage) {
173
+ return markupWarn(
174
+ doc,
175
+ `Failed to add ${recipe.packageName} to ${recipe.version}`,
176
+ failureMessage
177
+ );
178
+ }
179
+
180
+ // Add the dependency to the JSON AST (preserves formatting)
181
+ const visitor = new AddDependencyVisitor(
182
+ recipe.packageName,
183
+ recipe.version,
184
+ updateInfo.dependencyScope
185
+ );
186
+ const modifiedDoc = await visitor.visit(doc, undefined) as Json.Document;
187
+
188
+ // Update the NodeResolutionResult marker
189
+ return updateNodeResolutionMarker(modifiedDoc, updateInfo, acc);
190
+ }
191
+
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;
199
+ }
200
+
201
+ return doc;
202
+ }
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Runs the package manager in a temporary directory to update the lock file.
208
+ */
209
+ private async runPackageManagerInstall(
210
+ acc: Accumulator,
211
+ updateInfo: ProjectUpdateInfo,
212
+ _ctx: ExecutionContext
213
+ ): Promise<void> {
214
+ // Create modified package.json with the new dependency
215
+ const modifiedPackageJson = this.createModifiedPackageJson(
216
+ updateInfo.originalPackageJson,
217
+ updateInfo.dependencyScope
218
+ );
219
+
220
+ const result = await runInstallInTempDir(
221
+ updateInfo.projectDir,
222
+ updateInfo.packageManager,
223
+ modifiedPackageJson
224
+ );
225
+
226
+ storeInstallResult(result, acc, updateInfo, modifiedPackageJson);
227
+ }
228
+
229
+ /**
230
+ * Creates a modified package.json with the new dependency added.
231
+ */
232
+ private createModifiedPackageJson(
233
+ originalContent: string,
234
+ scope: DependencyScope
235
+ ): string {
236
+ const packageJson = JSON.parse(originalContent);
237
+
238
+ if (!packageJson[scope]) {
239
+ packageJson[scope] = {};
240
+ }
241
+
242
+ packageJson[scope][this.packageName] = this.version;
243
+
244
+ return JSON.stringify(packageJson, null, 2);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Visitor that adds a new dependency to a specific scope in package.json.
250
+ * If the scope doesn't exist, it creates it.
251
+ */
252
+ class AddDependencyVisitor extends JsonVisitor<void> {
253
+ private readonly packageName: string;
254
+ private readonly version: string;
255
+ private readonly targetScope: DependencyScope;
256
+ private scopeFound = false;
257
+ private dependencyAdded = false;
258
+ private baseIndent: string = ' '; // Will be detected from document
259
+
260
+ constructor(packageName: string, version: string, targetScope: DependencyScope) {
261
+ super();
262
+ this.packageName = packageName;
263
+ this.version = version;
264
+ this.targetScope = targetScope;
265
+ }
266
+
267
+ protected async visitDocument(doc: Json.Document, p: void): Promise<Json | undefined> {
268
+ // Detect indentation from the document
269
+ this.baseIndent = detectIndent(doc);
270
+
271
+ const result = await super.visitDocument(doc, p) as Json.Document;
272
+
273
+ // If scope wasn't found, we need to add it to the document
274
+ if (!this.scopeFound && !this.dependencyAdded) {
275
+ return this.addScopeToDocument(result);
276
+ }
277
+
278
+ return result;
279
+ }
280
+
281
+ protected async visitMember(member: Json.Member, p: void): Promise<Json | undefined> {
282
+ const keyName = getMemberKeyName(member);
283
+
284
+ if (keyName === this.targetScope) {
285
+ this.scopeFound = true;
286
+ return this.addDependencyToScope(member);
287
+ }
288
+
289
+ return super.visitMember(member, p);
290
+ }
291
+
292
+ /**
293
+ * Adds the dependency to an existing scope object.
294
+ */
295
+ private addDependencyToScope(scopeMember: Json.Member): Json.Member {
296
+ const value = scopeMember.value;
297
+
298
+ if (!isObject(value)) {
299
+ return scopeMember;
300
+ }
301
+
302
+ const members = [...(value.members || [])];
303
+
304
+ // Determine the closing whitespace (goes before closing brace after last element)
305
+ let closingWhitespace = '\n ';
306
+ if (members.length > 0) {
307
+ const lastMember = members[members.length - 1];
308
+ closingWhitespace = lastMember.after.whitespace;
309
+ // Update the last member's after to be empty (comma will be added by printer)
310
+ members[members.length - 1] = {
311
+ ...lastMember,
312
+ after: space('')
313
+ };
314
+ }
315
+
316
+ const newMember = this.createDependencyMemberWithAfter(closingWhitespace);
317
+ this.dependencyAdded = true;
318
+
319
+ members.push(newMember);
320
+
321
+ return {
322
+ ...scopeMember,
323
+ value: {
324
+ ...value,
325
+ members
326
+ } as Json.Object
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Creates a new dependency member node.
332
+ */
333
+ private createDependencyMemberWithAfter(afterWhitespace: string): Json.RightPadded<Json.Member> {
334
+ // Dependencies inside a scope are indented twice (e.g., 8 spaces if base is 4)
335
+ const depIndent = this.baseIndent + this.baseIndent;
336
+
337
+ const keyLiteral: Json.Literal = {
338
+ kind: Json.Kind.Literal,
339
+ id: randomId(),
340
+ prefix: space('\n' + depIndent),
341
+ markers: emptyMarkers,
342
+ source: `"${this.packageName}"`,
343
+ value: this.packageName
344
+ };
345
+
346
+ const valueLiteral: Json.Literal = {
347
+ kind: Json.Kind.Literal,
348
+ id: randomId(),
349
+ prefix: space(' '),
350
+ markers: emptyMarkers,
351
+ source: `"${this.version}"`,
352
+ value: this.version
353
+ };
354
+
355
+ const member: Json.Member = {
356
+ kind: Json.Kind.Member,
357
+ id: randomId(),
358
+ prefix: space(''),
359
+ markers: emptyMarkers,
360
+ key: rightPadded(keyLiteral, space('')),
361
+ value: valueLiteral
362
+ };
363
+
364
+ return rightPadded(member, space(afterWhitespace));
365
+ }
366
+
367
+ /**
368
+ * Adds a new scope section to the document when the target scope doesn't exist.
369
+ */
370
+ private addScopeToDocument(doc: Json.Document): Json.Document {
371
+ const docValue = doc.value;
372
+
373
+ if (!isObject(docValue)) {
374
+ return doc;
375
+ }
376
+
377
+ const members = [...(docValue.members || [])];
378
+
379
+ // Get the trailing whitespace from the last member
380
+ let closingWhitespace = '\n';
381
+ if (members.length > 0) {
382
+ const lastMember = members[members.length - 1];
383
+ closingWhitespace = lastMember.after.whitespace;
384
+ // Update the last member's after to be empty (comma will be added by printer)
385
+ members[members.length - 1] = {
386
+ ...lastMember,
387
+ after: space('')
388
+ };
389
+ }
390
+
391
+ const scopeMember = this.createScopeMemberWithAfter(closingWhitespace);
392
+ this.dependencyAdded = true;
393
+
394
+ members.push(scopeMember);
395
+
396
+ return {
397
+ ...doc,
398
+ value: {
399
+ ...docValue,
400
+ members
401
+ } as Json.Object
402
+ };
403
+ }
404
+
405
+ /**
406
+ * Creates a new scope member with the dependency inside.
407
+ */
408
+ private createScopeMemberWithAfter(afterWhitespace: string): Json.RightPadded<Json.Member> {
409
+ // Dependencies inside a scope are indented twice (e.g., 8 spaces if base is 4)
410
+ const depIndent = this.baseIndent + this.baseIndent;
411
+
412
+ const keyLiteral: Json.Literal = {
413
+ kind: Json.Kind.Literal,
414
+ id: randomId(),
415
+ prefix: space('\n' + this.baseIndent),
416
+ markers: emptyMarkers,
417
+ source: `"${this.targetScope}"`,
418
+ value: this.targetScope
419
+ };
420
+
421
+ const depKeyLiteral: Json.Literal = {
422
+ kind: Json.Kind.Literal,
423
+ id: randomId(),
424
+ prefix: space('\n' + depIndent),
425
+ markers: emptyMarkers,
426
+ source: `"${this.packageName}"`,
427
+ value: this.packageName
428
+ };
429
+
430
+ const depValueLiteral: Json.Literal = {
431
+ kind: Json.Kind.Literal,
432
+ id: randomId(),
433
+ prefix: space(' '),
434
+ markers: emptyMarkers,
435
+ source: `"${this.version}"`,
436
+ value: this.version
437
+ };
438
+
439
+ const depMember: Json.Member = {
440
+ kind: Json.Kind.Member,
441
+ id: randomId(),
442
+ prefix: space(''),
443
+ markers: emptyMarkers,
444
+ key: rightPadded(depKeyLiteral, space('')),
445
+ value: depValueLiteral
446
+ };
447
+
448
+ const scopeObject: Json.Object = {
449
+ kind: Json.Kind.Object,
450
+ id: randomId(),
451
+ prefix: space(' '),
452
+ markers: emptyMarkers,
453
+ members: [rightPadded(depMember, space('\n' + this.baseIndent))]
454
+ };
455
+
456
+ const scopeMemberNode: Json.Member = {
457
+ kind: Json.Kind.Member,
458
+ id: randomId(),
459
+ prefix: space(''),
460
+ markers: emptyMarkers,
461
+ key: rightPadded(keyLiteral, space('')),
462
+ value: scopeObject
463
+ };
464
+
465
+ return rightPadded(scopeMemberNode, space(afterWhitespace));
466
+ }
467
+ }
@@ -14,6 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
+ export * from "./add-dependency";
17
18
  export * from "./async-callback-in-sync-array-method";
18
19
  export * from "./auto-format";
19
20
  export * from "./upgrade-dependency-version";