@atlaskit/eslint-plugin-platform 2.9.0 → 2.9.2
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 +17 -0
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/rules/compiled/no-css-prop-in-object-spread/index.js +162 -0
- package/dist/cjs/rules/ensure-critical-dependency-resolutions/index.js +0 -1
- package/dist/cjs/rules/import/no-barrel-entry-imports/index.js +68 -16
- package/dist/cjs/rules/import/no-barrel-entry-jest-mock/index.js +42 -8
- package/dist/cjs/rules/import/shared/package-resolution.js +153 -8
- package/dist/cjs/rules/no-restricted-fedramp-imports/index.js +65 -0
- package/dist/cjs/rules/no-xcss-in-cx/index.js +221 -0
- package/dist/cjs/rules/visit-example-type-import-required/index.js +24 -14
- package/dist/es2019/index.js +8 -2
- package/dist/es2019/rules/compiled/no-css-prop-in-object-spread/index.js +136 -0
- package/dist/es2019/rules/ensure-critical-dependency-resolutions/index.js +0 -1
- package/dist/es2019/rules/import/no-barrel-entry-imports/index.js +66 -17
- package/dist/es2019/rules/import/no-barrel-entry-jest-mock/index.js +43 -9
- package/dist/es2019/rules/import/shared/package-resolution.js +119 -4
- package/dist/es2019/rules/no-restricted-fedramp-imports/index.js +47 -0
- package/dist/es2019/rules/no-xcss-in-cx/index.js +187 -0
- package/dist/es2019/rules/visit-example-type-import-required/index.js +24 -15
- package/dist/esm/index.js +8 -2
- package/dist/esm/rules/compiled/no-css-prop-in-object-spread/index.js +156 -0
- package/dist/esm/rules/ensure-critical-dependency-resolutions/index.js +0 -1
- package/dist/esm/rules/import/no-barrel-entry-imports/index.js +69 -17
- package/dist/esm/rules/import/no-barrel-entry-jest-mock/index.js +43 -9
- package/dist/esm/rules/import/shared/package-resolution.js +151 -8
- package/dist/esm/rules/no-restricted-fedramp-imports/index.js +59 -0
- package/dist/esm/rules/no-xcss-in-cx/index.js +216 -0
- package/dist/esm/rules/visit-example-type-import-required/index.js +24 -14
- package/dist/types/index.d.ts +278 -241
- package/dist/types/rules/compiled/no-css-prop-in-object-spread/index.d.ts +3 -0
- package/dist/types/rules/import/shared/package-resolution.d.ts +25 -0
- package/dist/types/rules/no-restricted-fedramp-imports/index.d.ts +3 -0
- package/dist/types/rules/no-xcss-in-cx/index.d.ts +31 -0
- package/dist/types-ts4.5/index.d.ts +222 -209
- package/dist/types-ts4.5/rules/compiled/no-css-prop-in-object-spread/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/import/shared/package-resolution.d.ts +25 -0
- package/dist/types-ts4.5/rules/no-restricted-fedramp-imports/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-xcss-in-cx/index.d.ts +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getScope, getSourceCode } from '../../util/context-compat';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the `css` Property node from an ObjectExpression, or null if not found.
|
|
5
|
+
*/
|
|
6
|
+
function getCssProperty(objectExpression) {
|
|
7
|
+
for (const prop of objectExpression.properties) {
|
|
8
|
+
if (prop.type !== 'Property') {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const {
|
|
12
|
+
key
|
|
13
|
+
} = prop;
|
|
14
|
+
if (key.type === 'Identifier' && key.name === 'css' || key.type === 'Literal' && key.value === 'css') {
|
|
15
|
+
return prop;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export const noCssPropInObjectSpread = {
|
|
21
|
+
meta: {
|
|
22
|
+
docs: {
|
|
23
|
+
url: 'https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/platform/eslint-plugin/src/rules/compiled/no-css-prop-in-object-spread/',
|
|
24
|
+
description: 'Disallows `css` property inside objects spread into JSX — the Compiled JSX pragma ignores it'
|
|
25
|
+
},
|
|
26
|
+
fixable: 'code',
|
|
27
|
+
messages: {
|
|
28
|
+
noCssPropInObjectSpread: 'The `css` property inside an object spread into JSX is a no-op. The Compiled JSX pragma only processes `css` as a direct JSX attribute. Move `css` out of the spread: <El css={...} />'
|
|
29
|
+
},
|
|
30
|
+
type: 'problem'
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
return {
|
|
34
|
+
JSXSpreadAttribute(node) {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
const spreadNode = node;
|
|
37
|
+
const arg = spreadNode.argument;
|
|
38
|
+
|
|
39
|
+
// Case 1: inline object literal — <div {...{ css: styles, id: 'foo' }} />
|
|
40
|
+
if (arg.type === 'ObjectExpression') {
|
|
41
|
+
const objectArg = arg;
|
|
42
|
+
const cssProp = getCssProperty(objectArg);
|
|
43
|
+
if (!cssProp) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
context.report({
|
|
47
|
+
node,
|
|
48
|
+
messageId: 'noCssPropInObjectSpread',
|
|
49
|
+
fix(fixer) {
|
|
50
|
+
const sourceCode = getSourceCode(context);
|
|
51
|
+
const cssValueText = sourceCode.getText(cssProp.value);
|
|
52
|
+
const remainingProps = objectArg.properties.filter(p => p !== cssProp);
|
|
53
|
+
const directCssProp = `css={${cssValueText}}`;
|
|
54
|
+
if (remainingProps.length === 0) {
|
|
55
|
+
return fixer.replaceText(node, directCssProp);
|
|
56
|
+
}
|
|
57
|
+
const remainingText = remainingProps.map(p => sourceCode.getText(p)).join(', ');
|
|
58
|
+
return fixer.replaceText(node, `${directCssProp} {...{ ${remainingText} }}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Case 2: variable reference — <div {...props} />
|
|
65
|
+
if (arg.type === 'Identifier') {
|
|
66
|
+
const scope = getScope(context, arg);
|
|
67
|
+
let currentScope = scope;
|
|
68
|
+
let variable = null;
|
|
69
|
+
while (currentScope) {
|
|
70
|
+
const found = currentScope.variables.find(v => v.name === arg.name);
|
|
71
|
+
if (found) {
|
|
72
|
+
variable = found;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
currentScope = currentScope.upper;
|
|
76
|
+
}
|
|
77
|
+
if (!variable || variable.defs.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const def = variable.defs[0];
|
|
81
|
+
if (def.type !== 'Variable' || !def.node.init || def.node.init.type !== 'ObjectExpression') {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const initObject = def.node.init;
|
|
85
|
+
const cssProp = getCssProperty(initObject);
|
|
86
|
+
if (!cssProp) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Only auto-fix when there is exactly one JSX spread site for this variable
|
|
91
|
+
const spreadCount = variable.references.filter(ref => {
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
const refParent = ref.identifier.parent;
|
|
94
|
+
return (refParent === null || refParent === void 0 ? void 0 : refParent.type) === 'JSXSpreadAttribute';
|
|
95
|
+
}).length;
|
|
96
|
+
context.report({
|
|
97
|
+
node,
|
|
98
|
+
messageId: 'noCssPropInObjectSpread',
|
|
99
|
+
...(spreadCount === 1 ? {
|
|
100
|
+
fix(fixer) {
|
|
101
|
+
const sourceCode = getSourceCode(context);
|
|
102
|
+
const cssValueText = sourceCode.getText(cssProp.value);
|
|
103
|
+
const fixes = [];
|
|
104
|
+
const remainingProps = initObject.properties.filter(p => p !== cssProp);
|
|
105
|
+
if (remainingProps.length === 0) {
|
|
106
|
+
fixes.push(fixer.replaceText(initObject, '{}'));
|
|
107
|
+
} else {
|
|
108
|
+
const propIndex = initObject.properties.indexOf(cssProp);
|
|
109
|
+
const isLast = propIndex === initObject.properties.length - 1;
|
|
110
|
+
const tokenBefore = sourceCode.getTokenBefore(cssProp);
|
|
111
|
+
const tokenAfter = sourceCode.getTokenAfter(cssProp);
|
|
112
|
+
if (!isLast && tokenAfter && tokenAfter.value === ',') {
|
|
113
|
+
const src = sourceCode.getText();
|
|
114
|
+
const afterEnd = tokenAfter.range[1];
|
|
115
|
+
let end = afterEnd;
|
|
116
|
+
while (end < src.length && src[end] === ' ') {
|
|
117
|
+
end++;
|
|
118
|
+
}
|
|
119
|
+
fixes.push(fixer.removeRange([cssProp.range[0], end]));
|
|
120
|
+
} else if (tokenBefore && tokenBefore.value === ',') {
|
|
121
|
+
fixes.push(fixer.removeRange([tokenBefore.range[0], cssProp.range[1]]));
|
|
122
|
+
} else {
|
|
123
|
+
fixes.push(fixer.remove(cssProp));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
fixes.push(fixer.insertTextBefore(node, `css={${cssValueText}} `));
|
|
127
|
+
return fixes;
|
|
128
|
+
}
|
|
129
|
+
} : {})
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
export default noCssPropInObjectSpread;
|
|
@@ -11,7 +11,6 @@ const DESIRED_PKG_VERSIONS = {
|
|
|
11
11
|
tslib: ['2.6', '2.8'],
|
|
12
12
|
'@types/react': ['16.14', '18.2', '18.3'],
|
|
13
13
|
'react-relay': ['npm:atl-react-relay@0.0.0-main-39e79f66'],
|
|
14
|
-
'relay-compiler': ['npm:atl-relay-compiler@0.0.0-main-39e79f66'],
|
|
15
14
|
'relay-runtime': ['npm:atl-relay-runtime@0.0.0-main-39e79f66'],
|
|
16
15
|
'relay-test-utils': ['npm:atl-relay-test-utils@0.0.0-main-39e79f66']
|
|
17
16
|
};
|
|
@@ -2,7 +2,7 @@ import { dirname } from 'path';
|
|
|
2
2
|
import { parseBarrelExports } from '../shared/barrel-parsing';
|
|
3
3
|
import { DEFAULT_TARGET_FOLDERS, findWorkspaceRoot, isRelativeImport } from '../shared/file-system';
|
|
4
4
|
import { findPackageInRegistry, isPackageInApplyToImportsFrom } from '../shared/package-registry';
|
|
5
|
-
import { findExportForSourceFile, parsePackageExports } from '../shared/package-resolution';
|
|
5
|
+
import { findCrossPackageBridgeExportPath, findExportForSourceFile, parsePackageExports } from '../shared/package-resolution';
|
|
6
6
|
import { realFileSystem } from '../shared/types';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -33,6 +33,10 @@ const ruleMeta = {
|
|
|
33
33
|
type: 'string'
|
|
34
34
|
},
|
|
35
35
|
description: 'The folder paths (relative to workspace root) containing packages whose imports will be checked and autofixed.'
|
|
36
|
+
},
|
|
37
|
+
preferImportedPackageSubpath: {
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
description: 'Prefer subpaths on the imported barrel package when they bridge to the dependency (e.g. @scope/pkg/subpath instead of @scope/dependency).'
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
additionalProperties: false
|
|
@@ -222,12 +226,14 @@ function classifySpecifiers({
|
|
|
222
226
|
node,
|
|
223
227
|
importContext,
|
|
224
228
|
workspaceRoot,
|
|
225
|
-
fs
|
|
229
|
+
fs,
|
|
230
|
+
preferImportedPackageSubpath
|
|
226
231
|
}) {
|
|
227
232
|
const {
|
|
228
233
|
currentExportPath,
|
|
229
234
|
exportsMap,
|
|
230
|
-
exportMap
|
|
235
|
+
exportMap,
|
|
236
|
+
packageName: importedPackageName
|
|
231
237
|
} = importContext;
|
|
232
238
|
const specifiers = node.specifiers;
|
|
233
239
|
const specifiersByTarget = new Map();
|
|
@@ -245,6 +251,7 @@ function classifySpecifiers({
|
|
|
245
251
|
let kind = 'value';
|
|
246
252
|
if (spec.type === 'ImportDefaultSpecifier') {
|
|
247
253
|
nameInSource = 'default';
|
|
254
|
+
kind = node.importKind === 'type' ? 'type' : 'value';
|
|
248
255
|
} else if (spec.type === 'ImportSpecifier') {
|
|
249
256
|
nameInSource = getImportedName(spec);
|
|
250
257
|
const parentImportKind = node.importKind;
|
|
@@ -260,6 +267,37 @@ function classifySpecifiers({
|
|
|
260
267
|
// Check if this is a cross-package re-export
|
|
261
268
|
const sourcePackageName = (_exportInfo$crossPack = exportInfo.crossPackageSource) === null || _exportInfo$crossPack === void 0 ? void 0 : _exportInfo$crossPack.packageName;
|
|
262
269
|
if (sourcePackageName) {
|
|
270
|
+
let targetKey;
|
|
271
|
+
let resolvedOriginalName = exportInfo.originalName;
|
|
272
|
+
if (preferImportedPackageSubpath) {
|
|
273
|
+
const bridge = findCrossPackageBridgeExportPath({
|
|
274
|
+
exportsMap,
|
|
275
|
+
crossPackageName: sourcePackageName,
|
|
276
|
+
exportedName: nameInSource,
|
|
277
|
+
fs
|
|
278
|
+
});
|
|
279
|
+
if (bridge) {
|
|
280
|
+
targetKey = importedPackageName + bridge.exportPath.slice(1);
|
|
281
|
+
if (bridge.entryPointExportName !== undefined) {
|
|
282
|
+
resolvedOriginalName = bridge.entryPointExportName === nameInSource ? undefined : bridge.entryPointExportName;
|
|
283
|
+
}
|
|
284
|
+
if (!specifiersByTarget.has(targetKey)) {
|
|
285
|
+
specifiersByTarget.set(targetKey, []);
|
|
286
|
+
}
|
|
287
|
+
specifiersByTarget.get(targetKey).push({
|
|
288
|
+
spec: {
|
|
289
|
+
...spec,
|
|
290
|
+
importKind: effectiveKind
|
|
291
|
+
},
|
|
292
|
+
originalName: resolvedOriginalName,
|
|
293
|
+
targetExportPath: targetKey,
|
|
294
|
+
kind: effectiveKind,
|
|
295
|
+
sourcePackageName
|
|
296
|
+
});
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
263
301
|
// For cross-package re-exports, find the most specific subpath in the source package
|
|
264
302
|
// Note: Package resolution is not constrained by applyToImportsFrom - any package can be resolved
|
|
265
303
|
let sourcePackageExportsMap = sourcePackageExportsMaps.get(sourcePackageName);
|
|
@@ -280,7 +318,6 @@ function classifySpecifiers({
|
|
|
280
318
|
|
|
281
319
|
// Find the best export path in the source package
|
|
282
320
|
let targetExportPath = null;
|
|
283
|
-
let resolvedOriginalName = exportInfo.originalName;
|
|
284
321
|
if (sourcePackageExportsMap) {
|
|
285
322
|
var _exportInfo$originalN, _matchResult$exportPa;
|
|
286
323
|
const sourceExportName = (_exportInfo$originalN = exportInfo.originalName) !== null && _exportInfo$originalN !== void 0 ? _exportInfo$originalN : nameInSource;
|
|
@@ -297,7 +334,7 @@ function classifySpecifiers({
|
|
|
297
334
|
}
|
|
298
335
|
|
|
299
336
|
// Build the full import path: @package/subpath or just @package if no subpath found
|
|
300
|
-
|
|
337
|
+
targetKey = targetExportPath ? sourcePackageName + targetExportPath.slice(1) // Remove leading '.' from subpath
|
|
301
338
|
: sourcePackageName;
|
|
302
339
|
if (!specifiersByTarget.has(targetKey)) {
|
|
303
340
|
specifiersByTarget.set(targetKey, []);
|
|
@@ -652,7 +689,8 @@ function createBarrelImportFix({
|
|
|
652
689
|
}
|
|
653
690
|
} else {
|
|
654
691
|
// Create new import
|
|
655
|
-
const
|
|
692
|
+
const allSpecsAreType = specsWithTarget.every(s => s.kind === 'type');
|
|
693
|
+
const isTypeImport = node.importKind === 'type' || allSpecsAreType;
|
|
656
694
|
const importStatement = buildImportStatement({
|
|
657
695
|
specs,
|
|
658
696
|
path: newImportPath,
|
|
@@ -668,7 +706,8 @@ function createBarrelImportFix({
|
|
|
668
706
|
// Handle unmapped specifiers - they stay in the original import
|
|
669
707
|
if (unmappedSpecifiers.length > 0) {
|
|
670
708
|
const unmappedSpecs = unmappedSpecifiers.map(u => u.spec);
|
|
671
|
-
const
|
|
709
|
+
const allUnmappedAreType = unmappedSpecifiers.every(u => u.kind === 'type');
|
|
710
|
+
const isTypeImport = node.importKind === 'type' || allUnmappedAreType;
|
|
672
711
|
const remainingImport = buildImportStatement({
|
|
673
712
|
specs: unmappedSpecs,
|
|
674
713
|
path: importPath,
|
|
@@ -814,7 +853,8 @@ function handleRequireMemberExpression({
|
|
|
814
853
|
context,
|
|
815
854
|
workspaceRoot,
|
|
816
855
|
fs,
|
|
817
|
-
applyToImportsFrom
|
|
856
|
+
applyToImportsFrom,
|
|
857
|
+
preferImportedPackageSubpath
|
|
818
858
|
}) {
|
|
819
859
|
if (node.computed || node.property.type !== 'Identifier') {
|
|
820
860
|
return;
|
|
@@ -842,7 +882,8 @@ function handleRequireMemberExpression({
|
|
|
842
882
|
node: synthetic,
|
|
843
883
|
importContext,
|
|
844
884
|
workspaceRoot,
|
|
845
|
-
fs
|
|
885
|
+
fs,
|
|
886
|
+
preferImportedPackageSubpath
|
|
846
887
|
});
|
|
847
888
|
if (hasNamespaceImport || specifiersByTarget.size === 0) {
|
|
848
889
|
return;
|
|
@@ -895,7 +936,8 @@ function handleRequireDestructuringDeclarator({
|
|
|
895
936
|
context,
|
|
896
937
|
workspaceRoot,
|
|
897
938
|
fs,
|
|
898
|
-
applyToImportsFrom
|
|
939
|
+
applyToImportsFrom,
|
|
940
|
+
preferImportedPackageSubpath
|
|
899
941
|
}) {
|
|
900
942
|
if (node.id.type !== 'ObjectPattern' || !node.init || node.init.type !== 'CallExpression') {
|
|
901
943
|
return;
|
|
@@ -957,7 +999,8 @@ function handleRequireDestructuringDeclarator({
|
|
|
957
999
|
node: synthetic,
|
|
958
1000
|
importContext,
|
|
959
1001
|
workspaceRoot,
|
|
960
|
-
fs
|
|
1002
|
+
fs,
|
|
1003
|
+
preferImportedPackageSubpath
|
|
961
1004
|
});
|
|
962
1005
|
if (hasNamespaceImport || specifiersByTarget.size === 0 || unmappedSpecifiers.length > 0) {
|
|
963
1006
|
return;
|
|
@@ -1035,7 +1078,8 @@ function handleImportDeclaration({
|
|
|
1035
1078
|
context,
|
|
1036
1079
|
workspaceRoot,
|
|
1037
1080
|
fs,
|
|
1038
|
-
applyToImportsFrom
|
|
1081
|
+
applyToImportsFrom,
|
|
1082
|
+
preferImportedPackageSubpath
|
|
1039
1083
|
}) {
|
|
1040
1084
|
// Resolve import context (validates and extracts package/export info)
|
|
1041
1085
|
// applyToImportsFrom is used here to filter which packages the rule applies to
|
|
@@ -1063,7 +1107,8 @@ function handleImportDeclaration({
|
|
|
1063
1107
|
node,
|
|
1064
1108
|
importContext,
|
|
1065
1109
|
workspaceRoot,
|
|
1066
|
-
fs
|
|
1110
|
+
fs,
|
|
1111
|
+
preferImportedPackageSubpath
|
|
1067
1112
|
});
|
|
1068
1113
|
|
|
1069
1114
|
// If namespace import, report without auto-fix if there are specific exports available
|
|
@@ -1113,9 +1158,10 @@ export function createRule(fs) {
|
|
|
1113
1158
|
return {
|
|
1114
1159
|
meta: ruleMeta,
|
|
1115
1160
|
create(context) {
|
|
1116
|
-
var _options$applyToImpor;
|
|
1161
|
+
var _options$applyToImpor, _options$preferImport;
|
|
1117
1162
|
const options = context.options[0] || {};
|
|
1118
1163
|
const applyToImportsFrom = (_options$applyToImpor = options.applyToImportsFrom) !== null && _options$applyToImpor !== void 0 ? _options$applyToImpor : DEFAULT_TARGET_FOLDERS;
|
|
1164
|
+
const preferImportedPackageSubpath = (_options$preferImport = options.preferImportedPackageSubpath) !== null && _options$preferImport !== void 0 ? _options$preferImport : false;
|
|
1119
1165
|
const workspaceRoot = findWorkspaceRoot({
|
|
1120
1166
|
startPath: dirname(context.filename),
|
|
1121
1167
|
fs,
|
|
@@ -1129,7 +1175,8 @@ export function createRule(fs) {
|
|
|
1129
1175
|
context,
|
|
1130
1176
|
workspaceRoot,
|
|
1131
1177
|
fs,
|
|
1132
|
-
applyToImportsFrom
|
|
1178
|
+
applyToImportsFrom,
|
|
1179
|
+
preferImportedPackageSubpath
|
|
1133
1180
|
});
|
|
1134
1181
|
},
|
|
1135
1182
|
MemberExpression(rawNode) {
|
|
@@ -1138,7 +1185,8 @@ export function createRule(fs) {
|
|
|
1138
1185
|
context,
|
|
1139
1186
|
workspaceRoot,
|
|
1140
1187
|
fs,
|
|
1141
|
-
applyToImportsFrom
|
|
1188
|
+
applyToImportsFrom,
|
|
1189
|
+
preferImportedPackageSubpath
|
|
1142
1190
|
});
|
|
1143
1191
|
},
|
|
1144
1192
|
VariableDeclarator(rawNode) {
|
|
@@ -1147,7 +1195,8 @@ export function createRule(fs) {
|
|
|
1147
1195
|
context,
|
|
1148
1196
|
workspaceRoot,
|
|
1149
1197
|
fs,
|
|
1150
|
-
applyToImportsFrom
|
|
1198
|
+
applyToImportsFrom,
|
|
1199
|
+
preferImportedPackageSubpath
|
|
1151
1200
|
});
|
|
1152
1201
|
}
|
|
1153
1202
|
};
|
|
@@ -4,7 +4,7 @@ import { hasReExportsFromOtherFiles, parseBarrelExports } from '../shared/barrel
|
|
|
4
4
|
import { DEFAULT_TARGET_FOLDERS, findWorkspaceRoot, isRelativeImport, readFileContent, resolveImportPath } from '../shared/file-system';
|
|
5
5
|
import { extractImportPath, findJestRequireActualCalls, findJestRequireMockCalls, isJestMockCall, isJestRequireActual, resolveNewPathForRequireMock } from '../shared/jest-utils';
|
|
6
6
|
import { findPackageInRegistry, isPackageInApplyToImportsFrom } from '../shared/package-registry';
|
|
7
|
-
import { findExportForSourceFile, parsePackageExports } from '../shared/package-resolution';
|
|
7
|
+
import { findCrossPackageBridgeExportPath, findExportForSourceFile, parsePackageExports } from '../shared/package-resolution';
|
|
8
8
|
import { realFileSystem } from '../shared/types';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -525,7 +525,9 @@ function traceSymbolsToExports({
|
|
|
525
525
|
exportMap,
|
|
526
526
|
exportsMap,
|
|
527
527
|
currentExportPath,
|
|
528
|
-
fs
|
|
528
|
+
fs,
|
|
529
|
+
barrelPackageName,
|
|
530
|
+
preferImportedPackageSubpath
|
|
529
531
|
}) {
|
|
530
532
|
const groupedByExport = new Map();
|
|
531
533
|
const crossPackageGroups = new Map();
|
|
@@ -540,16 +542,38 @@ function traceSymbolsToExports({
|
|
|
540
542
|
|
|
541
543
|
// Check for cross-package source first
|
|
542
544
|
if (exportInfo.crossPackageSource) {
|
|
543
|
-
|
|
545
|
+
let key;
|
|
546
|
+
let tracedOriginalName = exportInfo.originalName;
|
|
547
|
+
let barrelBridgeExportPath;
|
|
548
|
+
if (preferImportedPackageSubpath) {
|
|
549
|
+
const bridge = findCrossPackageBridgeExportPath({
|
|
550
|
+
exportsMap,
|
|
551
|
+
crossPackageName: exportInfo.crossPackageSource.packageName,
|
|
552
|
+
exportedName: symbolName,
|
|
553
|
+
fs
|
|
554
|
+
});
|
|
555
|
+
if (bridge) {
|
|
556
|
+
key = `${barrelPackageName}${bridge.exportPath.slice(1)}`;
|
|
557
|
+
barrelBridgeExportPath = bridge.exportPath;
|
|
558
|
+
if (bridge.entryPointExportName !== undefined) {
|
|
559
|
+
tracedOriginalName = bridge.entryPointExportName === symbolName ? undefined : bridge.entryPointExportName;
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
key = `${exportInfo.crossPackageSource.packageName}${exportInfo.crossPackageSource.exportPath === '.' ? '' : exportInfo.crossPackageSource.exportPath.slice(1)}`;
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
key = `${exportInfo.crossPackageSource.packageName}${exportInfo.crossPackageSource.exportPath === '.' ? '' : exportInfo.crossPackageSource.exportPath.slice(1)}`;
|
|
566
|
+
}
|
|
544
567
|
if (!crossPackageGroups.has(key)) {
|
|
545
568
|
crossPackageGroups.set(key, []);
|
|
546
569
|
}
|
|
547
570
|
crossPackageGroups.get(key).push({
|
|
548
571
|
symbolName,
|
|
549
|
-
originalName:
|
|
572
|
+
originalName: tracedOriginalName,
|
|
550
573
|
sourceFilePath: exportInfo.path,
|
|
551
574
|
isTypeOnly: exportInfo.isTypeOnly,
|
|
552
|
-
crossPackageSource: exportInfo.crossPackageSource
|
|
575
|
+
crossPackageSource: exportInfo.crossPackageSource,
|
|
576
|
+
barrelBridgeExportPath
|
|
553
577
|
});
|
|
554
578
|
continue;
|
|
555
579
|
}
|
|
@@ -876,6 +900,10 @@ const ruleMeta = {
|
|
|
876
900
|
type: 'string'
|
|
877
901
|
},
|
|
878
902
|
description: 'The folder paths (relative to workspace root) containing packages whose imports will be checked and autofixed.'
|
|
903
|
+
},
|
|
904
|
+
preferImportedPackageSubpath: {
|
|
905
|
+
type: 'boolean',
|
|
906
|
+
description: 'Prefer subpaths on the mocked barrel package when they bridge to the dependency.'
|
|
879
907
|
}
|
|
880
908
|
},
|
|
881
909
|
additionalProperties: false
|
|
@@ -894,9 +922,10 @@ export function createRule(fs) {
|
|
|
894
922
|
return {
|
|
895
923
|
meta: ruleMeta,
|
|
896
924
|
create(context) {
|
|
897
|
-
var _options$applyToImpor;
|
|
925
|
+
var _options$applyToImpor, _options$preferImport;
|
|
898
926
|
const options = context.options[0] || {};
|
|
899
927
|
const applyToImportsFrom = (_options$applyToImpor = options.applyToImportsFrom) !== null && _options$applyToImpor !== void 0 ? _options$applyToImpor : DEFAULT_TARGET_FOLDERS;
|
|
928
|
+
const preferImportedPackageSubpath = (_options$preferImport = options.preferImportedPackageSubpath) !== null && _options$preferImport !== void 0 ? _options$preferImport : false;
|
|
900
929
|
const workspaceRoot = findWorkspaceRoot({
|
|
901
930
|
startPath: dirname(context.filename),
|
|
902
931
|
fs,
|
|
@@ -985,7 +1014,9 @@ export function createRule(fs) {
|
|
|
985
1014
|
exportMap: raContext.exportMap,
|
|
986
1015
|
exportsMap: raContext.exportsMap,
|
|
987
1016
|
currentExportPath: raContext.currentExportPath,
|
|
988
|
-
fs
|
|
1017
|
+
fs,
|
|
1018
|
+
barrelPackageName: raContext.packageName,
|
|
1019
|
+
preferImportedPackageSubpath
|
|
989
1020
|
});
|
|
990
1021
|
let newPath = null;
|
|
991
1022
|
if (groupedByExport.size === 1 && crossPackageGroups.size === 0) {
|
|
@@ -1079,7 +1110,9 @@ export function createRule(fs) {
|
|
|
1079
1110
|
exportMap: mockContext.exportMap,
|
|
1080
1111
|
exportsMap: mockContext.exportsMap,
|
|
1081
1112
|
currentExportPath: mockContext.currentExportPath,
|
|
1082
|
-
fs
|
|
1113
|
+
fs,
|
|
1114
|
+
barrelPackageName: mockContext.packageName,
|
|
1115
|
+
preferImportedPackageSubpath
|
|
1083
1116
|
});
|
|
1084
1117
|
|
|
1085
1118
|
// If no symbols can be mapped to specific exports or cross-package sources,
|
|
@@ -1125,8 +1158,9 @@ export function createRule(fs) {
|
|
|
1125
1158
|
|
|
1126
1159
|
// Get cross-package source info from the first symbol (all symbols in same group have same source)
|
|
1127
1160
|
const crossPackageSource = symbols[0].crossPackageSource;
|
|
1161
|
+
const bridgeExportPath = symbols[0].barrelBridgeExportPath;
|
|
1128
1162
|
crossPackageMockGroups.push({
|
|
1129
|
-
exportPath: crossPackageSource.exportPath,
|
|
1163
|
+
exportPath: bridgeExportPath !== null && bridgeExportPath !== void 0 ? bridgeExportPath : crossPackageSource.exportPath,
|
|
1130
1164
|
importPath,
|
|
1131
1165
|
propertyNames: symbols.map(s => s.symbolName),
|
|
1132
1166
|
propertyTexts: new Map(symbols.map(s => [s.symbolName, mockProperties.get(s.symbolName).text])),
|
|
@@ -135,10 +135,52 @@ export function parsePackageExports({
|
|
|
135
135
|
});
|
|
136
136
|
return exportsMap;
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Check whether a subpath export key (e.g. `"./checkbox-select"`) is kebab-case.
|
|
140
|
+
*
|
|
141
|
+
* A key is considered kebab-case when the portion after the leading `"./"` prefix
|
|
142
|
+
* consists only of lowercase letters, digits, hyphens, dots, and forward-slash
|
|
143
|
+
* separators — i.e. no uppercase letters, underscores, or camelCase humps.
|
|
144
|
+
*/
|
|
145
|
+
export function isKebabCaseExportKey(key) {
|
|
146
|
+
const body = key.replace(/^\.\//, '');
|
|
147
|
+
if (body.length === 0) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return /^[a-z0-9][a-z0-9\-./]*$/.test(body);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Given a list of candidate {@link ExportMatchResult}s that all resolve to the same
|
|
155
|
+
* source file, pick the best one. When any candidate's export path is kebab-case
|
|
156
|
+
* and points to an entry-point file, prefer it over non-kebab-case alternatives.
|
|
157
|
+
* Falls back to the first candidate if no kebab-case entry-point candidate is found.
|
|
158
|
+
*/
|
|
159
|
+
function pickBestMatch(candidates, exportsMap) {
|
|
160
|
+
if (candidates.length === 1) {
|
|
161
|
+
return candidates[0];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Among candidates whose value is an entry-point file, prefer kebab-case keys.
|
|
165
|
+
const entryPointKebab = candidates.filter(c => {
|
|
166
|
+
const resolved = exportsMap.get(c.exportPath);
|
|
167
|
+
return resolved && isInEntryPointsFolder(resolved) && isKebabCaseExportKey(c.exportPath);
|
|
168
|
+
});
|
|
169
|
+
if (entryPointKebab.length > 0) {
|
|
170
|
+
return entryPointKebab[0];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Fall back to the first candidate (preserves previous behaviour).
|
|
174
|
+
return candidates[0];
|
|
175
|
+
}
|
|
176
|
+
|
|
138
177
|
/**
|
|
139
178
|
* Find a matching export entry for a given source file path.
|
|
140
179
|
* Returns the export path (e.g., "./controllers/analytics") or null if not found.
|
|
141
180
|
*
|
|
181
|
+
* When multiple export paths resolve to the same source file **and** point to an
|
|
182
|
+
* entry-point file, kebab-case keys are preferred over other casing styles.
|
|
183
|
+
*
|
|
142
184
|
* When `fs` is provided, also checks entry-point wrapper files. If an export resolves
|
|
143
185
|
* to a file inside a recognized entry-points folder (entry-points, entrypoints, etc.),
|
|
144
186
|
* the wrapper is parsed to see if it re-exports from `sourceFilePath`.
|
|
@@ -153,14 +195,22 @@ export function findExportForSourceFile({
|
|
|
153
195
|
fs,
|
|
154
196
|
sourceExportName
|
|
155
197
|
}) {
|
|
198
|
+
// --- Phase 1: direct matches (export value === sourceFilePath) ---
|
|
199
|
+
const directMatches = [];
|
|
156
200
|
for (const [exportPath, resolvedPath] of exportsMap) {
|
|
157
201
|
if (resolvedPath === sourceFilePath) {
|
|
158
|
-
|
|
202
|
+
directMatches.push({
|
|
159
203
|
exportPath
|
|
160
|
-
};
|
|
204
|
+
});
|
|
161
205
|
}
|
|
162
206
|
}
|
|
207
|
+
if (directMatches.length > 0) {
|
|
208
|
+
return pickBestMatch(directMatches, exportsMap);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Phase 2: entry-point wrapper re-export matches ---
|
|
163
212
|
if (fs) {
|
|
213
|
+
const entryPointMatches = [];
|
|
164
214
|
for (const [exportPath, resolvedPath] of exportsMap) {
|
|
165
215
|
if (isInEntryPointsFolder(resolvedPath)) {
|
|
166
216
|
const reExports = resolveEntryPointReExports({
|
|
@@ -173,13 +223,78 @@ export function findExportForSourceFile({
|
|
|
173
223
|
if (sourceExportName !== undefined && reExport.nameMap.has(sourceExportName)) {
|
|
174
224
|
entryPointExportName = reExport.nameMap.get(sourceExportName);
|
|
175
225
|
}
|
|
176
|
-
|
|
226
|
+
entryPointMatches.push({
|
|
177
227
|
exportPath,
|
|
178
228
|
entryPointExportName
|
|
179
|
-
};
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (entryPointMatches.length > 0) {
|
|
235
|
+
return pickBestMatch(entryPointMatches, exportsMap);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* When a symbol reaches the consumer through a barrel package that re-exports from
|
|
243
|
+
* `crossPackageName`, find a `package.json` export subpath of that barrel whose entry
|
|
244
|
+
* file directly re-exports the symbol from `crossPackageName` (named exports only).
|
|
245
|
+
*
|
|
246
|
+
* This enables rewriting imports to `@scope/barrel/subpath` instead of
|
|
247
|
+
* `@scope/cross-package/...` when the barrel exposes such a subpath (e.g. `@atlaskit/select/react-select`).
|
|
248
|
+
*/
|
|
249
|
+
export function findCrossPackageBridgeExportPath({
|
|
250
|
+
exportsMap,
|
|
251
|
+
crossPackageName,
|
|
252
|
+
exportedName,
|
|
253
|
+
fs
|
|
254
|
+
}) {
|
|
255
|
+
for (const [exportPath, resolvedPath] of exportsMap) {
|
|
256
|
+
const content = readFileContent({
|
|
257
|
+
filePath: resolvedPath,
|
|
258
|
+
fs
|
|
259
|
+
});
|
|
260
|
+
if (!content) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const sourceFile = ts.createSourceFile(resolvedPath, content, ts.ScriptTarget.Latest, true);
|
|
265
|
+
for (const statement of sourceFile.statements) {
|
|
266
|
+
if (!ts.isExportDeclaration(statement) || statement.isTypeOnly) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (!statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (statement.moduleSpecifier.text !== crossPackageName) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!statement.exportClause || ts.isNamespaceExport(statement.exportClause)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!ts.isNamedExports(statement.exportClause)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
for (const element of statement.exportClause.elements) {
|
|
282
|
+
if (element.isTypeOnly) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const publicName = element.name.text;
|
|
286
|
+
if (publicName !== exportedName) {
|
|
287
|
+
continue;
|
|
180
288
|
}
|
|
289
|
+
const entryPointExportName = element.propertyName ? element.propertyName.text : undefined;
|
|
290
|
+
return {
|
|
291
|
+
exportPath,
|
|
292
|
+
entryPointExportName
|
|
293
|
+
};
|
|
181
294
|
}
|
|
182
295
|
}
|
|
296
|
+
} catch {
|
|
297
|
+
// Ignore parse errors for individual export entry files
|
|
183
298
|
}
|
|
184
299
|
}
|
|
185
300
|
return null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
+
|
|
3
|
+
const RESTRICTED_IMPORTS = {
|
|
4
|
+
'@atlassian/atl-context': ['isFedRamp', 'isIsolatedCloud'],
|
|
5
|
+
'@atlaskit/atlassian-context': ['isFedRamp', 'isIsolatedCloud'],
|
|
6
|
+
'@atlassian/teams-common': ['isFedramp']
|
|
7
|
+
};
|
|
8
|
+
const rule = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Disallow importing deprecated FedRamp/IsolatedCloud context functions. Use isFeatureEnabled from AEM (Atlassian Environment Manager) instead.',
|
|
13
|
+
recommended: true
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
noRestrictedFedrampImports: '{{name}} from {{source}} will be deprecated soon. Please use isFeatureEnabled from AEM (Atlassian Environment Manager) instead. See go/AEM for more details.'
|
|
17
|
+
},
|
|
18
|
+
schema: []
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
ImportDeclaration(node) {
|
|
23
|
+
const source = node.source.value;
|
|
24
|
+
if (typeof source !== 'string') {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const restrictedNames = RESTRICTED_IMPORTS[source];
|
|
28
|
+
if (!restrictedNames) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const specifier of node.specifiers) {
|
|
32
|
+
if (specifier.type === 'ImportSpecifier' && restrictedNames.includes(specifier.imported.name)) {
|
|
33
|
+
context.report({
|
|
34
|
+
node: specifier,
|
|
35
|
+
messageId: 'noRestrictedFedrampImports',
|
|
36
|
+
data: {
|
|
37
|
+
name: specifier.imported.name,
|
|
38
|
+
source
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
export default rule;
|