@openrewrite/rewrite 8.68.0-20251124-203331 → 8.68.0-20251125-100455

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 (33) hide show
  1. package/dist/java/tree.d.ts +6 -1
  2. package/dist/java/tree.d.ts.map +1 -1
  3. package/dist/java/tree.js +13 -3
  4. package/dist/java/tree.js.map +1 -1
  5. package/dist/javascript/add-import.d.ts.map +1 -1
  6. package/dist/javascript/add-import.js +101 -19
  7. package/dist/javascript/add-import.js.map +1 -1
  8. package/dist/javascript/dependency-workspace.d.ts +1 -0
  9. package/dist/javascript/dependency-workspace.d.ts.map +1 -1
  10. package/dist/javascript/dependency-workspace.js +44 -0
  11. package/dist/javascript/dependency-workspace.js.map +1 -1
  12. package/dist/javascript/templating/pattern.d.ts +7 -0
  13. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  14. package/dist/javascript/templating/pattern.js +10 -0
  15. package/dist/javascript/templating/pattern.js.map +1 -1
  16. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  17. package/dist/javascript/templating/rewrite.js +17 -16
  18. package/dist/javascript/templating/rewrite.js.map +1 -1
  19. package/dist/javascript/templating/types.d.ts +56 -28
  20. package/dist/javascript/templating/types.d.ts.map +1 -1
  21. package/dist/javascript/type-mapping.d.ts +13 -1
  22. package/dist/javascript/type-mapping.d.ts.map +1 -1
  23. package/dist/javascript/type-mapping.js +207 -6
  24. package/dist/javascript/type-mapping.js.map +1 -1
  25. package/dist/version.txt +1 -1
  26. package/package.json +1 -1
  27. package/src/java/tree.ts +6 -1
  28. package/src/javascript/add-import.ts +101 -25
  29. package/src/javascript/dependency-workspace.ts +52 -0
  30. package/src/javascript/templating/pattern.ts +11 -0
  31. package/src/javascript/templating/rewrite.ts +19 -18
  32. package/src/javascript/templating/types.ts +60 -28
  33. package/src/javascript/type-mapping.ts +230 -7
