@openrewrite/rewrite 8.66.0-20251027-095903 → 8.66.0-20251027-133754

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.
@@ -0,0 +1,871 @@
1
+ import {JavaScriptVisitor} from "./visitor";
2
+ import {J, emptySpace, rightPadded, space, Statement, singleSpace, Type} from "../java";
3
+ import {JS} from "./tree";
4
+ import {randomId} from "../uuid";
5
+ import {emptyMarkers, markers} from "../markers";
6
+ import {ExecutionContext} from "../execution";
7
+
8
+ export enum ImportStyle {
9
+ ES6Named, // import { x } from 'module'
10
+ ES6Namespace, // import * as x from 'module'
11
+ ES6Default, // import x from 'module'
12
+ CommonJS // const x = require('module')
13
+ }
14
+
15
+ export interface AddImportOptions {
16
+ /** The module name (e.g., 'fs') to import from */
17
+ target: string;
18
+
19
+ /** Optionally, the specific member to import from the module.
20
+ * If not specified, adds a default import or namespace import */
21
+ member?: string;
22
+
23
+ /** Optional alias for the imported member */
24
+ alias?: string;
25
+
26
+ /** If true, only add the import if the member is actually used in the file. Default: true */
27
+ onlyIfReferenced?: boolean;
28
+
29
+ /** Optional import style to use. If not specified, auto-detects from file and existing imports */
30
+ style?: ImportStyle;
31
+ }
32
+
33
+ /**
34
+ * Register an AddImport visitor to add an import statement to a JavaScript/TypeScript file
35
+ * @param visitor The visitor to add the import addition to
36
+ * @param options Configuration options for the import to add
37
+ */
38
+ export function maybeAddImport(
39
+ visitor: JavaScriptVisitor<any>,
40
+ options: AddImportOptions
41
+ ) {
42
+ for (const v of visitor.afterVisit || []) {
43
+ if (v instanceof AddImport &&
44
+ v.target === options.target &&
45
+ v.member === options.member &&
46
+ v.alias === options.alias) {
47
+ return;
48
+ }
49
+ }
50
+ visitor.afterVisit.push(new AddImport(options));
51
+ }
52
+
53
+ export class AddImport<P> extends JavaScriptVisitor<P> {
54
+ readonly target: string;
55
+ readonly member?: string;
56
+ readonly alias?: string;
57
+ readonly onlyIfReferenced: boolean;
58
+ readonly style?: ImportStyle;
59
+
60
+ constructor(options: AddImportOptions) {
61
+ super();
62
+ this.target = options.target;
63
+ this.member = options.member;
64
+ this.alias = options.alias;
65
+ this.onlyIfReferenced = options.onlyIfReferenced ?? true;
66
+ this.style = options.style;
67
+ }
68
+
69
+ /**
70
+ * Extract module name from a module specifier literal
71
+ */
72
+ private getModuleName(moduleSpecifier: J): string | undefined {
73
+ if (moduleSpecifier.kind !== J.Kind.Literal) {
74
+ return undefined;
75
+ }
76
+ return (moduleSpecifier as J.Literal).value?.toString();
77
+ }
78
+
79
+ /**
80
+ * Check if a method invocation is a require() call
81
+ */
82
+ private isRequireCall(methodInv: J.MethodInvocation): boolean {
83
+ return methodInv.name?.kind === J.Kind.Identifier &&
84
+ (methodInv.name as J.Identifier).simpleName === 'require';
85
+ }
86
+
87
+ /**
88
+ * Determine the appropriate import style based on file type and existing imports
89
+ */
90
+ private determineImportStyle(compilationUnit: JS.CompilationUnit): ImportStyle {
91
+ // If style was explicitly provided, use it
92
+ if (this.style !== undefined) {
93
+ return this.style;
94
+ }
95
+
96
+ // Check the file extension from sourcePath
97
+ const sourcePath = compilationUnit.sourcePath;
98
+ const isTypeScript = sourcePath.endsWith('.ts') ||
99
+ sourcePath.endsWith('.tsx') ||
100
+ sourcePath.endsWith('.mts') ||
101
+ sourcePath.endsWith('.cts');
102
+
103
+ // Check for .cjs extension - must use CommonJS
104
+ if (sourcePath.endsWith('.cjs')) {
105
+ return ImportStyle.CommonJS;
106
+ }
107
+
108
+ // First, check if there's already an import from the same module
109
+ // and match that style
110
+ const existingStyleForModule = this.findExistingImportStyleForModule(compilationUnit);
111
+ if (existingStyleForModule !== null) {
112
+ return existingStyleForModule;
113
+ }
114
+
115
+ // For .mjs or TypeScript, prefer ES6
116
+ if (sourcePath.endsWith('.mjs') || isTypeScript) {
117
+ // If we're importing a member, use named imports
118
+ if (this.member !== undefined) {
119
+ return ImportStyle.ES6Named;
120
+ }
121
+ // Otherwise default import
122
+ return ImportStyle.ES6Default;
123
+ }
124
+
125
+ // For .js files, check what style is predominantly being used
126
+ let hasNamedImports = false;
127
+ let hasNamespaceImports = false;
128
+ let hasDefaultImports = false;
129
+ let hasCommonJSRequires = false;
130
+
131
+ for (const stmt of compilationUnit.statements) {
132
+ const statement = stmt.element;
133
+
134
+ // Check for ES6 imports
135
+ if (statement?.kind === JS.Kind.Import) {
136
+ const jsImport = statement as JS.Import;
137
+ const importClause = jsImport.importClause;
138
+
139
+ if (importClause) {
140
+ // Check for named bindings
141
+ if (importClause.namedBindings) {
142
+ if (importClause.namedBindings.kind === JS.Kind.NamedImports) {
143
+ hasNamedImports = true;
144
+ } else if (importClause.namedBindings.kind === J.Kind.Identifier ||
145
+ importClause.namedBindings.kind === JS.Kind.Alias) {
146
+ // import * as x from 'module'
147
+ hasNamespaceImports = true;
148
+ }
149
+ }
150
+
151
+ // Check for default import
152
+ if (importClause.name) {
153
+ hasDefaultImports = true;
154
+ }
155
+ }
156
+ }
157
+
158
+ // Check for CommonJS requires
159
+ if (statement?.kind === J.Kind.VariableDeclarations) {
160
+ const varDecl = statement as J.VariableDeclarations;
161
+ if (varDecl.variables.length === 1) {
162
+ const namedVar = varDecl.variables[0].element;
163
+ const initializer = namedVar?.initializer?.element;
164
+ if (initializer?.kind === J.Kind.MethodInvocation &&
165
+ this.isRequireCall(initializer as J.MethodInvocation)) {
166
+ hasCommonJSRequires = true;
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ // Prefer matching the predominant style
173
+ // If file uses CommonJS, stick with it
174
+ if (hasCommonJSRequires && !hasNamedImports && !hasNamespaceImports && !hasDefaultImports) {
175
+ return ImportStyle.CommonJS;
176
+ }
177
+
178
+ // If importing a member, prefer named imports if they exist in the file
179
+ if (this.member !== undefined) {
180
+ if (hasNamedImports) {
181
+ return ImportStyle.ES6Named;
182
+ }
183
+ if (hasNamespaceImports) {
184
+ return ImportStyle.ES6Namespace;
185
+ }
186
+ }
187
+
188
+ // For default/whole module imports
189
+ if (this.member === undefined) {
190
+ if (hasNamespaceImports) {
191
+ return ImportStyle.ES6Namespace;
192
+ }
193
+ if (hasDefaultImports) {
194
+ return ImportStyle.ES6Default;
195
+ }
196
+ }
197
+
198
+ // Default to named imports for members, default imports for modules
199
+ return this.member !== undefined ? ImportStyle.ES6Named : ImportStyle.ES6Default;
200
+ }
201
+
202
+ /**
203
+ * Find the import style used for an existing import from the same module
204
+ */
205
+ private findExistingImportStyleForModule(compilationUnit: JS.CompilationUnit): ImportStyle | null {
206
+ for (const stmt of compilationUnit.statements) {
207
+ const statement = stmt.element;
208
+
209
+ // Check ES6 imports
210
+ if (statement?.kind === JS.Kind.Import) {
211
+ const jsImport = statement as JS.Import;
212
+ const moduleSpecifier = jsImport.moduleSpecifier?.element;
213
+
214
+ if (moduleSpecifier) {
215
+ const moduleName = this.getModuleName(moduleSpecifier);
216
+
217
+ if (moduleName === this.target) {
218
+ const importClause = jsImport.importClause;
219
+ if (importClause?.namedBindings) {
220
+ if (importClause.namedBindings.kind === JS.Kind.NamedImports) {
221
+ return ImportStyle.ES6Named;
222
+ } else {
223
+ return ImportStyle.ES6Namespace;
224
+ }
225
+ }
226
+ if (importClause?.name) {
227
+ return ImportStyle.ES6Default;
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ // Check CommonJS requires
234
+ if (statement?.kind === J.Kind.VariableDeclarations) {
235
+ const varDecl = statement as J.VariableDeclarations;
236
+ if (varDecl.variables.length === 1) {
237
+ const namedVar = varDecl.variables[0].element;
238
+ const initializer = namedVar?.initializer?.element;
239
+
240
+ if (initializer?.kind === J.Kind.MethodInvocation &&
241
+ this.isRequireCall(initializer as J.MethodInvocation)) {
242
+ const moduleName = this.getModuleNameFromRequire(initializer as J.MethodInvocation);
243
+ if (moduleName === this.target) {
244
+ return ImportStyle.CommonJS;
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+
251
+ return null;
252
+ }
253
+
254
+ override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, p: P): Promise<J | undefined> {
255
+ // First, check if the import already exists
256
+ const hasImport = await this.checkImportExists(compilationUnit);
257
+ if (hasImport) {
258
+ return compilationUnit;
259
+ }
260
+
261
+ // If onlyIfReferenced is true, check if the identifier is actually used
262
+ if (this.onlyIfReferenced) {
263
+ const isReferenced = await this.checkIdentifierReferenced(compilationUnit);
264
+ if (!isReferenced) {
265
+ return compilationUnit;
266
+ }
267
+ }
268
+
269
+ // Determine the appropriate import style
270
+ const importStyle = this.determineImportStyle(compilationUnit);
271
+
272
+ // For ES6 named imports, check if we can merge into an existing import from the same module
273
+ if (importStyle === ImportStyle.ES6Named && this.member !== undefined) {
274
+ const mergedCu = await this.tryMergeIntoExistingImport(compilationUnit, p);
275
+ if (mergedCu !== compilationUnit) {
276
+ return mergedCu;
277
+ }
278
+ }
279
+
280
+ // Add the import using the appropriate style
281
+ if (importStyle === ImportStyle.CommonJS) {
282
+ // TODO: Implement CommonJS require creation
283
+ // For now, fall back to ES6 imports
284
+ // return this.addCommonJSRequire(compilationUnit, p);
285
+ }
286
+
287
+ // Add ES6 import (handles ES6Named, ES6Namespace, ES6Default)
288
+ return this.produceJavaScript<JS.CompilationUnit>(compilationUnit, p, async draft => {
289
+ // Find the position to insert the import
290
+ const insertionIndex = this.findImportInsertionIndex(compilationUnit);
291
+
292
+ const newImport = await this.createImportStatement(compilationUnit, insertionIndex, p);
293
+
294
+ // Insert the import at the appropriate position
295
+ // Create semicolon marker for the import statement
296
+ const semicolonMarkers = markers({
297
+ kind: J.Markers.Semicolon,
298
+ id: randomId()
299
+ });
300
+
301
+ if (insertionIndex === 0) {
302
+ // Insert at the beginning
303
+ // The `after` space should be empty since semicolon is printed after it
304
+ // The spacing comes from updating the next statement's prefix
305
+ const updatedStatements = compilationUnit.statements.length > 0
306
+ ? [
307
+ rightPadded(newImport, emptySpace, semicolonMarkers),
308
+ {
309
+ ...compilationUnit.statements[0],
310
+ element: compilationUnit.statements[0].element
311
+ ? {...compilationUnit.statements[0].element, prefix: space("\n\n")}
312
+ : undefined
313
+ } as J.RightPadded<Statement>,
314
+ ...compilationUnit.statements.slice(1)
315
+ ]
316
+ : [rightPadded(newImport, emptySpace, semicolonMarkers)];
317
+
318
+ draft.statements = updatedStatements;
319
+ } else {
320
+ // Insert after existing imports
321
+ const before = compilationUnit.statements.slice(0, insertionIndex);
322
+ const after = compilationUnit.statements.slice(insertionIndex);
323
+
324
+ //The `after` space is empty, spacing comes from next statement's prefix
325
+ // Ensure the next statement has at least one newline in its prefix
326
+ if (after.length > 0 && after[0].element) {
327
+ const currentPrefix = after[0].element.prefix;
328
+ const needsNewline = !currentPrefix.whitespace.includes('\n');
329
+
330
+ const updatedNextStatement = needsNewline ? {
331
+ ...after[0],
332
+ element: {...after[0].element, prefix: space("\n" + currentPrefix.whitespace)}
333
+ } : after[0];
334
+
335
+ draft.statements = [
336
+ ...before,
337
+ rightPadded(newImport, emptySpace, semicolonMarkers),
338
+ updatedNextStatement,
339
+ ...after.slice(1)
340
+ ];
341
+ } else {
342
+ draft.statements = [
343
+ ...before,
344
+ rightPadded(newImport, emptySpace, semicolonMarkers),
345
+ ...after
346
+ ];
347
+ }
348
+ }
349
+ });
350
+ }
351
+
352
+ /**
353
+ * Try to merge the new member into an existing import from the same module
354
+ */
355
+ private async tryMergeIntoExistingImport(compilationUnit: JS.CompilationUnit, p: P): Promise<JS.CompilationUnit> {
356
+ for (let i = 0; i < compilationUnit.statements.length; i++) {
357
+ const stmt = compilationUnit.statements[i];
358
+ const statement = stmt.element;
359
+
360
+ if (statement?.kind === JS.Kind.Import) {
361
+ const jsImport = statement as JS.Import;
362
+ const moduleSpecifier = jsImport.moduleSpecifier?.element;
363
+
364
+ if (!moduleSpecifier) {
365
+ continue;
366
+ }
367
+
368
+ const moduleName = this.getModuleName(moduleSpecifier);
369
+
370
+ // Check if this is an import from our target module
371
+ if (moduleName !== this.target) {
372
+ continue;
373
+ }
374
+
375
+ const importClause = jsImport.importClause;
376
+ if (!importClause || !importClause.namedBindings) {
377
+ continue;
378
+ }
379
+
380
+ // Only merge into NamedImports, not namespace imports
381
+ if (importClause.namedBindings.kind !== JS.Kind.NamedImports) {
382
+ continue;
383
+ }
384
+
385
+ // We found a matching import with named bindings - merge into it
386
+ return this.produceJavaScript<JS.CompilationUnit>(compilationUnit, p, async draft => {
387
+ const namedImports = importClause.namedBindings as JS.NamedImports;
388
+
389
+ // Create the new specifier with a space prefix (since it's not the first element)
390
+ const newSpecifierBase = this.createImportSpecifier();
391
+ const newSpecifier = {...newSpecifierBase, prefix: singleSpace};
392
+
393
+ // Add the new specifier to the elements
394
+ const updatedNamedImports: JS.NamedImports = await this.produceJavaScript<JS.NamedImports>(
395
+ namedImports, p, async namedDraft => {
396
+ namedDraft.elements = {
397
+ ...namedImports.elements,
398
+ elements: [
399
+ ...namedImports.elements.elements,
400
+ rightPadded(newSpecifier, emptySpace)
401
+ ]
402
+ };
403
+ }
404
+ );
405
+
406
+ // Update the import with the new named imports
407
+ const updatedImport: JS.Import = await this.produceJavaScript<JS.Import>(
408
+ jsImport, p, async importDraft => {
409
+ importDraft.importClause = await this.produceJavaScript<JS.ImportClause>(
410
+ importClause, p, async clauseDraft => {
411
+ clauseDraft.namedBindings = updatedNamedImports;
412
+ }
413
+ );
414
+ }
415
+ );
416
+
417
+ // Replace the statement in the compilation unit
418
+ draft.statements = compilationUnit.statements.map((s, idx) =>
419
+ idx === i ? {...s, element: updatedImport} : s
420
+ );
421
+ });
422
+ }
423
+ }
424
+
425
+ return compilationUnit;
426
+ }
427
+
428
+ /**
429
+ * Check if the import already exists in the compilation unit
430
+ */
431
+ private async checkImportExists(compilationUnit: JS.CompilationUnit): Promise<boolean> {
432
+ for (const stmt of compilationUnit.statements) {
433
+ const statement = stmt.element;
434
+
435
+ // Check ES6 imports
436
+ if (statement?.kind === JS.Kind.Import) {
437
+ const jsImport = statement as JS.Import;
438
+ if (this.isMatchingImport(jsImport)) {
439
+ return true;
440
+ }
441
+ }
442
+
443
+ // Check CommonJS require statements
444
+ if (statement?.kind === J.Kind.VariableDeclarations) {
445
+ const varDecl = statement as J.VariableDeclarations;
446
+ if (this.isMatchingRequire(varDecl)) {
447
+ return true;
448
+ }
449
+ }
450
+ }
451
+ return false;
452
+ }
453
+
454
+ /**
455
+ * Check if the import matches what we're trying to add
456
+ */
457
+ private isMatchingImport(jsImport: JS.Import): boolean {
458
+ // Check module specifier
459
+ const moduleSpecifier = jsImport.moduleSpecifier?.element;
460
+ if (!moduleSpecifier) {
461
+ return false;
462
+ }
463
+
464
+ const moduleName = this.getModuleName(moduleSpecifier);
465
+ if (moduleName !== this.target) {
466
+ return false;
467
+ }
468
+
469
+ const importClause = jsImport.importClause;
470
+ if (!importClause) {
471
+ return false;
472
+ }
473
+
474
+ // Check if the specific member or default import already exists
475
+ if (this.member === undefined) {
476
+ // We're adding a default import, check if one exists
477
+ return importClause.name !== undefined;
478
+ } else {
479
+ // We're adding a named import, check if it exists
480
+ const namedBindings = importClause.namedBindings;
481
+ if (!namedBindings) {
482
+ return false;
483
+ }
484
+
485
+ if (namedBindings.kind === JS.Kind.NamedImports) {
486
+ const namedImports = namedBindings as JS.NamedImports;
487
+ for (const elem of namedImports.elements.elements) {
488
+ if (elem.element?.kind === JS.Kind.ImportSpecifier) {
489
+ const specifier = elem.element as JS.ImportSpecifier;
490
+ const importName = this.getImportName(specifier);
491
+ const aliasName = this.getImportAlias(specifier);
492
+
493
+ if (importName === this.member && aliasName === this.alias) {
494
+ return true;
495
+ }
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ return false;
502
+ }
503
+
504
+ /**
505
+ * Check if this is a matching CommonJS require statement
506
+ */
507
+ private isMatchingRequire(varDecl: J.VariableDeclarations): boolean {
508
+ if (varDecl.variables.length !== 1) {
509
+ return false;
510
+ }
511
+
512
+ const namedVar = varDecl.variables[0].element;
513
+ if (!namedVar) {
514
+ return false;
515
+ }
516
+
517
+ const initializer = namedVar.initializer?.element;
518
+ if (!initializer || initializer.kind !== J.Kind.MethodInvocation) {
519
+ return false;
520
+ }
521
+
522
+ const methodInv = initializer as J.MethodInvocation;
523
+ if (!this.isRequireCall(methodInv)) {
524
+ return false;
525
+ }
526
+
527
+ const moduleName = this.getModuleNameFromRequire(methodInv);
528
+ if (moduleName !== this.target) {
529
+ return false;
530
+ }
531
+
532
+ // Check if the variable name matches what we're trying to add
533
+ const pattern = namedVar.name;
534
+ if (this.member === undefined && pattern?.kind === J.Kind.Identifier) {
535
+ // Default import style: const fs = require('fs')
536
+ return true;
537
+ } else if (this.member !== undefined && pattern?.kind === JS.Kind.ObjectBindingPattern) {
538
+ // Destructured import: const { member } = require('module')
539
+ const objectPattern = pattern as JS.ObjectBindingPattern;
540
+ for (const elem of objectPattern.bindings.elements) {
541
+ if (elem.element?.kind === JS.Kind.BindingElement) {
542
+ const bindingElem = elem.element as JS.BindingElement;
543
+ const name = (bindingElem.name as J.Identifier)?.simpleName;
544
+ if (name === (this.alias || this.member)) {
545
+ return true;
546
+ }
547
+ }
548
+ }
549
+ }
550
+
551
+ return false;
552
+ }
553
+
554
+ /**
555
+ * Check if the identifier is actually referenced in the file
556
+ */
557
+ private async checkIdentifierReferenced(compilationUnit: JS.CompilationUnit): Promise<boolean> {
558
+ // Use type attribution to detect if the identifier is referenced
559
+ // Map of module name -> Set of member names used from that module
560
+ const usedImports = new Map<string, Set<string>>();
561
+
562
+ // Helper to record usage of a method from a module
563
+ const recordMethodUsage = (methodType: Type.Method) => {
564
+ const moduleName = Type.FullyQualified.getFullyQualifiedName(methodType.declaringType);
565
+ if (moduleName) {
566
+ if (!usedImports.has(moduleName)) {
567
+ usedImports.set(moduleName, new Set());
568
+ }
569
+ usedImports.get(moduleName)!.add(methodType.name);
570
+ }
571
+ };
572
+
573
+ // Create a visitor to collect used identifiers with their type attribution
574
+ const collector = new class extends JavaScriptVisitor<ExecutionContext> {
575
+ override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
576
+ const type = identifier.type;
577
+ if (type && Type.isMethod(type)) {
578
+ recordMethodUsage(type as Type.Method);
579
+ }
580
+ return super.visitIdentifier(identifier, p);
581
+ }
582
+
583
+ override async visitMethodInvocation(methodInvocation: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
584
+ if (methodInvocation.methodType) {
585
+ recordMethodUsage(methodInvocation.methodType);
586
+ }
587
+ return super.visitMethodInvocation(methodInvocation, p);
588
+ }
589
+
590
+ override async visitFunctionCall(functionCall: JS.FunctionCall, p: ExecutionContext): Promise<J | undefined> {
591
+ if (functionCall.methodType) {
592
+ recordMethodUsage(functionCall.methodType);
593
+ }
594
+ return super.visitFunctionCall(functionCall, p);
595
+ }
596
+
597
+ override async visitFieldAccess(fieldAccess: J.FieldAccess, p: ExecutionContext): Promise<J | undefined> {
598
+ const type = fieldAccess.type;
599
+ if (type && Type.isMethod(type)) {
600
+ recordMethodUsage(type as Type.Method);
601
+ }
602
+ return super.visitFieldAccess(fieldAccess, p);
603
+ }
604
+ };
605
+
606
+ await collector.visit(compilationUnit, new ExecutionContext());
607
+
608
+ // Check if our target import is used based on type attribution
609
+ const moduleMembers = usedImports.get(this.target);
610
+ if (!moduleMembers) {
611
+ return false;
612
+ }
613
+
614
+ // For specific members, check if that member is used; otherwise check if any member is used
615
+ return this.member ? moduleMembers.has(this.member) : moduleMembers.size > 0;
616
+ }
617
+
618
+ /**
619
+ * Create a new import statement
620
+ */
621
+ private async createImportStatement(compilationUnit: JS.CompilationUnit, insertionIndex: number, p: P): Promise<JS.Import> {
622
+ // Determine the appropriate prefix (spacing before the import)
623
+ const prefix = this.determineImportPrefix(compilationUnit, insertionIndex);
624
+
625
+ // Create the module specifier
626
+ const moduleSpecifier: J.Literal = {
627
+ id: randomId(),
628
+ kind: J.Kind.Literal,
629
+ prefix: singleSpace,
630
+ markers: emptyMarkers,
631
+ value: `'${this.target}'`,
632
+ valueSource: `'${this.target}'`,
633
+ unicodeEscapes: [],
634
+ type: undefined
635
+ };
636
+
637
+ let importClause: JS.ImportClause | undefined;
638
+
639
+ if (this.member === undefined) {
640
+ // Default import: import target from 'module'
641
+ const defaultName: J.Identifier = {
642
+ id: randomId(),
643
+ kind: J.Kind.Identifier,
644
+ prefix: singleSpace,
645
+ markers: emptyMarkers,
646
+ annotations: [],
647
+ simpleName: this.alias || this.target,
648
+ type: undefined,
649
+ fieldType: undefined
650
+ };
651
+
652
+ importClause = {
653
+ id: randomId(),
654
+ kind: JS.Kind.ImportClause,
655
+ prefix: emptySpace,
656
+ markers: emptyMarkers,
657
+ typeOnly: false,
658
+ name: rightPadded(defaultName, emptySpace),
659
+ namedBindings: undefined
660
+ };
661
+ } else {
662
+ // Named import: import { member } from 'module'
663
+ const importSpec = this.createImportSpecifier();
664
+
665
+ const namedImports: JS.NamedImports = {
666
+ id: randomId(),
667
+ kind: JS.Kind.NamedImports,
668
+ prefix: singleSpace,
669
+ markers: emptyMarkers,
670
+ elements: {
671
+ kind: J.Kind.Container,
672
+ before: emptySpace,
673
+ elements: [rightPadded(importSpec, emptySpace)],
674
+ markers: emptyMarkers
675
+ }
676
+ };
677
+
678
+ importClause = {
679
+ id: randomId(),
680
+ kind: JS.Kind.ImportClause,
681
+ prefix: emptySpace,
682
+ markers: emptyMarkers,
683
+ typeOnly: false,
684
+ name: undefined,
685
+ namedBindings: namedImports
686
+ };
687
+ }
688
+
689
+ const jsImport: JS.Import = {
690
+ id: randomId(),
691
+ kind: JS.Kind.Import,
692
+ prefix,
693
+ markers: emptyMarkers,
694
+ modifiers: [],
695
+ importClause,
696
+ moduleSpecifier: {
697
+ kind: J.Kind.LeftPadded,
698
+ before: singleSpace,
699
+ element: moduleSpecifier,
700
+ markers: emptyMarkers
701
+ },
702
+ initializer: undefined
703
+ };
704
+
705
+ return jsImport;
706
+ }
707
+
708
+ /**
709
+ * Create an import specifier for a named import
710
+ */
711
+ private createImportSpecifier(): JS.ImportSpecifier {
712
+ let specifier: J.Identifier | JS.Alias;
713
+
714
+ if (this.alias) {
715
+ // Aliased import: import { member as alias } from 'module'
716
+ const propertyName: J.Identifier = {
717
+ id: randomId(),
718
+ kind: J.Kind.Identifier,
719
+ prefix: emptySpace,
720
+ markers: emptyMarkers,
721
+ annotations: [],
722
+ simpleName: this.member!,
723
+ type: undefined,
724
+ fieldType: undefined
725
+ };
726
+
727
+ const aliasName: J.Identifier = {
728
+ id: randomId(),
729
+ kind: J.Kind.Identifier,
730
+ prefix: singleSpace,
731
+ markers: emptyMarkers,
732
+ annotations: [],
733
+ simpleName: this.alias,
734
+ type: undefined,
735
+ fieldType: undefined
736
+ };
737
+
738
+ specifier = {
739
+ id: randomId(),
740
+ kind: JS.Kind.Alias,
741
+ prefix: emptySpace,
742
+ markers: emptyMarkers,
743
+ propertyName: rightPadded(propertyName, singleSpace),
744
+ alias: aliasName
745
+ };
746
+ } else {
747
+ // Regular import: import { member } from 'module'
748
+ specifier = {
749
+ id: randomId(),
750
+ kind: J.Kind.Identifier,
751
+ prefix: emptySpace,
752
+ markers: emptyMarkers,
753
+ annotations: [],
754
+ simpleName: this.member!,
755
+ type: undefined,
756
+ fieldType: undefined
757
+ };
758
+ }
759
+
760
+ return {
761
+ id: randomId(),
762
+ kind: JS.Kind.ImportSpecifier,
763
+ prefix: emptySpace,
764
+ markers: emptyMarkers,
765
+ importType: {
766
+ kind: J.Kind.LeftPadded,
767
+ before: emptySpace,
768
+ element: false,
769
+ markers: emptyMarkers
770
+ },
771
+ specifier
772
+ };
773
+ }
774
+
775
+ /**
776
+ * Determine the appropriate spacing before the import statement
777
+ */
778
+ private determineImportPrefix(compilationUnit: JS.CompilationUnit, insertionIndex: number): J.Space {
779
+ // If inserting at the beginning (index 0), use the prefix of the first statement
780
+ // but only the whitespace part (preserve comments on the original first statement)
781
+ if (insertionIndex === 0 && compilationUnit.statements.length > 0) {
782
+ const firstPrefix = compilationUnit.statements[0].element?.prefix;
783
+ if (firstPrefix) {
784
+ // Keep only whitespace, not comments
785
+ return {
786
+ kind: J.Kind.Space,
787
+ comments: [],
788
+ whitespace: firstPrefix.whitespace
789
+ };
790
+ }
791
+ return emptySpace;
792
+ }
793
+
794
+ // If inserting after other statements, ensure we have at least one newline
795
+ // to separate from the previous statement
796
+ return space("\n");
797
+ }
798
+
799
+ /**
800
+ * Find the index where the new import should be inserted
801
+ */
802
+ private findImportInsertionIndex(compilationUnit: JS.CompilationUnit): number {
803
+ let lastImportIndex = -1;
804
+
805
+ for (let i = 0; i < compilationUnit.statements.length; i++) {
806
+ const statement = compilationUnit.statements[i].element;
807
+ if (statement?.kind === JS.Kind.Import) {
808
+ lastImportIndex = i;
809
+ } else if (lastImportIndex >= 0) {
810
+ // We've found a non-import after imports, insert after the last import
811
+ return lastImportIndex + 1;
812
+ }
813
+ }
814
+
815
+ // If we found imports, insert after them
816
+ if (lastImportIndex >= 0) {
817
+ return lastImportIndex + 1;
818
+ }
819
+
820
+ // No imports found, insert at the beginning
821
+ return 0;
822
+ }
823
+
824
+ /**
825
+ * Get the module name from a require() call
826
+ */
827
+ private getModuleNameFromRequire(methodInv: J.MethodInvocation): string | undefined {
828
+ const args = methodInv.arguments?.elements;
829
+ if (!args || args.length === 0) {
830
+ return undefined;
831
+ }
832
+
833
+ const firstArg = args[0].element;
834
+ if (!firstArg || firstArg.kind !== J.Kind.Literal || typeof (firstArg as J.Literal).value !== 'string') {
835
+ return undefined;
836
+ }
837
+
838
+ return (firstArg as J.Literal).value?.toString();
839
+ }
840
+
841
+ /**
842
+ * Get the import name from an import specifier
843
+ */
844
+ private getImportName(specifier: JS.ImportSpecifier): string {
845
+ const spec = specifier.specifier;
846
+ if (spec?.kind === JS.Kind.Alias) {
847
+ const alias = spec as JS.Alias;
848
+ const propertyName = alias.propertyName.element;
849
+ if (propertyName?.kind === J.Kind.Identifier) {
850
+ return (propertyName as J.Identifier).simpleName;
851
+ }
852
+ } else if (spec?.kind === J.Kind.Identifier) {
853
+ return (spec as J.Identifier).simpleName;
854
+ }
855
+ return '';
856
+ }
857
+
858
+ /**
859
+ * Get the import alias from an import specifier
860
+ */
861
+ private getImportAlias(specifier: JS.ImportSpecifier): string | undefined {
862
+ const spec = specifier.specifier;
863
+ if (spec?.kind === JS.Kind.Alias) {
864
+ const alias = spec as JS.Alias;
865
+ if (alias.alias?.kind === J.Kind.Identifier) {
866
+ return (alias.alias as J.Identifier).simpleName;
867
+ }
868
+ }
869
+ return undefined;
870
+ }
871
+ }