@atlaskit/eslint-plugin-platform 2.8.0 → 2.9.0

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 (39) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/index.js +6 -1
  3. package/dist/cjs/rules/ensure-use-sync-external-store-server-snapshot/index.js +41 -0
  4. package/dist/cjs/rules/import/no-barrel-entry-imports/index.js +475 -67
  5. package/dist/cjs/rules/import/no-barrel-entry-jest-mock/index.js +387 -112
  6. package/dist/cjs/rules/import/no-jest-mock-barrel-files/index.js +3 -2
  7. package/dist/cjs/rules/import/no-relative-barrel-file-imports/index.js +7 -3
  8. package/dist/cjs/rules/import/shared/jest-utils.js +62 -9
  9. package/dist/cjs/rules/import/shared/package-resolution.js +156 -23
  10. package/dist/cjs/rules/visit-example-type-import-required/index.js +409 -0
  11. package/dist/es2019/index.js +6 -1
  12. package/dist/es2019/rules/ensure-use-sync-external-store-server-snapshot/index.js +43 -0
  13. package/dist/es2019/rules/import/no-barrel-entry-imports/index.js +372 -15
  14. package/dist/es2019/rules/import/no-barrel-entry-jest-mock/index.js +245 -17
  15. package/dist/es2019/rules/import/no-jest-mock-barrel-files/index.js +3 -2
  16. package/dist/es2019/rules/import/no-relative-barrel-file-imports/index.js +7 -3
  17. package/dist/es2019/rules/import/shared/jest-utils.js +44 -0
  18. package/dist/es2019/rules/import/shared/package-resolution.js +97 -5
  19. package/dist/es2019/rules/visit-example-type-import-required/index.js +375 -0
  20. package/dist/esm/index.js +6 -1
  21. package/dist/esm/rules/ensure-use-sync-external-store-server-snapshot/index.js +35 -0
  22. package/dist/esm/rules/import/no-barrel-entry-imports/index.js +475 -67
  23. package/dist/esm/rules/import/no-barrel-entry-jest-mock/index.js +388 -113
  24. package/dist/esm/rules/import/no-jest-mock-barrel-files/index.js +3 -2
  25. package/dist/esm/rules/import/no-relative-barrel-file-imports/index.js +7 -3
  26. package/dist/esm/rules/import/shared/jest-utils.js +61 -9
  27. package/dist/esm/rules/import/shared/package-resolution.js +156 -25
  28. package/dist/esm/rules/visit-example-type-import-required/index.js +402 -0
  29. package/dist/types/index.d.ts +12 -0
  30. package/dist/types/rules/ensure-use-sync-external-store-server-snapshot/index.d.ts +3 -0
  31. package/dist/types/rules/import/shared/jest-utils.d.ts +8 -0
  32. package/dist/types/rules/import/shared/package-resolution.d.ts +22 -2
  33. package/dist/types/rules/visit-example-type-import-required/index.d.ts +4 -0
  34. package/dist/types-ts4.5/index.d.ts +12 -0
  35. package/dist/types-ts4.5/rules/ensure-use-sync-external-store-server-snapshot/index.d.ts +3 -0
  36. package/dist/types-ts4.5/rules/import/shared/jest-utils.d.ts +8 -0
  37. package/dist/types-ts4.5/rules/import/shared/package-resolution.d.ts +22 -2
  38. package/dist/types-ts4.5/rules/visit-example-type-import-required/index.d.ts +4 -0
  39. package/package.json +3 -1
@@ -2,7 +2,7 @@ import { dirname } from 'path';
2
2
  import * as ts from 'typescript';
3
3
  import { hasReExportsFromOtherFiles, parseBarrelExports } from '../shared/barrel-parsing';
4
4
  import { DEFAULT_TARGET_FOLDERS, findWorkspaceRoot, isRelativeImport, readFileContent, resolveImportPath } from '../shared/file-system';