@@ -47,6 +47,42 @@ export class DependencyWorkspace {
47
47
  // Create/update workspace in target directory
48
48
  fs.mkdirSync(targetDir, {recursive: true});
49
49
 
50
+ // Check if we can reuse a cached workspace by symlinking node_modules
51
+ const hash = this.hashDependencies(dependencies);
52
+ const cachedWorkspaceDir = path.join(this.WORKSPACE_BASE, hash);
53
+ const cachedNodeModules = path.join(cachedWorkspaceDir, 'node_modules');
54
+
55
+ if (fs.existsSync(cachedNodeModules) && this.isWorkspaceValid(cachedWorkspaceDir, dependencies)) {
56
+ // Symlink node_modules from cached workspace
57
+ try {
58
+ const targetNodeModules = path.join(targetDir, 'node_modules');
59
+
60
+ // Remove existing node_modules if present (might be invalid)
61
+ if (fs.existsSync(targetNodeModules)) {
62
+ fs.rmSync(targetNodeModules, {recursive: true, force: true});
63
+ }
64
+
65
+ // Create symlink to cached node_modules
66
+ fs.symlinkSync(cachedNodeModules, targetNodeModules, 'dir');
67
+
68
+ // Write package.json
69
+ const packageJson = {
70
+ name: "openrewrite-template-workspace",
71
+ version: "1.0.0",
72
+ private: true,
73
+ dependencies: dependencies
74
+ };
75
+ fs.writeFileSync(
76
+ path.join(targetDir, 'package.json'),
77
+ JSON.stringify(packageJson, null, 2)
78
+ );
79
+
80
+ return targetDir;
81
+ } catch (symlinkError) {
82
+ // Symlink failed (e.g., cross-device, permissions) - fall through to npm install
83
+ }
84
+ }
85
+
50
86
  try {
51
87
  const packageJson = {
52
88
  name: "openrewrite-template-workspace",
@@ -214,6 +250,7 @@ export class DependencyWorkspace {
214
250
 
215
251
  /**
216
252
  * Checks if a workspace is valid (has node_modules and matching package.json).
253
+ * Handles both real node_modules directories and symlinks to cached workspaces.
217
254
  *
218
255
  * @param workspaceDir Directory to check
219
256
  * @param expectedDependencies Optional dependencies to check against package.json
@@ -222,10 +259,25 @@ export class DependencyWorkspace {
222
259
  const nodeModules = path.join(workspaceDir, 'node_modules');
223
260
  const packageJsonPath = path.join(workspaceDir, 'package.json');
224
261
 
262
+ // Check node_modules exists (as directory or symlink)
225
263
  if (!fs.existsSync(nodeModules) || !fs.existsSync(packageJsonPath)) {
226
264
  return false;
227
265
  }
228
266
 
267
+ // If node_modules is a symlink, verify the target still exists
268
+ try {
269
+ const stats = fs.lstatSync(nodeModules);
270
+ if (stats.isSymbolicLink()) {
271
+ const target = fs.readlinkSync(nodeModules);
272
+ const absoluteTarget = path.isAbsolute(target) ? target : path.resolve(path.dirname(nodeModules), target);
273
+ if (!fs.existsSync(absoluteTarget)) {
274
+ return false;
275
+ }
276
+ }
277
+ } catch {
278
+ return false;
279
+ }
280
+
229
281
  // If dependencies provided, check if they match
230
282
  if (expectedDependencies) {
231
283
  try {
@@ -616,6 +616,17 @@ export class MatchResult implements IMatchResult {
616
616
  return this.extractElements(value);
617
617
  }
618
618
 
619
+ /**
620
+ * Checks if a capture has been matched.
621
+ *
622
+ * @param capture The capture name (string) or Capture object
623
+ * @returns true if the capture exists in the match result
624
+ */
625
+ has(capture: Capture | string): boolean {
626
+ const name = typeof capture === "string" ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName());
627
+ return this.storage.has(name);
628
+ }
629
+
619
630
  /**
620
631
  * Extracts semantic elements from storage value.
621
632
  * For wrappers, extracts the .element; for arrays, returns array of elements.
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import {Cursor, ExecutionContext, Recipe} from '../..';
17
17
  import {J} from '../../java';
18
- import {RewriteRule, RewriteConfig} from './types';
18
+ import {RewriteRule, RewriteConfig, PreMatchContext, PostMatchContext} from './types';
19
19
  import {Pattern, MatchResult} from './pattern';
20
20
  import {Template} from './template';
21
21
 
@@ -26,28 +26,29 @@ class RewriteRuleImpl implements RewriteRule {
26
26
  constructor(
27
27
  private readonly before: Pattern[],
28
28
  private readonly after: Template | ((match: MatchResult) => Template),
29
- private readonly where?: (node: J, cursor: Cursor) => boolean | Promise<boolean>,
30
- private readonly whereNot?: (node: J, cursor: Cursor) => boolean | Promise<boolean>
29
+ private readonly preMatch?: (node: J, context: PreMatchContext) => boolean | Promise<boolean>,
30
+ private readonly postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>
31
31
  ) {
32
32
  }
33
33
 
34
34
  async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
35
+ // Evaluate preMatch before attempting any pattern matching
36
+ if (this.preMatch) {
37
+ const preMatchResult = await this.preMatch(node, { cursor });
38
+ if (!preMatchResult) {
39
+ return undefined; // Early exit - don't attempt pattern matching
40
+ }
41
+ }
42
+
35
43
  for (const pattern of this.before) {
36
44
  // Pass cursor to pattern.match() for context-aware capture constraints
37
45
  const match = await pattern.match(node, cursor);
38
46
  if (match) {
39
- // Evaluate context predicates after structural match
40
- if (this.where) {
41
- const whereResult = await this.where(node, cursor);
42
- if (!whereResult) {
43
- continue; // Pattern matched but context doesn't, try next pattern
44
- }
45
- }
46
-
47
- if (this.whereNot) {
48
- const whereNotResult = await this.whereNot(node, cursor);
49
- if (whereNotResult) {
50
- continue; // Pattern matched but context is excluded, try next pattern
47
+ // Evaluate postMatch after structural match succeeds
48
+ if (this.postMatch) {
49
+ const postMatchResult = await this.postMatch(node, { cursor, captures: match });
50
+ if (!postMatchResult) {
51
+ continue; // Pattern matched but postMatch failed, try next pattern
51
52
  }
52
53
  }
53
54
 
@@ -69,7 +70,7 @@ class RewriteRuleImpl implements RewriteRule {
69
70
  }
70
71
  }
71
72
 
72
- // Return undefined if no patterns match or all context checks failed
73
+ // Return undefined if no patterns match or all postMatch checks failed
73
74
  return undefined;
74
75
  }
75
76
 
@@ -168,8 +169,8 @@ export function rewrite(
168
169
  return new RewriteRuleImpl(
169
170
  Array.isArray(config.before) ? config.before : [config.before],
170
171
  config.after,
171
- config.where,
172
- config.whereNot
172
+ config.preMatch,
173
+ config.postMatch
173
174
  );
174
175
  }
175
176
 
@@ -590,6 +590,33 @@ export interface RewriteRule {
590
590
  orElse(alternative: RewriteRule): RewriteRule;
591
591
  }
592
592
 
593
+ /**
594
+ * Context for preMatch predicate - only has cursor, no captures yet.
595
+ */
596
+ export interface PreMatchContext {
597
+ /**
598
+ * The cursor pointing to the node being considered for matching.
599
+ * Allows navigating the AST (parent, root, etc.).
600
+ */
601
+ cursor: Cursor;
602
+ }
603
+
604
+ /**
605
+ * Context for postMatch predicate - has cursor and captured values.
606
+ */
607
+ export interface PostMatchContext {
608
+ /**
609
+ * The cursor pointing to the matched node.
610
+ * Allows navigating the AST (parent, root, etc.).
611
+ */
612
+ cursor: Cursor;
613
+
614
+ /**
615
+ * Values captured during pattern matching.
616
+ */
617
+ captures: CaptureMap;
618
+ }
619
+
593
620
  /**
594
621
  * Configuration for a replacement rule.
595
622
  */
@@ -598,55 +625,52 @@ export interface RewriteConfig {
598
625
  after: Template | ((match: MatchResult) => Template);
599
626
 
600
627
  /**
601
- * Optional context predicate that must evaluate to true for the transformation to be applied.
602
- * Evaluated after the pattern matches structurally but before applying the template.
603
- * Provides access to both the matched node and the cursor for context inspection.
628
+ * Optional predicate evaluated BEFORE pattern matching.
629
+ * Use for efficient early filtering based on AST context when captures aren't needed.
630
+ * If this returns false, pattern matching is skipped entirely.
604
631
  *
605
- * @param node The matched AST node
606
- * @param cursor The cursor at the matched node, providing access to ancestors and context
607
- * @returns true if the transformation should be applied, false otherwise
632
+ * @param node The AST node being considered for matching
633
+ * @param context Context providing cursor for AST navigation
634
+ * @returns true to proceed with pattern matching, false to skip this node
608
635
  *
609
636
  * @example
610
637
  * ```typescript
611
638
  * rewrite(() => ({
612
- * before: pattern`await ${_('promise')}`,
613
- * after: template`await ${_('promise')}.catch(handleError)`,
614
- * where: (node, cursor) => {
615
- * // Only apply inside async functions
616
- * const method = cursor.firstEnclosing((n: any): n is J.MethodDeclaration =>
617
- * n.kind === J.Kind.MethodDeclaration
618
- * );
619
- * return method?.modifiers.some(m => m.type === 'async') || false;
639
+ * before: pattern`console.log(${_('msg')})`,
640
+ * after: template`logger.info(${_('msg')})`,
641
+ * preMatch: (node, {cursor}) => {
642
+ * // Only attempt matching inside functions named 'handleError'
643
+ * const method = cursor.firstEnclosing(isMethodDeclaration);
644
+ * return method?.name.simpleName === 'handleError';
620
645
  * }
621
646
  * }));
622
647
  * ```
623
648
  */
624
- where?: (node: J, cursor: Cursor) => boolean | Promise<boolean>;
649
+ preMatch?: (node: J, context: PreMatchContext) => boolean | Promise<boolean>;
625
650
 
626
651
  /**
627
- * Optional context predicate that must evaluate to false for the transformation to be applied.
628
- * Evaluated after the pattern matches structurally but before applying the template.
629
- * Provides access to both the matched node and the cursor for context inspection.
652
+ * Optional predicate evaluated AFTER pattern matching succeeds.
653
+ * Use when you need access to captured values to decide whether to apply the transformation.
654
+ * If this returns false, the transformation is not applied.
630
655
  *
631
656
  * @param node The matched AST node
632
- * @param cursor The cursor at the matched node, providing access to ancestors and context
633
- * @returns true if the transformation should NOT be applied, false if it should proceed
657
+ * @param context Context providing cursor for AST navigation and captured values
658
+ * @returns true to apply the transformation, false to skip
634
659
  *
635
660
  * @example
636
661
  * ```typescript
637
662
  * rewrite(() => ({
638
- * before: pattern`await ${_('promise')}`,
639
- * after: template`await ${_('promise')}.catch(handleError)`,
640
- * whereNot: (node, cursor) => {
641
- * // Don't apply inside try-catch blocks
642
- * return cursor.firstEnclosing((n: any): n is J.Try =>
643
- * n.kind === J.Kind.Try
644
- * ) !== undefined;
663
+ * before: pattern`${_('a')} + ${_('b')}`,
664
+ * after: template`${_('b')} + ${_('a')}`,
665
+ * postMatch: (node, {cursor, captures}) => {
666
+ * // Only swap if 'a' is a literal number
667
+ * const a = captures.get('a');
668
+ * return a?.kind === J.Kind.Literal && typeof a.value === 'number';
645
669
  * }
646
670
  * }));
647
671
  * ```
648
672
  */
649
- whereNot?: (node: J, cursor: Cursor) => boolean | Promise<boolean>;
673
+ postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>;
650
674
  }
651
675
 
652
676
  /**
@@ -755,6 +779,14 @@ export interface MatchResult {
755
779
  get(capture: string): any;
756
780
 
757
781
  get<T>(capture: Capture<T>): T | undefined;
782
+
783
+ /**
784
+ * Checks if a capture has been matched.
785
+ *
786
+ * @param capture The capture name (string) or Capture object
787
+ * @returns true if the capture exists in the match result
788
+ */
789
+ has(capture: Capture | string): boolean;
758
790
  }
759
791
 
760
792
  /**
@@ -65,6 +65,16 @@ export class JavaScriptTypeMapping {
65
65
  }
66
66
 
67
67
  type(node: ts.Node): Type | undefined {
68
+ // For identifiers, check if this references a variable
69
+ // This enables fieldType attribution for variable references
70
+ if (ts.isIdentifier(node)) {
71
+ const variableType = this.variableType(node);
72
+ if (variableType) {
73
+ return variableType;
74
+ }
75
+ // Fall through to regular type checking if not a variable
76
+ }
77
+
68
78
  let type: ts.Type | undefined;
69
79
  if (ts.isExpression(node)) {
70
80
  type = this.checker.getTypeAtLocation(node);
@@ -318,16 +328,229 @@ export class JavaScriptTypeMapping {
318
328
  return Type.isPrimitive(type) ? type : Type.Primitive.None;
319
329
  }
320
330
 
321
- variableType(node: ts.NamedDeclaration): Type.Variable | undefined {
331
+ variableType(node: ts.Node): Type.Variable | undefined {
332
+ let symbol: ts.Symbol | undefined;
333
+ let location: ts.Node = node;
334
+
335
+ // Get the symbol depending on node type
322
336
  if (ts.isVariableDeclaration(node)) {
323
- const symbol = this.checker.getSymbolAtLocation(node.name);
324
- if (symbol) {
325
- // TODO: Implement in Phase 6
326
- // const type = this.checker.getTypeOfSymbolAtLocation(symbol, node);
327
- // return JavaType.Variable with proper mapping
337
+ symbol = this.checker.getSymbolAtLocation(node.name);
338
+ } else if (ts.isParameter(node)) {
339
+ symbol = this.checker.getSymbolAtLocation(node.name);
340
+ } else if (ts.isIdentifier(node)) {
341
+ // For identifier references (like 'vi' in 'vi.fn()')
342
+ symbol = this.checker.getSymbolAtLocation(node);
343
+ } else if (ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) {
344
+ symbol = this.checker.getSymbolAtLocation(node.name);
345
+ } else {
346
+ // Not a variable/parameter/property we can handle
347
+ return undefined;
348
+ }
349
+
350
+ if (!symbol) {
351
+ return undefined;
352
+ }
353
+
354
+ // Get the variable declaration (resolve aliases if needed)
355
+ let actualSymbol = symbol;
356
+ if (symbol.flags & ts.SymbolFlags.Alias) {
357
+ actualSymbol = this.checker.getAliasedSymbol(symbol);
358
+ }
359
+
360
+ // Check if this symbol represents a variable, parameter, or property
361
+ // Exclude functions, classes, interfaces, namespaces, type aliases
362
+ const isExcluded = actualSymbol.flags & (
363
+ ts.SymbolFlags.Function |
364
+ ts.SymbolFlags.Class |
365
+ ts.SymbolFlags.Interface |
366
+ ts.SymbolFlags.Enum |
367
+ ts.SymbolFlags.ValueModule |
368
+ ts.SymbolFlags.NamespaceModule |
369
+ ts.SymbolFlags.TypeAlias |
370
+ ts.SymbolFlags.TypeParameter
371
+ );
372
+
373
+ if (isExcluded) {
374
+ // Not a variable - it's a type, function, class, namespace, etc.
375
+ return undefined;
376
+ }
377
+
378
+ const isVariable = actualSymbol.flags & (
379
+ ts.SymbolFlags.Variable |
380
+ ts.SymbolFlags.Property |
381
+ ts.SymbolFlags.FunctionScopedVariable |
382
+ ts.SymbolFlags.BlockScopedVariable
383
+ );
384
+
385
+ if (!isVariable) {
386
+ // Not a variable we recognize
387
+ return undefined;
388
+ }
389
+
390
+ // Get the type of the variable
391
+ const variableType = this.checker.getTypeOfSymbolAtLocation(actualSymbol, location);
392
+ const mappedType = this.getType(variableType);
393
+
394
+ // Get the owner (declaring type) for the variable
395
+ let ownerType: Type | undefined;
396
+
397
+ // Check if the variable is imported
398
+ if (symbol.flags & ts.SymbolFlags.Alias) {
399
+ // For imported variables, find the module specifier
400
+ const declarations = symbol.declarations;
401
+ if (declarations && declarations.length > 0) {
402
+ let importNode: ts.Node | undefined = declarations[0];
403
+
404
+ // Traverse up to find the ImportDeclaration
405
+ while (importNode && !ts.isImportDeclaration(importNode)) {
406
+ importNode = importNode.parent;
407
+ }
408
+
409
+ if (importNode && ts.isImportDeclaration(importNode)) {
410
+ const importDecl = importNode as ts.ImportDeclaration;
411
+ if (ts.isStringLiteral(importDecl.moduleSpecifier)) {
412
+ const moduleSpecifier = importDecl.moduleSpecifier.text;
413
+ // Create a Type.Class representing the module
414
+ ownerType = Object.assign(new NonDraftableType(), {
415
+ kind: Type.Kind.Class,
416
+ flags: 0,
417
+ classKind: Type.Class.Kind.Interface,
418
+ fullyQualifiedName: moduleSpecifier,
419
+ typeParameters: [],
420
+ annotations: [],
421
+ interfaces: [],
422
+ members: [],
423
+ methods: [],
424
+ toJSON: function () {
425
+ return Type.signature(this);
426
+ }
427
+ }) as Type.Class;
428
+ }
429
+ }
328
430
  }
431
+ } else {
432
+ // For non-imported variables, check if they belong to a class/interface/namespace
433
+ const parentSymbol = (actualSymbol as any).parent as ts.Symbol | undefined;
434
+ if (parentSymbol) {
435
+ const parentType = this.checker.getDeclaredTypeOfSymbol(parentSymbol);
436
+ if (parentType) {
437
+ ownerType = this.getType(parentType);
438
+
439
+ // If the parent is a namespace, try to find the module it came from
440
+ // This handles cases like React.forwardRef where the namespace is React
441
+ // but the module is "react"
442
+ if (parentSymbol.flags & ts.SymbolFlags.ValueModule ||
443
+ parentSymbol.flags & ts.SymbolFlags.NamespaceModule) {
444
+ // Check if this namespace was imported
445
+ const parentDeclarations = parentSymbol.declarations;
446
+ if (parentDeclarations && parentDeclarations.length > 0) {
447
+ const firstDecl = parentDeclarations[0];
448
+ const sourceFile = firstDecl.getSourceFile();
449
+ // If it's from node_modules or a .d.ts file, try to extract the module name
450
+ if (sourceFile.isDeclarationFile) {
451
+ const fileName = sourceFile.fileName;
452
+ const moduleName = this.extractModuleNameFromPath(fileName);
453
+ if (moduleName) {
454
+ // Store the module as the owningClass for now
455
+ // (This is a bit of a hack, but works with the current type system)
456
+ if (Type.isClass(ownerType)) {
457
+ (ownerType as any).owningClass = Object.assign(new NonDraftableType(), {
458
+ kind: Type.Kind.Class,
459
+ flags: 0,
460
+ classKind: Type.Class.Kind.Interface,
461
+ fullyQualifiedName: moduleName,
462
+ typeParameters: [],
463
+ annotations: [],
464
+ interfaces: [],
465
+ members: [],
466
+ methods: [],
467
+ toJSON: function () {
468
+ return Type.signature(this);
469
+ }
470
+ }) as Type.Class;
471
+ }
472
+ }
473
+ }
474
+ }
475
+ }
476
+ }
477
+ }
478
+ }
479
+
480
+ // Create the Type.Variable
481
+ const variable = Object.assign(new NonDraftableType(), {
482
+ kind: Type.Kind.Variable,
483
+ name: actualSymbol.getName(),
484
+ owner: ownerType,
485
+ type: mappedType,
486
+ annotations: [],
487
+ toJSON: function () {
488
+ return Type.signature(this);
489
+ }
490
+ }) as Type.Variable;
491
+
492
+ return variable;
493
+ }
494
+
495
+ /**
496
+ * Extract the npm module name from a file path.
497
+ * Handles various package manager layouts:
498
+ * - Standard: /path/node_modules/react/index.d.ts -> react
499
+ * - Scoped: /path/node_modules/@types/react/index.d.ts -> react
500
+ * - Scoped with __ encoding: /path/node_modules/@types/testing-library__react/index.d.ts -> @testing-library/react
501
+ * - Nested node_modules: /path/node_modules/pkg/node_modules/dep/index.d.ts -> dep
502
+ * - pnpm: /path/node_modules/.pnpm/react@18.2.0/node_modules/react/index.d.ts -> react
503
+ *
504
+ * @returns The module name, or undefined if not from node_modules
505
+ */
506
+ private extractModuleNameFromPath(fileName: string): string | undefined {
507
+ if (!fileName.includes('node_modules/')) {
508
+ return undefined;
509
+ }
510
+
511
+ // Find the last occurrence of node_modules/ to handle nested dependencies
512
+ // This also correctly handles pnpm's .pnpm structure
513
+ const lastNodeModulesIndex = fileName.lastIndexOf('node_modules/');
514
+ const afterNodeModules = fileName.substring(lastNodeModulesIndex + 'node_modules/'.length);
515
+
516
+ // Split by '/' to get path segments
517
+ const segments = afterNodeModules.split('/');
518
+ if (segments.length === 0) {
519
+ return undefined;
520
+ }
521
+
522
+ let moduleName: string;
523
+
524
+ // Handle scoped packages (@scope/package)
525
+ if (segments[0].startsWith('@') && segments.length > 1) {
526
+ moduleName = `${segments[0]}/${segments[1]}`;
527
+ } else {
528
+ moduleName = segments[0];
329
529
  }
330
- return undefined;
530
+
531
+ // Skip pnpm's .pnpm directory - it contains versioned package paths
532
+ // In pnpm, the actual package is in: .pnpm/pkg@version/node_modules/pkg
533
+ // So we already handled this by using lastIndexOf above
534
+ if (moduleName === '.pnpm') {
535
+ return undefined;
536
+ }
537
+
538
+ // Remove @types/ prefix and decode DefinitelyTyped scoped package encoding
539
+ // DefinitelyTyped encodes scoped packages using __ instead of /
540
+ // Example: @types/testing-library__react -> @testing-library/react
541
+ if (moduleName.startsWith('@types/')) {
542
+ moduleName = moduleName.substring('@types/'.length);
543
+ // Decode __ encoding for scoped packages
544
+ // testing-library__react -> @testing-library/react
545
+ if (moduleName.includes('__')) {
546
+ const parts = moduleName.split('__');
547
+ if (parts.length === 2) {
548
+ moduleName = `@${parts[0]}/${parts[1]}`;
549
+ }
550
+ }
551
+ }
552
+
553
+ return moduleName;
331
554
  }
332
555
 
333
556
  /**