@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cjs/index.js +8 -2
  3. package/dist/cjs/rules/compiled/no-css-prop-in-object-spread/index.js +162 -0
  4. package/dist/cjs/rules/ensure-critical-dependency-resolutions/index.js +0 -1
  5. package/dist/cjs/rules/import/no-barrel-entry-imports/index.js +68 -16
  6. package/dist/cjs/rules/import/no-barrel-entry-jest-mock/index.js +42 -8
  7. package/dist/cjs/rules/import/shared/package-resolution.js +153 -8
  8. package/dist/cjs/rules/no-restricted-fedramp-imports/index.js +65 -0
  9. package/dist/cjs/rules/no-xcss-in-cx/index.js +221 -0
  10. package/dist/cjs/rules/visit-example-type-import-required/index.js +24 -14
  11. package/dist/es2019/index.js +8 -2
  12. package/dist/es2019/rules/compiled/no-css-prop-in-object-spread/index.js +136 -0
  13. package/dist/es2019/rules/ensure-critical-dependency-resolutions/index.js +0 -1
  14. package/dist/es2019/rules/import/no-barrel-entry-imports/index.js +66 -17
  15. package/dist/es2019/rules/import/no-barrel-entry-jest-mock/index.js +43 -9
  16. package/dist/es2019/rules/import/shared/package-resolution.js +119 -4
  17. package/dist/es2019/rules/no-restricted-fedramp-imports/index.js +47 -0
  18. package/dist/es2019/rules/no-xcss-in-cx/index.js +187 -0
  19. package/dist/es2019/rules/visit-example-type-import-required/index.js +24 -15
  20. package/dist/esm/index.js +8 -2
  21. package/dist/esm/rules/compiled/no-css-prop-in-object-spread/index.js +156 -0
  22. package/dist/esm/rules/ensure-critical-dependency-resolutions/index.js +0 -1
  23. package/dist/esm/rules/import/no-barrel-entry-imports/index.js +69 -17
  24. package/dist/esm/rules/import/no-barrel-entry-jest-mock/index.js +43 -9
  25. package/dist/esm/rules/import/shared/package-resolution.js +151 -8
  26. package/dist/esm/rules/no-restricted-fedramp-imports/index.js +59 -0
  27. package/dist/esm/rules/no-xcss-in-cx/index.js +216 -0
  28. package/dist/esm/rules/visit-example-type-import-required/index.js +24 -14
  29. package/dist/types/index.d.ts +278 -241
  30. package/dist/types/rules/compiled/no-css-prop-in-object-spread/index.d.ts +3 -0
  31. package/dist/types/rules/import/shared/package-resolution.d.ts +25 -0
  32. package/dist/types/rules/no-restricted-fedramp-imports/index.d.ts +3 -0
  33. package/dist/types/rules/no-xcss-in-cx/index.d.ts +31 -0
  34. package/dist/types-ts4.5/index.d.ts +222 -209
  35. package/dist/types-ts4.5/rules/compiled/no-css-prop-in-object-spread/index.d.ts +3 -0
  36. package/dist/types-ts4.5/rules/import/shared/package-resolution.d.ts +25 -0
  37. package/dist/types-ts4.5/rules/no-restricted-fedramp-imports/index.d.ts +3 -0
  38. package/dist/types-ts4.5/rules/no-xcss-in-cx/index.d.ts +31 -0
  39. 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
- const targetKey = targetExportPath ? sourcePackageName + targetExportPath.slice(1) // Remove leading '.' from subpath
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 isTypeImport = node.importKind === 'type';
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 isTypeImport = node.importKind === 'type';
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
- const key = `${exportInfo.crossPackageSource.packageName}${exportInfo.crossPackageSource.exportPath === '.' ? '' : exportInfo.crossPackageSource.exportPath.slice(1)}`;
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: exportInfo.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
- return {
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
- return {
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;