@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.
- package/CHANGELOG.md +9 -0
- package/dist/cjs/index.js +6 -1
- package/dist/cjs/rules/ensure-use-sync-external-store-server-snapshot/index.js +41 -0
- package/dist/cjs/rules/import/no-barrel-entry-imports/index.js +475 -67
- package/dist/cjs/rules/import/no-barrel-entry-jest-mock/index.js +387 -112
- package/dist/cjs/rules/import/no-jest-mock-barrel-files/index.js +3 -2
- package/dist/cjs/rules/import/no-relative-barrel-file-imports/index.js +7 -3
- package/dist/cjs/rules/import/shared/jest-utils.js +62 -9
- package/dist/cjs/rules/import/shared/package-resolution.js +156 -23
- package/dist/cjs/rules/visit-example-type-import-required/index.js +409 -0
- package/dist/es2019/index.js +6 -1
- package/dist/es2019/rules/ensure-use-sync-external-store-server-snapshot/index.js +43 -0
- package/dist/es2019/rules/import/no-barrel-entry-imports/index.js +372 -15
- package/dist/es2019/rules/import/no-barrel-entry-jest-mock/index.js +245 -17
- package/dist/es2019/rules/import/no-jest-mock-barrel-files/index.js +3 -2
- package/dist/es2019/rules/import/no-relative-barrel-file-imports/index.js +7 -3
- package/dist/es2019/rules/import/shared/jest-utils.js +44 -0
- package/dist/es2019/rules/import/shared/package-resolution.js +97 -5
- package/dist/es2019/rules/visit-example-type-import-required/index.js +375 -0
- package/dist/esm/index.js +6 -1
- package/dist/esm/rules/ensure-use-sync-external-store-server-snapshot/index.js +35 -0
- package/dist/esm/rules/import/no-barrel-entry-imports/index.js +475 -67
- package/dist/esm/rules/import/no-barrel-entry-jest-mock/index.js +388 -113
- package/dist/esm/rules/import/no-jest-mock-barrel-files/index.js +3 -2
- package/dist/esm/rules/import/no-relative-barrel-file-imports/index.js +7 -3
- package/dist/esm/rules/import/shared/jest-utils.js +61 -9
- package/dist/esm/rules/import/shared/package-resolution.js +156 -25
- package/dist/esm/rules/visit-example-type-import-required/index.js +402 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/rules/ensure-use-sync-external-store-server-snapshot/index.d.ts +3 -0
- package/dist/types/rules/import/shared/jest-utils.d.ts +8 -0
- package/dist/types/rules/import/shared/package-resolution.d.ts +22 -2
- package/dist/types/rules/visit-example-type-import-required/index.d.ts +4 -0
- package/dist/types-ts4.5/index.d.ts +12 -0
- package/dist/types-ts4.5/rules/ensure-use-sync-external-store-server-snapshot/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/import/shared/jest-utils.d.ts +8 -0
- package/dist/types-ts4.5/rules/import/shared/package-resolution.d.ts +22 -2
- package/dist/types-ts4.5/rules/visit-example-type-import-required/index.d.ts +4 -0
- 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 =
|
|
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
|
|
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
|
|
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
|
-
//
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|