@atlaskit/eslint-plugin-platform 2.7.1 → 2.8.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 +14 -0
- package/dist/cjs/index.js +17 -9
- package/dist/cjs/rules/constants.js +1 -1
- package/dist/cjs/rules/ensure-critical-dependency-resolutions/index.js +5 -5
- package/dist/cjs/rules/ensure-no-private-dependencies/index.js +48 -66
- package/dist/cjs/rules/feature-gating/inline-usage/index.js +14 -3
- package/dist/cjs/rules/feature-gating/no-alias/index.js +1 -1
- package/dist/cjs/rules/feature-gating/no-module-level-eval/index.js +1 -1
- package/dist/cjs/rules/feature-gating/no-module-level-eval-nav4/index.js +1 -1
- package/dist/cjs/rules/feature-gating/no-preconditioning/index.js +4 -1
- package/dist/cjs/rules/feature-gating/prefer-fg/index.js +1 -1
- package/dist/cjs/rules/feature-gating/static-feature-flags/index.js +2 -2
- package/dist/cjs/rules/feature-gating/use-recommended-utils/index.js +1 -1
- package/dist/cjs/rules/feature-gating/valid-gate-name/index.js +60 -0
- package/dist/cjs/rules/import/no-barrel-entry-imports/index.js +871 -0
- package/dist/cjs/rules/import/no-barrel-entry-jest-mock/index.js +1384 -0
- package/dist/cjs/rules/import/no-conversation-assistant-barrel-imports/index.js +43 -0
- package/dist/cjs/rules/import/no-jest-mock-barrel-files/index.js +1401 -0
- package/dist/cjs/rules/import/no-relative-barrel-file-imports/index.js +777 -0
- package/dist/cjs/rules/import/shared/barrel-parsing.js +511 -0
- package/dist/cjs/rules/import/shared/file-system.js +186 -0
- package/dist/cjs/rules/import/shared/jest-utils.js +191 -0
- package/dist/cjs/rules/import/shared/package-registry.js +263 -0
- package/dist/cjs/rules/import/shared/package-resolution.js +185 -0
- package/dist/cjs/rules/import/shared/perf.js +89 -0
- package/dist/cjs/rules/import/shared/types.js +67 -0
- package/dist/cjs/rules/no-invalid-storybook-decorator-usage/index.js +1 -1
- package/dist/cjs/rules/no-sparse-checkout/index.js +1 -1
- package/dist/cjs/rules/prefer-crypto-random-uuid/index.js +87 -0
- package/dist/cjs/rules/use-entrypoints-in-examples/index.js +1 -1
- package/dist/es2019/index.js +17 -9
- package/dist/es2019/rules/constants.js +1 -1
- package/dist/es2019/rules/ensure-critical-dependency-resolutions/index.js +5 -5
- package/dist/es2019/rules/ensure-no-private-dependencies/index.js +10 -9
- package/dist/es2019/rules/feature-gating/inline-usage/index.js +14 -3
- package/dist/es2019/rules/feature-gating/no-alias/index.js +1 -1
- package/dist/es2019/rules/feature-gating/no-module-level-eval/index.js +1 -1
- package/dist/es2019/rules/feature-gating/no-module-level-eval-nav4/index.js +1 -1
- package/dist/es2019/rules/feature-gating/no-preconditioning/index.js +4 -1
- package/dist/es2019/rules/feature-gating/prefer-fg/index.js +1 -1
- package/dist/es2019/rules/feature-gating/static-feature-flags/index.js +2 -2
- package/dist/es2019/rules/feature-gating/use-recommended-utils/index.js +1 -1
- package/dist/es2019/rules/feature-gating/valid-gate-name/index.js +52 -0
- package/dist/es2019/rules/import/no-barrel-entry-imports/index.js +801 -0
- package/dist/es2019/rules/import/no-barrel-entry-jest-mock/index.js +1113 -0
- package/dist/es2019/rules/import/no-conversation-assistant-barrel-imports/index.js +37 -0
- package/dist/es2019/rules/import/no-jest-mock-barrel-files/index.js +1179 -0
- package/dist/es2019/rules/import/no-relative-barrel-file-imports/index.js +738 -0
- package/dist/es2019/rules/import/shared/barrel-parsing.js +433 -0
- package/dist/es2019/rules/import/shared/file-system.js +174 -0
- package/dist/es2019/rules/import/shared/jest-utils.js +159 -0
- package/dist/es2019/rules/import/shared/package-registry.js +240 -0
- package/dist/es2019/rules/import/shared/package-resolution.js +161 -0
- package/dist/es2019/rules/import/shared/perf.js +83 -0
- package/dist/es2019/rules/import/shared/types.js +57 -0
- package/dist/es2019/rules/no-invalid-storybook-decorator-usage/index.js +1 -1
- package/dist/es2019/rules/no-sparse-checkout/index.js +1 -1
- package/dist/es2019/rules/prefer-crypto-random-uuid/index.js +81 -0
- package/dist/es2019/rules/use-entrypoints-in-examples/index.js +1 -1
- package/dist/esm/index.js +17 -9
- package/dist/esm/rules/constants.js +1 -1
- package/dist/esm/rules/ensure-critical-dependency-resolutions/index.js +5 -5
- package/dist/esm/rules/ensure-no-private-dependencies/index.js +48 -65
- package/dist/esm/rules/feature-gating/inline-usage/index.js +14 -3
- package/dist/esm/rules/feature-gating/no-alias/index.js +1 -1
- package/dist/esm/rules/feature-gating/no-module-level-eval/index.js +1 -1
- package/dist/esm/rules/feature-gating/no-module-level-eval-nav4/index.js +1 -1
- package/dist/esm/rules/feature-gating/no-preconditioning/index.js +4 -1
- package/dist/esm/rules/feature-gating/prefer-fg/index.js +1 -1
- package/dist/esm/rules/feature-gating/static-feature-flags/index.js +2 -2
- package/dist/esm/rules/feature-gating/use-recommended-utils/index.js +1 -1
- package/dist/esm/rules/feature-gating/valid-gate-name/index.js +53 -0
- package/dist/esm/rules/import/no-barrel-entry-imports/index.js +864 -0
- package/dist/esm/rules/import/no-barrel-entry-jest-mock/index.js +1375 -0
- package/dist/esm/rules/import/no-conversation-assistant-barrel-imports/index.js +37 -0
- package/dist/esm/rules/import/no-jest-mock-barrel-files/index.js +1391 -0
- package/dist/esm/rules/import/no-relative-barrel-file-imports/index.js +770 -0
- package/dist/esm/rules/import/shared/barrel-parsing.js +500 -0
- package/dist/esm/rules/import/shared/file-system.js +176 -0
- package/dist/esm/rules/import/shared/jest-utils.js +179 -0
- package/dist/esm/rules/import/shared/package-registry.js +256 -0
- package/dist/esm/rules/import/shared/package-resolution.js +175 -0
- package/dist/esm/rules/import/shared/perf.js +80 -0
- package/dist/esm/rules/import/shared/types.js +61 -0
- package/dist/esm/rules/no-invalid-storybook-decorator-usage/index.js +1 -1
- package/dist/esm/rules/no-sparse-checkout/index.js +1 -1
- package/dist/esm/rules/prefer-crypto-random-uuid/index.js +81 -0
- package/dist/esm/rules/use-entrypoints-in-examples/index.js +1 -1
- package/dist/types/index.d.ts +18 -16
- package/dist/types/rules/import/no-barrel-entry-imports/index.d.ts +9 -0
- package/dist/types/rules/import/no-barrel-entry-jest-mock/index.d.ts +9 -0
- package/dist/types/rules/import/no-conversation-assistant-barrel-imports/index.d.ts +3 -0
- package/dist/types/rules/import/no-jest-mock-barrel-files/index.d.ts +22 -0
- package/dist/types/rules/import/no-relative-barrel-file-imports/index.d.ts +5 -0
- package/dist/types/rules/import/shared/barrel-parsing.d.ts +30 -0
- package/dist/types/rules/import/shared/file-system.d.ts +38 -0
- package/dist/types/rules/import/shared/jest-utils.d.ts +47 -0
- package/dist/types/rules/import/shared/package-registry.d.ts +26 -0
- package/dist/types/rules/import/shared/package-resolution.d.ts +38 -0
- package/dist/types/rules/import/shared/perf.d.ts +13 -0
- package/dist/types/rules/import/shared/types.d.ts +131 -0
- package/dist/types/rules/prefer-crypto-random-uuid/index.d.ts +3 -0
- package/dist/types-ts4.5/index.d.ts +18 -28
- package/dist/types-ts4.5/rules/import/no-barrel-entry-imports/index.d.ts +9 -0
- package/dist/types-ts4.5/rules/import/no-barrel-entry-jest-mock/index.d.ts +9 -0
- package/dist/types-ts4.5/rules/import/no-jest-mock-barrel-files/index.d.ts +22 -0
- package/dist/types-ts4.5/rules/import/no-relative-barrel-file-imports/index.d.ts +5 -0
- package/dist/types-ts4.5/rules/import/shared/barrel-parsing.d.ts +30 -0
- package/dist/types-ts4.5/rules/import/shared/file-system.d.ts +38 -0
- package/dist/types-ts4.5/rules/import/shared/jest-utils.d.ts +47 -0
- package/dist/types-ts4.5/rules/import/shared/package-registry.d.ts +26 -0
- package/dist/types-ts4.5/rules/import/shared/package-resolution.d.ts +38 -0
- package/dist/types-ts4.5/rules/import/shared/perf.d.ts +13 -0
- package/dist/types-ts4.5/rules/import/shared/types.d.ts +131 -0
- package/package.json +4 -5
- package/dist/cjs/rules/ensure-feature-flag-prefix/index.js +0 -75
- package/dist/cjs/rules/ensure-native-and-af-exports-synced/index.js +0 -158
- package/dist/es2019/rules/ensure-feature-flag-prefix/index.js +0 -65
- package/dist/es2019/rules/ensure-native-and-af-exports-synced/index.js +0 -146
- package/dist/esm/rules/ensure-feature-flag-prefix/index.js +0 -69
- package/dist/esm/rules/ensure-native-and-af-exports-synced/index.js +0 -151
- /package/dist/types/rules/{ensure-native-and-af-exports-synced → feature-gating/valid-gate-name}/index.d.ts +0 -0
- /package/dist/types-ts4.5/rules/{ensure-feature-flag-prefix → feature-gating/valid-gate-name}/index.d.ts +0 -0
- /package/dist/types-ts4.5/rules/{ensure-native-and-af-exports-synced → import/no-conversation-assistant-barrel-imports}/index.d.ts +0 -0
- /package/dist/{types/rules/ensure-feature-flag-prefix → types-ts4.5/rules/prefer-crypto-random-uuid}/index.d.ts +0 -0
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
import { dirname } from 'path';
|
|
2
|
+
import { parseBarrelExports } from '../shared/barrel-parsing';
|
|
3
|
+
import { DEFAULT_TARGET_FOLDERS, findWorkspaceRoot, isRelativeImport } from '../shared/file-system';
|
|
4
|
+
import { findPackageInRegistry, isPackageInApplyToImportsFrom } from '../shared/package-registry';
|
|
5
|
+
import { findExportForSourceFile, parsePackageExports } from '../shared/package-resolution';
|
|
6
|
+
import { realFileSystem } from '../shared/types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for the no-barrel-entry-imports rule.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Represents a Jest automock call: jest.mock('path') with no additional arguments
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Metadata for the ESLint rule
|
|
18
|
+
*/
|
|
19
|
+
const ruleMeta = {
|
|
20
|
+
type: 'problem',
|
|
21
|
+
docs: {
|
|
22
|
+
description: 'Disallow importing from barrel files in entry points.',
|
|
23
|
+
category: 'Best Practices',
|
|
24
|
+
recommended: false
|
|
25
|
+
},
|
|
26
|
+
fixable: 'code',
|
|
27
|
+
schema: [{
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
applyToImportsFrom: {
|
|
31
|
+
type: 'array',
|
|
32
|
+
items: {
|
|
33
|
+
type: 'string'
|
|
34
|
+
},
|
|
35
|
+
description: 'The folder paths (relative to workspace root) containing packages whose imports will be checked and autofixed.'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
additionalProperties: false
|
|
39
|
+
}],
|
|
40
|
+
messages: {
|
|
41
|
+
barrelEntryImport: "Importing from barrel file '{{path}}' is not allowed. Import directly from the source file using a more specific package.json export instead."
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the imported name from an ImportSpecifier, handling both Identifier and Literal
|
|
47
|
+
*/
|
|
48
|
+
function getImportedName(spec) {
|
|
49
|
+
const imported = spec.imported;
|
|
50
|
+
return imported.type === 'Identifier' ? imported.name : String(imported.value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build an import statement for a set of specifiers
|
|
55
|
+
*/
|
|
56
|
+
function buildImportStatement({
|
|
57
|
+
specs,
|
|
58
|
+
path,
|
|
59
|
+
quoteChar,
|
|
60
|
+
isTypeImport = false
|
|
61
|
+
}) {
|
|
62
|
+
const importNames = specs.map(spec => {
|
|
63
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
64
|
+
return spec.local.name;
|
|
65
|
+
} else if (spec.type === 'ImportSpecifier') {
|
|
66
|
+
const imported = getImportedName(spec);
|
|
67
|
+
const local = spec.local.name;
|
|
68
|
+
const isInlineType = spec.importKind === 'type' && !isTypeImport;
|
|
69
|
+
const prefix = isInlineType ? 'type ' : '';
|
|
70
|
+
return imported === local ? `${prefix}${imported}` : `${prefix}${imported} as ${local}`;
|
|
71
|
+
}
|
|
72
|
+
return '';
|
|
73
|
+
}).filter(name => name.length > 0);
|
|
74
|
+
if (importNames.length === 0) {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
const typeKeyword = isTypeImport ? 'type ' : '';
|
|
78
|
+
const hasDefault = specs.some(spec => spec.type === 'ImportDefaultSpecifier');
|
|
79
|
+
const hasNamed = specs.some(spec => spec.type === 'ImportSpecifier');
|
|
80
|
+
if (hasDefault && hasNamed) {
|
|
81
|
+
var _specs$find;
|
|
82
|
+
const defaultName = (_specs$find = specs.find(spec => spec.type === 'ImportDefaultSpecifier')) === null || _specs$find === void 0 ? void 0 : _specs$find.local.name;
|
|
83
|
+
const namedImports = specs.filter(spec => spec.type === 'ImportSpecifier').map(spec => {
|
|
84
|
+
const imported = getImportedName(spec);
|
|
85
|
+
const local = spec.local.name;
|
|
86
|
+
const isInlineType = spec.importKind === 'type' && !isTypeImport;
|
|
87
|
+
const prefix = isInlineType ? 'type ' : '';
|
|
88
|
+
return imported === local ? `${prefix}${imported}` : `${prefix}${imported} as ${local}`;
|
|
89
|
+
}).join(', ');
|
|
90
|
+
return `import ${typeKeyword}${defaultName}, { ${namedImports} } from ${quoteChar}${path}${quoteChar};`;
|
|
91
|
+
} else if (hasDefault) {
|
|
92
|
+
var _specs$find2;
|
|
93
|
+
const defaultName = (_specs$find2 = specs.find(spec => spec.type === 'ImportDefaultSpecifier')) === null || _specs$find2 === void 0 ? void 0 : _specs$find2.local.name;
|
|
94
|
+
return `import ${typeKeyword}${defaultName} from ${quoteChar}${path}${quoteChar};`;
|
|
95
|
+
} else {
|
|
96
|
+
return `import ${typeKeyword}{ ${importNames.join(', ')} } from ${quoteChar}${path}${quoteChar};`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Represents a specifier with its resolved target export path information.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Context resolved for an import that may be a barrel import.
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Result of classifying specifiers by their target export paths.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolves import context for barrel file analysis.
|
|
114
|
+
* Returns null if the import should not be processed (relative import, not in target folder, etc.)
|
|
115
|
+
*/
|
|
116
|
+
function resolveImportContext({
|
|
117
|
+
node,
|
|
118
|
+
workspaceRoot,
|
|
119
|
+
fs,
|
|
120
|
+
applyToImportsFrom
|
|
121
|
+
}) {
|
|
122
|
+
if (!node.source || typeof node.source.value !== 'string') {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const importPath = node.source.value;
|
|
126
|
+
|
|
127
|
+
// Skip relative imports - this rule is for cross-package imports
|
|
128
|
+
if (isRelativeImport(importPath)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Extract the base package name (without subpath)
|
|
133
|
+
// e.g., "@atlassian/conversation-assistant-instrumentation" from
|
|
134
|
+
// "@atlassian/conversation-assistant-instrumentation" or
|
|
135
|
+
// "@atlassian/conversation-assistant-instrumentation/controllers/analytics"
|
|
136
|
+
const packageNameMatch = importPath.match(/^(@[^/]+\/[^/]+)/);
|
|
137
|
+
if (!packageNameMatch) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const packageName = packageNameMatch[1];
|
|
141
|
+
const subPath = importPath.slice(packageName.length); // e.g., "" or "/controllers/analytics"
|
|
142
|
+
|
|
143
|
+
// Find the package (resolution is not constrained by applyToImportsFrom)
|
|
144
|
+
const packageDir = findPackageInRegistry({
|
|
145
|
+
packageName,
|
|
146
|
+
workspaceRoot,
|
|
147
|
+
fs
|
|
148
|
+
});
|
|
149
|
+
if (!packageDir) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Only check imports from packages in our applyToImportsFrom folders
|
|
154
|
+
if (!isPackageInApplyToImportsFrom({
|
|
155
|
+
packageDir,
|
|
156
|
+
workspaceRoot,
|
|
157
|
+
applyToImportsFrom
|
|
158
|
+
})) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Get the exports map for this package
|
|
163
|
+
const exportsMap = parsePackageExports({
|
|
164
|
+
packageDir,
|
|
165
|
+
fs
|
|
166
|
+
});
|
|
167
|
+
if (exportsMap.size === 0) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Determine which export path we're importing from
|
|
172
|
+
// For bare package imports, it's ".", for subpath imports it's "./" + subPath
|
|
173
|
+
const currentExportPath = subPath ? '.' + subPath : '.';
|
|
174
|
+
|
|
175
|
+
// Get the resolved path for the current export (the entry point file for this import)
|
|
176
|
+
const entryFilePath = exportsMap.get(currentExportPath);
|
|
177
|
+
if (!entryFilePath) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Parse the entry file to find where each export originates
|
|
182
|
+
// Pass workspaceRoot to enable cross-package re-export resolution
|
|
183
|
+
const exportMap = parseBarrelExports({
|
|
184
|
+
barrelFilePath: entryFilePath,
|
|
185
|
+
fs,
|
|
186
|
+
workspaceRoot
|
|
187
|
+
});
|
|
188
|
+
if (exportMap.size === 0) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
importPath,
|
|
193
|
+
packageName,
|
|
194
|
+
currentExportPath,
|
|
195
|
+
exportsMap,
|
|
196
|
+
exportMap
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Classifies import specifiers by their target export paths.
|
|
202
|
+
* Groups specifiers that can be remapped to more specific exports.
|
|
203
|
+
* For cross-package re-exports, suggests importing from the source package's most specific subpath.
|
|
204
|
+
*/
|
|
205
|
+
function classifySpecifiers({
|
|
206
|
+
node,
|
|
207
|
+
importContext,
|
|
208
|
+
workspaceRoot,
|
|
209
|
+
fs
|
|
210
|
+
}) {
|
|
211
|
+
const {
|
|
212
|
+
currentExportPath,
|
|
213
|
+
exportsMap,
|
|
214
|
+
exportMap
|
|
215
|
+
} = importContext;
|
|
216
|
+
const specifiers = node.specifiers;
|
|
217
|
+
const specifiersByTarget = new Map();
|
|
218
|
+
const unmappedSpecifiers = [];
|
|
219
|
+
let hasNamespaceImport = false;
|
|
220
|
+
|
|
221
|
+
// Cache for source package exports maps to avoid redundant parsing
|
|
222
|
+
const sourcePackageExportsMaps = new Map();
|
|
223
|
+
for (const spec of specifiers) {
|
|
224
|
+
if (spec.type === 'ImportNamespaceSpecifier') {
|
|
225
|
+
hasNamespaceImport = true;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
let nameInSource;
|
|
229
|
+
let kind = 'value';
|
|
230
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
231
|
+
nameInSource = 'default';
|
|
232
|
+
} else if (spec.type === 'ImportSpecifier') {
|
|
233
|
+
nameInSource = getImportedName(spec);
|
|
234
|
+
const parentImportKind = node.importKind;
|
|
235
|
+
kind = parentImportKind === 'type' || spec.importKind === 'type' ? 'type' : 'value';
|
|
236
|
+
} else {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const exportInfo = exportMap.get(nameInSource);
|
|
240
|
+
if (exportInfo) {
|
|
241
|
+
var _exportInfo$crossPack;
|
|
242
|
+
const effectiveKind = kind === 'type' || exportInfo.isTypeOnly ? 'type' : 'value';
|
|
243
|
+
|
|
244
|
+
// Check if this is a cross-package re-export
|
|
245
|
+
const sourcePackageName = (_exportInfo$crossPack = exportInfo.crossPackageSource) === null || _exportInfo$crossPack === void 0 ? void 0 : _exportInfo$crossPack.packageName;
|
|
246
|
+
if (sourcePackageName) {
|
|
247
|
+
// For cross-package re-exports, find the most specific subpath in the source package
|
|
248
|
+
// Note: Package resolution is not constrained by applyToImportsFrom - any package can be resolved
|
|
249
|
+
let sourcePackageExportsMap = sourcePackageExportsMaps.get(sourcePackageName);
|
|
250
|
+
if (!sourcePackageExportsMap) {
|
|
251
|
+
const sourcePackageDir = findPackageInRegistry({
|
|
252
|
+
packageName: sourcePackageName,
|
|
253
|
+
workspaceRoot,
|
|
254
|
+
fs
|
|
255
|
+
});
|
|
256
|
+
if (sourcePackageDir) {
|
|
257
|
+
sourcePackageExportsMap = parsePackageExports({
|
|
258
|
+
packageDir: sourcePackageDir,
|
|
259
|
+
fs
|
|
260
|
+
});
|
|
261
|
+
sourcePackageExportsMaps.set(sourcePackageName, sourcePackageExportsMap);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Find the best export path in the source package
|
|
266
|
+
let targetExportPath = null;
|
|
267
|
+
if (sourcePackageExportsMap) {
|
|
268
|
+
targetExportPath = findExportForSourceFile({
|
|
269
|
+
sourceFilePath: exportInfo.path,
|
|
270
|
+
exportsMap: sourcePackageExportsMap
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Build the full import path: @package/subpath or just @package if no subpath found
|
|
275
|
+
const targetKey = targetExportPath ? sourcePackageName + targetExportPath.slice(1) // Remove leading '.' from subpath
|
|
276
|
+
: sourcePackageName;
|
|
277
|
+
if (!specifiersByTarget.has(targetKey)) {
|
|
278
|
+
specifiersByTarget.set(targetKey, []);
|
|
279
|
+
}
|
|
280
|
+
specifiersByTarget.get(targetKey).push({
|
|
281
|
+
spec: {
|
|
282
|
+
...spec,
|
|
283
|
+
importKind: effectiveKind
|
|
284
|
+
},
|
|
285
|
+
originalName: exportInfo.originalName,
|
|
286
|
+
targetExportPath: targetKey,
|
|
287
|
+
kind: effectiveKind,
|
|
288
|
+
sourcePackageName
|
|
289
|
+
});
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Find if there's a package.json export that points to this source file
|
|
294
|
+
const targetExportPath = findExportForSourceFile({
|
|
295
|
+
sourceFilePath: exportInfo.path,
|
|
296
|
+
exportsMap
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Get the file that the current export path resolves to
|
|
300
|
+
const currentExportResolvedFile = exportsMap.get(currentExportPath);
|
|
301
|
+
|
|
302
|
+
// Skip if:
|
|
303
|
+
// 1. No target export path found
|
|
304
|
+
// 2. Target is same as current (no change needed)
|
|
305
|
+
// 3. Current export path already resolves to the same file as the source
|
|
306
|
+
// (handles multiple exports pointing to same file - avoid no-op changes)
|
|
307
|
+
const currentExportAlreadyPointsToSourceFile = currentExportResolvedFile !== undefined && currentExportResolvedFile === exportInfo.path;
|
|
308
|
+
if (targetExportPath && targetExportPath !== currentExportPath && !currentExportAlreadyPointsToSourceFile) {
|
|
309
|
+
if (!specifiersByTarget.has(targetExportPath)) {
|
|
310
|
+
specifiersByTarget.set(targetExportPath, []);
|
|
311
|
+
}
|
|
312
|
+
specifiersByTarget.get(targetExportPath).push({
|
|
313
|
+
spec: {
|
|
314
|
+
...spec,
|
|
315
|
+
importKind: effectiveKind
|
|
316
|
+
},
|
|
317
|
+
originalName: exportInfo.originalName,
|
|
318
|
+
targetExportPath,
|
|
319
|
+
kind: effectiveKind
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
// No more specific export available
|
|
323
|
+
unmappedSpecifiers.push({
|
|
324
|
+
spec: spec,
|
|
325
|
+
targetExportPath: null,
|
|
326
|
+
kind
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
unmappedSpecifiers.push({
|
|
331
|
+
spec: spec,
|
|
332
|
+
targetExportPath: null,
|
|
333
|
+
kind
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
specifiersByTarget,
|
|
339
|
+
unmappedSpecifiers,
|
|
340
|
+
hasNamespaceImport
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Transforms a specifier to use the original export name (handling aliasing).
|
|
346
|
+
* Converts named imports of default exports to ImportDefaultSpecifier.
|
|
347
|
+
*/
|
|
348
|
+
function transformSpecifierForExport({
|
|
349
|
+
spec,
|
|
350
|
+
originalName,
|
|
351
|
+
kind
|
|
352
|
+
}) {
|
|
353
|
+
if (!originalName) {
|
|
354
|
+
return spec;
|
|
355
|
+
}
|
|
356
|
+
if (originalName === 'default') {
|
|
357
|
+
// Should be ImportDefaultSpecifier
|
|
358
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
359
|
+
return spec;
|
|
360
|
+
}
|
|
361
|
+
// Convert ImportSpecifier to ImportDefaultSpecifier
|
|
362
|
+
return {
|
|
363
|
+
type: 'ImportDefaultSpecifier',
|
|
364
|
+
local: spec.local,
|
|
365
|
+
range: spec.range,
|
|
366
|
+
loc: spec.loc,
|
|
367
|
+
parent: spec.parent
|
|
368
|
+
};
|
|
369
|
+
} else {
|
|
370
|
+
// Create synthetic ImportSpecifier with correct importKind
|
|
371
|
+
return {
|
|
372
|
+
type: 'ImportSpecifier',
|
|
373
|
+
local: spec.local,
|
|
374
|
+
imported: {
|
|
375
|
+
type: 'Identifier',
|
|
376
|
+
name: originalName,
|
|
377
|
+
range: [0, 0],
|
|
378
|
+
loc: {
|
|
379
|
+
start: {
|
|
380
|
+
line: 0,
|
|
381
|
+
column: 0
|
|
382
|
+
},
|
|
383
|
+
end: {
|
|
384
|
+
line: 0,
|
|
385
|
+
column: 0
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
importKind: kind,
|
|
390
|
+
range: spec.range,
|
|
391
|
+
loc: spec.loc,
|
|
392
|
+
parent: spec.parent
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Merges new specifiers with an existing import declaration.
|
|
399
|
+
* Returns the new import statement string.
|
|
400
|
+
*/
|
|
401
|
+
function buildMergedImportStatement({
|
|
402
|
+
existingImport,
|
|
403
|
+
newSpecs,
|
|
404
|
+
newImportPath,
|
|
405
|
+
nodeImportKind,
|
|
406
|
+
quoteChar
|
|
407
|
+
}) {
|
|
408
|
+
const existingSpecs = existingImport.specifiers.map(s => {
|
|
409
|
+
if (existingImport.importKind === 'type') {
|
|
410
|
+
return {
|
|
411
|
+
...s,
|
|
412
|
+
importKind: 'type'
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return s;
|
|
416
|
+
});
|
|
417
|
+
const augmentedNewSpecs = newSpecs.map(s => {
|
|
418
|
+
if (nodeImportKind === 'type') {
|
|
419
|
+
return {
|
|
420
|
+
...s,
|
|
421
|
+
importKind: 'type'
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return s;
|
|
425
|
+
});
|
|
426
|
+
const mergedSpecs = [...existingSpecs, ...augmentedNewSpecs];
|
|
427
|
+
|
|
428
|
+
// Determine if we should use 'import type'
|
|
429
|
+
const allType = mergedSpecs.every(s => s.importKind === 'type');
|
|
430
|
+
return buildImportStatement({
|
|
431
|
+
specs: mergedSpecs,
|
|
432
|
+
path: newImportPath,
|
|
433
|
+
quoteChar,
|
|
434
|
+
isTypeImport: allType
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Check if an ExpressionStatement is a Jest automock: jest.mock('path') with exactly one string argument.
|
|
440
|
+
* Returns the JestAutomock info if it is, null otherwise.
|
|
441
|
+
*/
|
|
442
|
+
function getJestAutomock(node) {
|
|
443
|
+
if (node.type !== 'ExpressionStatement') {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const statement = node;
|
|
447
|
+
const expr = statement.expression;
|
|
448
|
+
if (expr.type !== 'CallExpression') {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Check for jest.mock(...)
|
|
453
|
+
const callee = expr.callee;
|
|
454
|
+
if (callee.type !== 'MemberExpression' || callee.object.type !== 'Identifier' || callee.object.name !== 'jest' || callee.property.type !== 'Identifier' || callee.property.name !== 'mock') {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Must have exactly one argument (automock = no factory function)
|
|
459
|
+
if (expr.arguments.length !== 1) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
const arg = expr.arguments[0];
|
|
463
|
+
if (arg.type !== 'Literal' || typeof arg.value !== 'string') {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Get the quote character from the raw value
|
|
468
|
+
const raw = arg.raw || `'${arg.value}'`;
|
|
469
|
+
const quoteChar = raw[0];
|
|
470
|
+
return {
|
|
471
|
+
statementNode: statement,
|
|
472
|
+
path: arg.value,
|
|
473
|
+
quoteChar
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Find all Jest automocks in the AST that match the given import path.
|
|
479
|
+
*/
|
|
480
|
+
function findMatchingAutomocks({
|
|
481
|
+
sourceCode,
|
|
482
|
+
importPath
|
|
483
|
+
}) {
|
|
484
|
+
const automocks = [];
|
|
485
|
+
const ast = sourceCode.ast;
|
|
486
|
+
for (const statement of ast.body) {
|
|
487
|
+
const automock = getJestAutomock(statement);
|
|
488
|
+
if (automock && automock.path === importPath) {
|
|
489
|
+
automocks.push(automock);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return automocks;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Build a jest.mock() statement string
|
|
497
|
+
*/
|
|
498
|
+
function buildAutomockStatement({
|
|
499
|
+
path,
|
|
500
|
+
quoteChar
|
|
501
|
+
}) {
|
|
502
|
+
return `jest.mock(${quoteChar}${path}${quoteChar});`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Creates a fix to remove a node with proper whitespace handling.
|
|
507
|
+
* Removes surrounding newlines to avoid leaving blank lines.
|
|
508
|
+
*/
|
|
509
|
+
function createNodeRemovalFix({
|
|
510
|
+
fixer,
|
|
511
|
+
node,
|
|
512
|
+
sourceCode
|
|
513
|
+
}) {
|
|
514
|
+
const nodeStart = node.range[0];
|
|
515
|
+
const nodeEnd = node.range[1];
|
|
516
|
+
|
|
517
|
+
// Check for leading newline (prefer removing the line separator before the node)
|
|
518
|
+
const textBeforeNode = sourceCode.text.slice(0, nodeStart);
|
|
519
|
+
const leadingNewlineMatch = textBeforeNode.match(/(\r?\n)$/);
|
|
520
|
+
if (leadingNewlineMatch) {
|
|
521
|
+
// Remove the leading newline plus the node
|
|
522
|
+
return fixer.removeRange([nodeStart - leadingNewlineMatch[1].length, nodeEnd]);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// No leading newline - check for trailing newline
|
|
526
|
+
const textAfterNode = sourceCode.text.slice(nodeEnd);
|
|
527
|
+
const trailingNewlineMatch = textAfterNode.match(/^(\r?\n)/);
|
|
528
|
+
if (trailingNewlineMatch) {
|
|
529
|
+
return fixer.removeRange([nodeStart, nodeEnd + trailingNewlineMatch[1].length]);
|
|
530
|
+
}
|
|
531
|
+
return fixer.remove(node);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Creates the auto-fix for barrel import violations.
|
|
536
|
+
* Generates new import statements and handles merging with existing imports.
|
|
537
|
+
* Also updates Jest automocks (jest.mock calls with only a path) when present.
|
|
538
|
+
*/
|
|
539
|
+
function createBarrelImportFix({
|
|
540
|
+
fixer,
|
|
541
|
+
node,
|
|
542
|
+
context,
|
|
543
|
+
importContext,
|
|
544
|
+
specifiersByTarget,
|
|
545
|
+
unmappedSpecifiers
|
|
546
|
+
}) {
|
|
547
|
+
const {
|
|
548
|
+
importPath,
|
|
549
|
+
packageName
|
|
550
|
+
} = importContext;
|
|
551
|
+
const sourceCode = context.sourceCode;
|
|
552
|
+
const quote = sourceCode.getText(node.source)[0]; // Get quote character
|
|
553
|
+
|
|
554
|
+
const fixes = [];
|
|
555
|
+
const newStatements = [];
|
|
556
|
+
|
|
557
|
+
// Find any Jest automocks that match this import path
|
|
558
|
+
const automocks = findMatchingAutomocks({
|
|
559
|
+
sourceCode,
|
|
560
|
+
importPath
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Track which new import paths need automocks (only value imports, not type-only)
|
|
564
|
+
const automockPaths = [];
|
|
565
|
+
|
|
566
|
+
// Track if we have any value imports at all (to determine if automocks should be updated)
|
|
567
|
+
let hasAnyValueImports = false;
|
|
568
|
+
|
|
569
|
+
// Get all existing imports to check for merging
|
|
570
|
+
const allImports = sourceCode.ast.body.filter(n => n.type === 'ImportDeclaration' && n !== node);
|
|
571
|
+
|
|
572
|
+
// Generate new import statements for each target export path
|
|
573
|
+
for (const [targetExportPath, specsWithTarget] of specifiersByTarget) {
|
|
574
|
+
// Check if this is a cross-package re-export (sourcePackageName is set)
|
|
575
|
+
const isCrossPackage = specsWithTarget.some(s => s.sourcePackageName);
|
|
576
|
+
const newImportPath = isCrossPackage ? targetExportPath // For cross-package, targetExportPath is already the full import path (e.g., @package/subpath)
|
|
577
|
+
: packageName + targetExportPath.slice(1); // Remove leading '.' for same-package imports
|
|
578
|
+
|
|
579
|
+
// Transform specifiers if needed (handle aliasing)
|
|
580
|
+
const specs = specsWithTarget.map(({
|
|
581
|
+
spec,
|
|
582
|
+
originalName,
|
|
583
|
+
kind
|
|
584
|
+
}) => transformSpecifierForExport({
|
|
585
|
+
spec,
|
|
586
|
+
originalName,
|
|
587
|
+
kind
|
|
588
|
+
}));
|
|
589
|
+
|
|
590
|
+
// Check if any specifier in this group is a value import (not type-only)
|
|
591
|
+
// Only add automock paths for value imports (types don't need mocking at runtime)
|
|
592
|
+
if (automocks.length > 0) {
|
|
593
|
+
const hasValueImport = specsWithTarget.some(({
|
|
594
|
+
kind,
|
|
595
|
+
spec
|
|
596
|
+
}) => kind === 'value' && (spec.type !== 'ImportSpecifier' || spec.importKind !== 'type'));
|
|
597
|
+
if (hasValueImport) {
|
|
598
|
+
hasAnyValueImports = true;
|
|
599
|
+
automockPaths.push(newImportPath);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Check for existing import from the same path
|
|
604
|
+
const existingImport = allImports.find(n => n.source.value === newImportPath);
|
|
605
|
+
|
|
606
|
+
// Skip merging if existing is namespace import
|
|
607
|
+
const isNamespace = existingImport === null || existingImport === void 0 ? void 0 : existingImport.specifiers.some(s => s.type === 'ImportNamespaceSpecifier');
|
|
608
|
+
if (existingImport && !isNamespace) {
|
|
609
|
+
// Merge with existing import
|
|
610
|
+
const newImportStatement = buildMergedImportStatement({
|
|
611
|
+
existingImport,
|
|
612
|
+
newSpecs: specs,
|
|
613
|
+
newImportPath,
|
|
614
|
+
nodeImportKind: node.importKind,
|
|
615
|
+
quoteChar: quote
|
|
616
|
+
});
|
|
617
|
+
if (newImportStatement.length > 0) {
|
|
618
|
+
fixes.push(fixer.replaceText(existingImport, newImportStatement));
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
// Create new import
|
|
622
|
+
const isTypeImport = node.importKind === 'type';
|
|
623
|
+
const importStatement = buildImportStatement({
|
|
624
|
+
specs,
|
|
625
|
+
path: newImportPath,
|
|
626
|
+
quoteChar: quote,
|
|
627
|
+
isTypeImport
|
|
628
|
+
});
|
|
629
|
+
if (importStatement.length > 0) {
|
|
630
|
+
newStatements.push(importStatement);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Handle unmapped specifiers - they stay in the original import
|
|
636
|
+
if (unmappedSpecifiers.length > 0) {
|
|
637
|
+
const unmappedSpecs = unmappedSpecifiers.map(u => u.spec);
|
|
638
|
+
const isTypeImport = node.importKind === 'type';
|
|
639
|
+
const remainingImport = buildImportStatement({
|
|
640
|
+
specs: unmappedSpecs,
|
|
641
|
+
path: importPath,
|
|
642
|
+
quoteChar: quote,
|
|
643
|
+
isTypeImport
|
|
644
|
+
});
|
|
645
|
+
if (remainingImport.length > 0) {
|
|
646
|
+
newStatements.push(remainingImport);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// If there are unmapped value specifiers and automocks, keep the original automock path too
|
|
650
|
+
if (automocks.length > 0) {
|
|
651
|
+
const hasUnmappedValueImport = unmappedSpecifiers.some(({
|
|
652
|
+
kind,
|
|
653
|
+
spec
|
|
654
|
+
}) => kind === 'value' && (spec.type !== 'ImportSpecifier' || spec.importKind !== 'type'));
|
|
655
|
+
if (hasUnmappedValueImport) {
|
|
656
|
+
hasAnyValueImports = true;
|
|
657
|
+
automockPaths.push(importPath);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (newStatements.length > 0) {
|
|
662
|
+
fixes.push(fixer.replaceText(node, newStatements.join('\n')));
|
|
663
|
+
} else {
|
|
664
|
+
// If all were merged, remove the node including surrounding whitespace/newlines
|
|
665
|
+
fixes.push(createNodeRemovalFix({
|
|
666
|
+
fixer,
|
|
667
|
+
node,
|
|
668
|
+
sourceCode
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Handle automock updates
|
|
673
|
+
// Only modify automocks if there are value imports being fixed
|
|
674
|
+
// Type-only imports don't need runtime mocking, so we preserve existing automocks
|
|
675
|
+
if (automocks.length > 0 && hasAnyValueImports && automockPaths.length > 0) {
|
|
676
|
+
for (const automock of automocks) {
|
|
677
|
+
// Build new automock statements for all new paths
|
|
678
|
+
const newAutomockStatements = automockPaths.map(path => buildAutomockStatement({
|
|
679
|
+
path,
|
|
680
|
+
quoteChar: automock.quoteChar
|
|
681
|
+
}));
|
|
682
|
+
|
|
683
|
+
// Replace the original automock statement with the new automock(s)
|
|
684
|
+
fixes.push(fixer.replaceTextRange(automock.statementNode.range, newAutomockStatements.join('\n')));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return fixes;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Handles an ImportDeclaration node to check for barrel file imports.
|
|
692
|
+
* Reports and auto-fixes imports that could use more specific export paths.
|
|
693
|
+
*/
|
|
694
|
+
function handleImportDeclaration({
|
|
695
|
+
node,
|
|
696
|
+
context,
|
|
697
|
+
workspaceRoot,
|
|
698
|
+
fs,
|
|
699
|
+
applyToImportsFrom
|
|
700
|
+
}) {
|
|
701
|
+
// Resolve import context (validates and extracts package/export info)
|
|
702
|
+
// applyToImportsFrom is used here to filter which packages the rule applies to
|
|
703
|
+
const importContext = resolveImportContext({
|
|
704
|
+
node,
|
|
705
|
+
workspaceRoot,
|
|
706
|
+
fs,
|
|
707
|
+
applyToImportsFrom
|
|
708
|
+
});
|
|
709
|
+
if (!importContext) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Check each imported specifier to see if we can find a more specific export
|
|
714
|
+
if (node.specifiers.length === 0) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Classify specifiers by their target export paths
|
|
719
|
+
const {
|
|
720
|
+
specifiersByTarget,
|
|
721
|
+
unmappedSpecifiers,
|
|
722
|
+
hasNamespaceImport
|
|
723
|
+
} = classifySpecifiers({
|
|
724
|
+
node,
|
|
725
|
+
importContext,
|
|
726
|
+
workspaceRoot,
|
|
727
|
+
fs
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// If namespace import, report without auto-fix if there are specific exports available
|
|
731
|
+
if (hasNamespaceImport) {
|
|
732
|
+
if (specifiersByTarget.size > 0) {
|
|
733
|
+
context.report({
|
|
734
|
+
node,
|
|
735
|
+
messageId: 'barrelEntryImport',
|
|
736
|
+
data: {
|
|
737
|
+
path: importContext.importPath
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// If no specifiers can be remapped to more specific imports, don't report
|
|
745
|
+
if (specifiersByTarget.size === 0) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Report with auto-fix
|
|
750
|
+
context.report({
|
|
751
|
+
node,
|
|
752
|
+
messageId: 'barrelEntryImport',
|
|
753
|
+
data: {
|
|
754
|
+
path: importContext.importPath
|
|
755
|
+
},
|
|
756
|
+
fix(fixer) {
|
|
757
|
+
return createBarrelImportFix({
|
|
758
|
+
fixer,
|
|
759
|
+
node,
|
|
760
|
+
context,
|
|
761
|
+
importContext,
|
|
762
|
+
specifiersByTarget,
|
|
763
|
+
unmappedSpecifiers
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Factory function to create the ESLint rule with a given file system.
|
|
771
|
+
* This enables testing with mock file systems.
|
|
772
|
+
*/
|
|
773
|
+
export function createRule(fs) {
|
|
774
|
+
return {
|
|
775
|
+
meta: ruleMeta,
|
|
776
|
+
create(context) {
|
|
777
|
+
var _options$applyToImpor;
|
|
778
|
+
const options = context.options[0] || {};
|
|
779
|
+
const applyToImportsFrom = (_options$applyToImpor = options.applyToImportsFrom) !== null && _options$applyToImpor !== void 0 ? _options$applyToImpor : DEFAULT_TARGET_FOLDERS;
|
|
780
|
+
const workspaceRoot = findWorkspaceRoot({
|
|
781
|
+
startPath: dirname(context.filename),
|
|
782
|
+
fs,
|
|
783
|
+
applyToImportsFrom
|
|
784
|
+
});
|
|
785
|
+
return {
|
|
786
|
+
ImportDeclaration(rawNode) {
|
|
787
|
+
const node = rawNode;
|
|
788
|
+
handleImportDeclaration({
|
|
789
|
+
node,
|
|
790
|
+
context,
|
|
791
|
+
workspaceRoot,
|
|
792
|
+
fs,
|
|
793
|
+
applyToImportsFrom
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
const rule = createRule(realFileSystem);
|
|
801
|
+
export default rule;
|