5
- import { extractImportPath, findJestRequireMockCalls, isJestMockCall, isJestRequireActual, resolveNewPathForRequireMock } from '../shared/jest-utils';
5
+ import { extractImportPath, findJestRequireActualCalls, findJestRequireMockCalls, isJestMockCall, isJestRequireActual, resolveNewPathForRequireMock } from '../shared/jest-utils';
6
6
  import { findPackageInRegistry, isPackageInApplyToImportsFrom } from '../shared/package-registry';
7
7
  import { findExportForSourceFile, parsePackageExports } from '../shared/package-resolution';
8
8
  import { realFileSystem } from '../shared/types';
@@ -531,6 +531,7 @@ function traceSymbolsToExports({
531
531
  const crossPackageGroups = new Map();
532
532
  const unmappedSymbols = [];
533
533
  for (const symbolName of symbolNames) {
534
+ var _findExportForSourceF, _findExportForSourceF2;
534
535
  const exportInfo = exportMap.get(symbolName);
535
536
  if (!exportInfo) {
536
537
  unmappedSymbols.push(symbolName);
@@ -554,10 +555,10 @@ function traceSymbolsToExports({
554
555
  }
555
556
 
556
557
  // First try to find an export that directly exposes the source file
557
- let targetExportPath = findExportForSourceFile({
558
+ let targetExportPath = (_findExportForSourceF = (_findExportForSourceF2 = findExportForSourceFile({
558
559
  sourceFilePath: exportInfo.path,
559
560
  exportsMap
560
- });
561
+ })) === null || _findExportForSourceF2 === void 0 ? void 0 : _findExportForSourceF2.exportPath) !== null && _findExportForSourceF !== void 0 ? _findExportForSourceF : null;
561
562
 
562
563
  // If no direct match, check which export can provide this symbol
563
564
  // (handles nested barrels where the symbol is re-exported through intermediate files)
@@ -613,6 +614,59 @@ function escapeRegExp(str) {
613
614
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
614
615
  }
615
616
 
617
+ /** Mock object keys that are module interop metadata, not package exports. */
618
+ const ESM_INTEROP_MOCK_KEYS = new Set(['__esModule']);
619
+
620
+ /**
621
+ * If a preamble line is `const|let actual = jest.requireActual('<barrel>')`, rewrite the specifier to
622
+ * `targetImportPathForThisMock` when every `binding.<prop>` read in this split mock's implementation
623
+ * resolves to that same path via `symbolToNewImportPath`. Otherwise leave unchanged (e.g. mixed paths
624
+ * still need the barrel module).
625
+ */
626
+ function rewritePreambleLineBarrelRequireActual({
627
+ lineText,
628
+ oldBarrelPath,
629
+ targetImportPathForThisMock,
630
+ mockImplementationTextForGroup,
631
+ symbolToNewImportPath
632
+ }) {
633
+ const requireActualRe = /jest\.requireActual(?:<[^>]*>)?\((['"])([^'"]+)\1\)/;
634
+ const requireMatch = requireActualRe.exec(lineText);
635
+ if (!requireMatch || requireMatch[2] !== oldBarrelPath) {
636
+ return lineText;
637
+ }
638
+ const quote = requireMatch[1];
639
+ const bindingMatch = /^\s*(?:const|let)\s+(\w+)\s*=/m.exec(lineText);
640
+ if (!bindingMatch) {
641
+ return lineText;
642
+ }
643
+ const binding = bindingMatch[1];
644
+ const propAccessRe = new RegExp(`\\b${escapeRegExp(binding)}\\.(\\w+)`, 'g');
645
+ const accessedProps = new Set();
646
+ let propMatch;
647
+ while ((propMatch = propAccessRe.exec(mockImplementationTextForGroup)) !== null) {
648
+ accessedProps.add(propMatch[1]);
649
+ }
650
+ if (accessedProps.size === 0) {
651
+ return lineText;
652
+ }
653
+ const resolvedPaths = [];
654
+ for (const prop of accessedProps) {
655
+ const mapped = symbolToNewImportPath.get(prop);
656
+ if (mapped) {
657
+ resolvedPaths.push(mapped);
658
+ }
659
+ }
660
+ if (resolvedPaths.length === 0) {
661
+ return lineText;
662
+ }
663
+ const uniquePaths = new Set(resolvedPaths);
664
+ if (uniquePaths.size !== 1 || !uniquePaths.has(targetImportPathForThisMock)) {
665
+ return lineText;
666
+ }
667
+ return lineText.replace(`jest.requireActual(${quote}${oldBarrelPath}${quote})`, `jest.requireActual(${quote}${targetImportPathForThisMock}${quote})`);
668
+ }
669
+
616
670
  /**
617
671
  * Generate fix text for multiple jest.mock calls
618
672
  */
@@ -622,7 +676,10 @@ function generateMockFixes({
622
676
  packageName,
623
677
  mockProperties,
624
678
  quote,
625
- preambleStatements
679
+ preambleStatements,
680
+ propagateEsModuleFromOriginalMock,
681
+ oldBarrelImportPath,
682
+ symbolToNewImportPath
626
683
  }) {
627
684
  const mockCalls = [];
628
685
 
@@ -631,8 +688,8 @@ function generateMockFixes({
631
688
  const propTexts = [];
632
689
  propTexts.push(`...jest.requireActual(${quote}${fullImportPath}${quote})`);
633
690
 
634
- // Add __esModule: true when mocking default exports
635
- if (group.hasDefaultExport) {
691
+ // Add __esModule: true when mocking default exports, or when the original mock already used __esModule (apply to every split).
692
+ if (group.hasDefaultExport || propagateEsModuleFromOriginalMock) {
636
693
  propTexts.push('__esModule: true');
637
694
  }
638
695
  for (const propName of group.propertyNames) {
@@ -669,15 +726,26 @@ function generateMockFixes({
669
726
  }
670
727
  }
671
728
  }
729
+ const combinedGroupImplText = group.propertyNames.map(name => {
730
+ var _ref, _group$propertyTexts$, _mockProperties$get;
731
+ return (_ref = (_group$propertyTexts$ = group.propertyTexts.get(name)) !== null && _group$propertyTexts$ !== void 0 ? _group$propertyTexts$ : (_mockProperties$get = mockProperties.get(name)) === null || _mockProperties$get === void 0 ? void 0 : _mockProperties$get.text) !== null && _ref !== void 0 ? _ref : '';
732
+ }).join('\n');
672
733
 
673
734
  // Determine if we need preamble for this group
674
735
  const neededPreamble = getNeededPreamble({
675
736
  propertyTexts: propTexts,
676
737
  allPreamble: preambleStatements
677
738
  });
739
+ const rewrittenPreamble = neededPreamble.map(p => rewritePreambleLineBarrelRequireActual({
740
+ lineText: p.text,
741
+ oldBarrelPath: oldBarrelImportPath,
742
+ targetImportPathForThisMock: fullImportPath,
743
+ mockImplementationTextForGroup: combinedGroupImplText,
744
+ symbolToNewImportPath
745
+ }));
678
746
  if (neededPreamble.length > 0) {
679
747
  // Generate block body arrow function with preamble
680
- const preambleLines = neededPreamble.map(p => `\t${p.text}`).join('\n');
748
+ const preambleLines = rewrittenPreamble.map(text => `\t${text}`).join('\n');
681
749
  const formattedProps = propTexts.map(p => `\t\t${p},`).join('\n');
682
750
  return `jest.mock(${quote}${fullImportPath}${quote}, () => {\n${preambleLines}\n\treturn {\n${formattedProps}\n\t};\n})`;
683
751
  } else {
@@ -813,7 +881,8 @@ const ruleMeta = {
813
881
  additionalProperties: false
814
882
  }],
815
883
  messages: {
816
- barrelEntryMock: "jest.mock('{{path}}') is mocking a barrel file entry point. Split into separate mocks for each source file using package.json exports."
884
+ barrelEntryMock: "jest.mock('{{path}}') is mocking a barrel file entry point. Split into separate mocks for each source file using package.json exports.",
885
+ barrelEntryRequireActual: "jest.requireActual('{{path}}') references a barrel file entry point. Use a specific package.json export path instead."
817
886
  }
818
887
  };
819
888
 
@@ -836,6 +905,122 @@ export function createRule(fs) {
836
905
  return {
837
906
  CallExpression(rawNode) {
838
907
  const node = rawNode;
908
+
909
+ // Handle standalone jest.requireActual() calls that reference barrel entries.
910
+ // e.g. jest.requireActual('@atlaskit/pkg').Foo or const { Foo } = jest.requireActual('@atlaskit/pkg')
911
+ if (isJestRequireActual(node)) {
912
+ const raImportPath = extractImportPath(node);
913
+ if (!raImportPath) {
914
+ return;
915
+ }
916
+ const raContext = resolveJestMockContext({
917
+ importPath: raImportPath,
918
+ workspaceRoot,
919
+ fs,
920
+ applyToImportsFrom
921
+ });
922
+ if (!raContext) {
923
+ return;
924
+ }
925
+ if (!isBarrelFile({
926
+ exportMap: raContext.exportMap,
927
+ entryFilePath: raContext.entryFilePath
928
+ })) {
929
+ return;
930
+ }
931
+
932
+ // `jest.requireActual('<barrel>')` inside `jest.mock('<barrel>')` is handled by the mock
933
+ // rule's fix (preamble retargeting + `jest.requireActual('barrel').x` rewriting), including
934
+ // `const actual = jest.requireActual('<barrel>')` with no member access on the call.
935
+ // Skip standalone handling here to avoid duplicate diagnostics.
936
+ let ancestor = node.parent;
937
+ while (ancestor) {
938
+ if (ancestor.type === 'CallExpression' && isJestMockCall(ancestor)) {
939
+ const ancestorPath = extractImportPath(ancestor);
940
+ if (ancestorPath) {
941
+ const ancestorCtx = resolveJestMockContext({
942
+ importPath: ancestorPath,
943
+ workspaceRoot,
944
+ fs,
945
+ applyToImportsFrom
946
+ });
947
+ if (ancestorCtx && isBarrelFile({
948
+ exportMap: ancestorCtx.exportMap,
949
+ entryFilePath: ancestorCtx.entryFilePath
950
+ })) {
951
+ return;
952
+ }
953
+ }
954
+ }
955
+ ancestor = ancestor.parent;
956
+ }
957
+
958
+ // Determine which symbols are accessed from the barrel
959
+ const parent = node.parent;
960
+ const accessedSymbols = [];
961
+ if ((parent === null || parent === void 0 ? void 0 : parent.type) === 'MemberExpression' && parent.property.type === 'Identifier') {
962
+ accessedSymbols.push(parent.property.name);
963
+ } else if ((parent === null || parent === void 0 ? void 0 : parent.type) === 'VariableDeclarator' && parent.id.type === 'ObjectPattern') {
964
+ for (const prop of parent.id.properties) {
965
+ if (prop.type === 'Property' && prop.key.type === 'Identifier') {
966
+ accessedSymbols.push(prop.key.name);
967
+ }
968
+ }
969
+ }
970
+ if (accessedSymbols.length === 0) {
971
+ context.report({
972
+ node: node,
973
+ messageId: 'barrelEntryRequireActual',
974
+ data: {
975
+ path: raImportPath
976
+ }
977
+ });
978
+ return;
979
+ }
980
+ const {
981
+ groupedByExport,
982
+ crossPackageGroups
983
+ } = traceSymbolsToExports({
984
+ symbolNames: accessedSymbols,
985
+ exportMap: raContext.exportMap,
986
+ exportsMap: raContext.exportsMap,
987
+ currentExportPath: raContext.currentExportPath,
988
+ fs
989
+ });
990
+ let newPath = null;
991
+ if (groupedByExport.size === 1 && crossPackageGroups.size === 0) {
992
+ const [exportPath] = groupedByExport.keys();
993
+ newPath = `${raContext.packageName}${exportPath.slice(1)}`;
994
+ } else if (crossPackageGroups.size === 1 && groupedByExport.size === 0) {
995
+ const [cpImportPath] = crossPackageGroups.keys();
996
+ newPath = cpImportPath;
997
+ }
998
+ const sourceCode = context.getSourceCode();
999
+ if (newPath) {
1000
+ const resolvedNewPath = newPath;
1001
+ context.report({
1002
+ node: node,
1003
+ messageId: 'barrelEntryRequireActual',
1004
+ data: {
1005
+ path: raImportPath
1006
+ },
1007
+ fix(fixer) {
1008
+ const firstArg = node.arguments[0];
1009
+ const quote = sourceCode.getText(firstArg)[0];
1010
+ return fixer.replaceText(firstArg, `${quote}${resolvedNewPath}${quote}`);
1011
+ }
1012
+ });
1013
+ } else {
1014
+ context.report({
1015
+ node: node,
1016
+ messageId: 'barrelEntryRequireActual',
1017
+ data: {
1018
+ path: raImportPath
1019
+ }
1020
+ });
1021
+ }
1022
+ return;
1023
+ }
839
1024
  if (!isJestMockCall(node)) {
840
1025
  return;
841
1026
  }
@@ -883,7 +1068,8 @@ export function createRule(fs) {
883
1068
  if (mockProperties.size === 0) {
884
1069
  return;
885
1070
  }
886
- const symbolNames = Array.from(mockProperties.keys());
1071
+ const originalMockHadEsModule = mockProperties.has('__esModule');
1072
+ const symbolNames = Array.from(mockProperties.keys()).filter(name => !ESM_INTEROP_MOCK_KEYS.has(name));
887
1073
  const {
888
1074
  groupedByExport,
889
1075
  crossPackageGroups,
@@ -1028,22 +1214,36 @@ export function createRule(fs) {
1028
1214
  mergedGroups.push(group);
1029
1215
  }
1030
1216
  }
1031
- const fixText = generateMockFixes({
1217
+ const symbolToNewImportPath = new Map();
1218
+ for (const group of [...mergedGroups, ...crossPackageMockGroups]) {
1219
+ for (const propName of group.propertyNames) {
1220
+ symbolToNewImportPath.set(propName, group.importPath);
1221
+ }
1222
+ }
1223
+ let fixText = generateMockFixes({
1032
1224
  groups: mergedGroups,
1033
1225
  crossPackageGroups: crossPackageMockGroups,
1034
1226
  packageName: mockContext.packageName,
1035
1227
  mockProperties,
1036
1228
  quote,
1037
- preambleStatements
1229
+ preambleStatements,
1230
+ propagateEsModuleFromOriginalMock: originalMockHadEsModule,
1231
+ oldBarrelImportPath: oldImportPath,
1232
+ symbolToNewImportPath
1038
1233
  });
1039
1234
 
1040
- // Build a map of symbol name -> new import path for jest.requireMock() rewriting
1041
- const symbolToNewImportPath = new Map();
1042
- for (const group of [...mergedGroups, ...crossPackageMockGroups]) {
1043
- for (const propName of group.propertyNames) {
1044
- symbolToNewImportPath.set(propName, group.importPath);
1235
+ // Post-process fixText to update jest.requireActual('barrel').Symbol
1236
+ // references embedded in property texts (e.g. inside jest.fn callbacks)
1237
+ fixText = fixText.replace(/jest\.requireActual(?:<[^>]*>)?\((['"])([^'"]+)\1\)\.(\w+)/g, (match, _q, path, symbol) => {
1238
+ if (path !== oldImportPath) {
1239
+ return match;
1045
1240
  }
1046
- }
1241
+ const newPath = symbolToNewImportPath.get(symbol);
1242
+ if (newPath && newPath !== path) {
1243
+ return match.replace(path, newPath);
1244
+ }
1245
+ return match;
1246
+ });
1047
1247
 
1048
1248
  // Sort nodes by position
1049
1249
  const sortedNodesToRemove = nodesToRemove.sort((a, b) => {
@@ -1101,6 +1301,34 @@ export function createRule(fs) {
1101
1301
  fixes.push(fixer.replaceText(requireMockArg, `${quote}${newPath}${quote}`));
1102
1302
  }
1103
1303
  }
1304
+
1305
+ // Fix jest.requireActual() calls that reference the old barrel path.
1306
+ // Only fix calls OUTSIDE the replaced jest.mock node range
1307
+ // (calls inside it are handled via fixText string replacement below).
1308
+ const replacedRanges = sortedNodesToRemove.map(n => n.range);
1309
+ const requireActualCalls = findJestRequireActualCalls({
1310
+ ast,
1311
+ matchPath: candidatePath => candidatePath === oldImportPath
1312
+ });
1313
+ for (const raNode of requireActualCalls) {
1314
+ const raArg = raNode.arguments[0];
1315
+ if (!raArg || !raNode.range) {
1316
+ continue;
1317
+ }
1318
+
1319
+ // Skip calls inside any node being replaced (ranges overlap)
1320
+ const insideReplacedNode = replacedRanges.some(([start, end]) => raNode.range[0] >= start && raNode.range[1] <= end);
1321
+ if (insideReplacedNode) {
1322
+ continue;
1323
+ }
1324
+ const newPath = resolveNewPathForRequireMock({
1325
+ requireMockNode: raNode,
1326
+ symbolToNewPath: symbolToNewImportPath
1327
+ });
1328
+ if (newPath) {
1329
+ fixes.push(fixer.replaceText(raArg, `${quote}${newPath}${quote}`));
1330
+ }
1331
+ }
1104
1332
  return fixes;
1105
1333
  }
1106
1334
  });
@@ -152,6 +152,7 @@ function getImportPathForSourceFile({
152
152
  var _exportInfo$crossPack;
153
153
  const crossPackageName = exportInfo === null || exportInfo === void 0 ? void 0 : (_exportInfo$crossPack = exportInfo.crossPackageSource) === null || _exportInfo$crossPack === void 0 ? void 0 : _exportInfo$crossPack.packageName;
154
154
  if (crossPackageName) {
155
+ var _findExportForSourceF;
155
156
  const sourcePackageExportsMaps = getSourcePackageExportsMaps(fs);
156
157
  let exportsMap = sourcePackageExportsMaps.get(crossPackageName);
157
158
  if (!exportsMap) {
@@ -168,10 +169,10 @@ function getImportPathForSourceFile({
168
169
  sourcePackageExportsMaps.set(crossPackageName, exportsMap);
169
170
  }
170
171
  }
171
- const targetExportPath = exportsMap ? findExportForSourceFile({
172
+ const targetExportPath = exportsMap ? (_findExportForSourceF = findExportForSourceFile({
172
173
  sourceFilePath,
173
174
  exportsMap
174
- }) : null;
175
+ })) === null || _findExportForSourceF === void 0 ? void 0 : _findExportForSourceF.exportPath : null;
175
176
  return targetExportPath ? crossPackageName + targetExportPath.slice(1) : crossPackageName;
176
177
  }
177
178
  return getRelativeImportPath({
@@ -54,6 +54,7 @@ function getImportPathForSourceFile({
54
54
  var _exportInfo$crossPack;
55
55
  const crossPackageName = exportInfo === null || exportInfo === void 0 ? void 0 : (_exportInfo$crossPack = exportInfo.crossPackageSource) === null || _exportInfo$crossPack === void 0 ? void 0 : _exportInfo$crossPack.packageName;
56
56
  if (crossPackageName) {
57
+ var _findExportForSourceF;
57
58
  const sourcePackageExportsMaps = getSourcePackageExportsMaps(fs);
58
59
  let exportsMap = sourcePackageExportsMaps.get(crossPackageName);
59
60
  if (!exportsMap) {
@@ -70,10 +71,10 @@ function getImportPathForSourceFile({
70
71
  sourcePackageExportsMaps.set(crossPackageName, exportsMap);
71
72
  }
72
73
  }
73
- const targetExportPath = exportsMap ? findExportForSourceFile({
74
+ const targetExportPath = exportsMap ? (_findExportForSourceF = findExportForSourceFile({
74
75
  sourceFilePath,
75
76
  exportsMap
76
- }) : null;
77
+ })) === null || _findExportForSourceF === void 0 ? void 0 : _findExportForSourceF.exportPath : null;
77
78
  return targetExportPath ? crossPackageName + targetExportPath.slice(1) : crossPackageName;
78
79
  }
79
80
  return getRelativeImportPath({
@@ -437,10 +438,13 @@ function transformExportSpecifiers({
437
438
  }) {
438
439
  return specsWithOriginal.map(({
439
440
  originalName,
441
+ nameInSource,
440
442
  nameInLocal,
441
443
  kind
442
444
  }) => ({
443
- nameInSource: originalName || 'default',
445
+ // Use originalName if available (means there was an alias in the barrel),
446
+ // otherwise use nameInSource (the direct export name from the barrel)
447
+ nameInSource: originalName !== null && originalName !== void 0 ? originalName : nameInSource,
444
448
  nameInLocal,
445
449
  kind
446
450
  }));
@@ -112,6 +112,50 @@ export function findJestRequireMockCalls({
112
112
  return results;
113
113
  }
114
114
 
115
+ /**
116
+ * Find all jest.requireActual() calls in the AST whose import path matches a given target.
117
+ * Works identically to findJestRequireMockCalls but for requireActual.
118
+ */
119
+ export function findJestRequireActualCalls({
120
+ ast,
121
+ matchPath
122
+ }) {
123
+ const results = [];
124
+ const visited = new WeakSet();
125
+ const skipKeys = new Set(['parent', 'loc', 'range', 'tokens', 'comments']);
126
+ function visit(node) {
127
+ if (visited.has(node)) {
128
+ return;
129
+ }
130
+ visited.add(node);
131
+ if (node.type === 'CallExpression' && isJestRequireActual(node)) {
132
+ const path = extractImportPath(node);
133
+ if (path && matchPath(path)) {
134
+ results.push(node);
135
+ }
136
+ }
137
+ for (const key in node) {
138
+ if (skipKeys.has(key)) {
139
+ continue;
140
+ }
141
+ const value = node[key];
142
+ if (value && typeof value === 'object') {
143
+ if (Array.isArray(value)) {
144
+ for (const child of value) {
145
+ if (child && typeof child === 'object' && 'type' in child) {
146
+ visit(child);
147
+ }
148
+ }
149
+ } else if ('type' in value) {
150
+ visit(value);
151
+ }
152
+ }
153
+ }
154
+ }
155
+ visit(ast);
156
+ return results;
157
+ }
158
+
115
159
  /**
116
160
  * Determine the best new import path for a jest.requireMock() call by inspecting
117
161
  * the destructured symbols or property access at the call site.
@@ -1,6 +1,65 @@
1
- import { join } from 'path';
2
- import { readFileContent, resolveImportPath } from './file-system';
1
+ import { dirname, join } from 'path';
2
+ import * as ts from 'typescript';
3
+ import { isRelativeImport, readFileContent, resolveImportPath } from './file-system';
3
4
  import { findPackageInRegistry } from './package-registry';
5
+ const ENTRY_POINT_FOLDER_NAMES = new Set(['entry-points', 'entrypoints', 'entrypoint', 'entry-point']);
6
+ function isInEntryPointsFolder(filePath) {
7
+ const parts = filePath.split(/[/\\]/);
8
+ return parts.some(part => ENTRY_POINT_FOLDER_NAMES.has(part));
9
+ }
10
+ /**
11
+ * Parse an entry-point wrapper file and resolve the source files it re-exports from,
12
+ * along with name mappings (source export name → entry-point export name).
13
+ */
14
+ function resolveEntryPointReExports({
15
+ entryPointFilePath,
16
+ fs
17
+ }) {
18
+ const content = readFileContent({
19
+ filePath: entryPointFilePath,
20
+ fs
21
+ });
22
+ if (!content) {
23
+ return [];
24
+ }
25
+ try {
26
+ const sourceFile = ts.createSourceFile(entryPointFilePath, content, ts.ScriptTarget.Latest, true);
27
+ const basedir = dirname(entryPointFilePath);
28
+ const results = [];
29
+ for (const statement of sourceFile.statements) {
30
+ if (ts.isExportDeclaration(statement) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
31
+ const modulePath = statement.moduleSpecifier.text;
32
+ if (!isRelativeImport(modulePath)) {
33
+ continue;
34
+ }
35
+ const resolved = resolveImportPath({
36
+ basedir,
37
+ importPath: modulePath,
38
+ fs
39
+ });
40
+ if (!resolved) {
41
+ continue;
42
+ }
43
+ const nameMap = new Map();
44
+ if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
45
+ for (const element of statement.exportClause.elements) {
46
+ const exportedName = element.name.text;
47
+ const sourceName = element.propertyName ? element.propertyName.text : exportedName;
48
+ nameMap.set(sourceName, exportedName);
49
+ }
50
+ }
51
+ results.push({
52
+ sourcePath: resolved,
53
+ nameMap
54
+ });
55
+ }
56
+ }
57
+ return results;
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+
4
63
  /**
5
64
  * Parse the package.json exports field and return a map of export paths to resolved file paths.
6
65
  */
@@ -76,18 +135,51 @@ export function parsePackageExports({
76
135
  });
77
136
  return exportsMap;
78
137
  }
79
-
80
138
  /**
81
139
  * Find a matching export entry for a given source file path.
82
140
  * Returns the export path (e.g., "./controllers/analytics") or null if not found.
141
+ *
142
+ * When `fs` is provided, also checks entry-point wrapper files. If an export resolves
143
+ * to a file inside a recognized entry-points folder (entry-points, entrypoints, etc.),
144
+ * the wrapper is parsed to see if it re-exports from `sourceFilePath`.
145
+ *
146
+ * `sourceExportName` is the name under which the symbol is exported from the source file
147
+ * (e.g. `'default'`). Used to look up the corresponding entry-point export name so the
148
+ * caller can generate the correct import style.
83
149
  */
84
150
  export function findExportForSourceFile({
85
151
  sourceFilePath,
86
- exportsMap
152
+ exportsMap,
153
+ fs,
154
+ sourceExportName
87
155
  }) {
88
156
  for (const [exportPath, resolvedPath] of exportsMap) {
89
157
  if (resolvedPath === sourceFilePath) {
90
- return exportPath;
158
+ return {
159
+ exportPath
160
+ };
161
+ }
162
+ }
163
+ if (fs) {
164
+ for (const [exportPath, resolvedPath] of exportsMap) {
165
+ if (isInEntryPointsFolder(resolvedPath)) {
166
+ const reExports = resolveEntryPointReExports({
167
+ entryPointFilePath: resolvedPath,
168
+ fs
169
+ });
170
+ for (const reExport of reExports) {
171
+ if (reExport.sourcePath === sourceFilePath) {
172
+ let entryPointExportName;
173
+ if (sourceExportName !== undefined && reExport.nameMap.has(sourceExportName)) {
174
+ entryPointExportName = reExport.nameMap.get(sourceExportName);
175
+ }
176
+ return {
177
+ exportPath,
178
+ entryPointExportName
179
+ };
180
+ }
181
+ }
182
+ }
91
183
  }
92
184
  }
93
185
  return null;