@atlaskit/eslint-plugin-platform 2.8.0 → 2.9.1

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 (47) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/index.js +8 -1
  3. package/dist/cjs/rules/ensure-critical-dependency-resolutions/index.js +0 -1
  4. package/dist/cjs/rules/ensure-use-sync-external-store-server-snapshot/index.js +41 -0
  5. package/dist/cjs/rules/import/no-barrel-entry-imports/index.js +534 -74
  6. package/dist/cjs/rules/import/no-barrel-entry-jest-mock/index.js +428 -119
  7. package/dist/cjs/rules/import/no-jest-mock-barrel-files/index.js +3 -2
  8. package/dist/cjs/rules/import/no-relative-barrel-file-imports/index.js +7 -3
  9. package/dist/cjs/rules/import/shared/jest-utils.js +62 -9
  10. package/dist/cjs/rules/import/shared/package-resolution.js +300 -22
  11. package/dist/cjs/rules/no-restricted-fedramp-imports/index.js +65 -0
  12. package/dist/cjs/rules/visit-example-type-import-required/index.js +409 -0
  13. package/dist/es2019/index.js +8 -1
  14. package/dist/es2019/rules/ensure-critical-dependency-resolutions/index.js +0 -1
  15. package/dist/es2019/rules/ensure-use-sync-external-store-server-snapshot/index.js +43 -0
  16. package/dist/es2019/rules/import/no-barrel-entry-imports/index.js +431 -25
  17. package/dist/es2019/rules/import/no-barrel-entry-jest-mock/index.js +287 -25
  18. package/dist/es2019/rules/import/no-jest-mock-barrel-files/index.js +3 -2
  19. package/dist/es2019/rules/import/no-relative-barrel-file-imports/index.js +7 -3
  20. package/dist/es2019/rules/import/shared/jest-utils.js +44 -0
  21. package/dist/es2019/rules/import/shared/package-resolution.js +211 -4
  22. package/dist/es2019/rules/no-restricted-fedramp-imports/index.js +47 -0
  23. package/dist/es2019/rules/visit-example-type-import-required/index.js +375 -0
  24. package/dist/esm/index.js +8 -1
  25. package/dist/esm/rules/ensure-critical-dependency-resolutions/index.js +0 -1
  26. package/dist/esm/rules/ensure-use-sync-external-store-server-snapshot/index.js +35 -0
  27. package/dist/esm/rules/import/no-barrel-entry-imports/index.js +535 -75
  28. package/dist/esm/rules/import/no-barrel-entry-jest-mock/index.js +430 -121
  29. package/dist/esm/rules/import/no-jest-mock-barrel-files/index.js +3 -2
  30. package/dist/esm/rules/import/no-relative-barrel-file-imports/index.js +7 -3
  31. package/dist/esm/rules/import/shared/jest-utils.js +61 -9
  32. package/dist/esm/rules/import/shared/package-resolution.js +298 -24
  33. package/dist/esm/rules/no-restricted-fedramp-imports/index.js +59 -0
  34. package/dist/esm/rules/visit-example-type-import-required/index.js +402 -0
  35. package/dist/types/index.d.ts +14 -0
  36. package/dist/types/rules/ensure-use-sync-external-store-server-snapshot/index.d.ts +3 -0
  37. package/dist/types/rules/import/shared/jest-utils.d.ts +8 -0
  38. package/dist/types/rules/import/shared/package-resolution.d.ts +47 -2
  39. package/dist/types/rules/no-restricted-fedramp-imports/index.d.ts +3 -0
  40. package/dist/types/rules/visit-example-type-import-required/index.d.ts +4 -0
  41. package/dist/types-ts4.5/index.d.ts +14 -0
  42. package/dist/types-ts4.5/rules/ensure-use-sync-external-store-server-snapshot/index.d.ts +3 -0
  43. package/dist/types-ts4.5/rules/import/shared/jest-utils.d.ts +8 -0
  44. package/dist/types-ts4.5/rules/import/shared/package-resolution.d.ts +47 -2
  45. package/dist/types-ts4.5/rules/no-restricted-fedramp-imports/index.d.ts +3 -0
  46. package/dist/types-ts4.5/rules/visit-example-type-import-required/index.d.ts +4 -0
  47. package/package.json +3 -1
@@ -1,6 +1,65 @@
1
- import { join } from 'path';
2
- import { readFileContent, resolveImportPath } from './file-system';
1
+ import { dirname, join } from 'path';
2
+ import * as ts from 'typescript';
3
+ import { isRelativeImport, readFileContent, resolveImportPath } from './file-system';
3
4
  import { findPackageInRegistry } from './package-registry';
5
+ const ENTRY_POINT_FOLDER_NAMES = new Set(['entry-points', 'entrypoints', 'entrypoint', 'entry-point']);
6
+ function isInEntryPointsFolder(filePath) {
7
+ const parts = filePath.split(/[/\\]/);
8
+ return parts.some(part => ENTRY_POINT_FOLDER_NAMES.has(part));
9
+ }
10
+ /**
11
+ * Parse an entry-point wrapper file and resolve the source files it re-exports from,
12
+ * along with name mappings (source export name → entry-point export name).
13
+ */
14
+ function resolveEntryPointReExports({
15
+ entryPointFilePath,
16
+ fs
17
+ }) {
18
+ const content = readFileContent({
19
+ filePath: entryPointFilePath,
20
+ fs
21
+ });
22
+ if (!content) {
23
+ return [];
24
+ }
25
+ try {
26
+ const sourceFile = ts.createSourceFile(entryPointFilePath, content, ts.ScriptTarget.Latest, true);
27
+ const basedir = dirname(entryPointFilePath);
28
+ const results = [];
29
+ for (const statement of sourceFile.statements) {
30
+ if (ts.isExportDeclaration(statement) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
31
+ const modulePath = statement.moduleSpecifier.text;
32
+ if (!isRelativeImport(modulePath)) {
33
+ continue;
34
+ }
35
+ const resolved = resolveImportPath({
36
+ basedir,
37
+ importPath: modulePath,
38
+ fs
39
+ });
40
+ if (!resolved) {
41
+ continue;
42
+ }
43
+ const nameMap = new Map();
44
+ if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
45
+ for (const element of statement.exportClause.elements) {
46
+ const exportedName = element.name.text;
47
+ const sourceName = element.propertyName ? element.propertyName.text : exportedName;
48
+ nameMap.set(sourceName, exportedName);
49
+ }
50
+ }
51
+ results.push({
52
+ sourcePath: resolved,
53
+ nameMap
54
+ });
55
+ }
56
+ }
57
+ return results;
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+
4
63
  /**
5
64
  * Parse the package.json exports field and return a map of export paths to resolved file paths.
6
65
  */
@@ -76,18 +135,166 @@ export function parsePackageExports({
76
135
  });
77
136
  return exportsMap;
78
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
+ }
79
176
 
80
177
  /**
81
178
  * Find a matching export entry for a given source file path.
82
179
  * Returns the export path (e.g., "./controllers/analytics") or null if not found.
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
+ *
184
+ * When `fs` is provided, also checks entry-point wrapper files. If an export resolves
185
+ * to a file inside a recognized entry-points folder (entry-points, entrypoints, etc.),
186
+ * the wrapper is parsed to see if it re-exports from `sourceFilePath`.
187
+ *
188
+ * `sourceExportName` is the name under which the symbol is exported from the source file
189
+ * (e.g. `'default'`). Used to look up the corresponding entry-point export name so the
190
+ * caller can generate the correct import style.
83
191
  */
84
192
  export function findExportForSourceFile({
85
193
  sourceFilePath,
86
- exportsMap
194
+ exportsMap,
195
+ fs,
196
+ sourceExportName
87
197
  }) {
198
+ // --- Phase 1: direct matches (export value === sourceFilePath) ---
199
+ const directMatches = [];
88
200
  for (const [exportPath, resolvedPath] of exportsMap) {
89
201
  if (resolvedPath === sourceFilePath) {
90
- return exportPath;
202
+ directMatches.push({
203
+ exportPath
204
+ });
205
+ }
206
+ }
207
+ if (directMatches.length > 0) {
208
+ return pickBestMatch(directMatches, exportsMap);
209
+ }
210
+
211
+ // --- Phase 2: entry-point wrapper re-export matches ---
212
+ if (fs) {
213
+ const entryPointMatches = [];
214
+ for (const [exportPath, resolvedPath] of exportsMap) {
215
+ if (isInEntryPointsFolder(resolvedPath)) {
216
+ const reExports = resolveEntryPointReExports({
217
+ entryPointFilePath: resolvedPath,
218
+ fs
219
+ });
220
+ for (const reExport of reExports) {
221
+ if (reExport.sourcePath === sourceFilePath) {
222
+ let entryPointExportName;
223
+ if (sourceExportName !== undefined && reExport.nameMap.has(sourceExportName)) {
224
+ entryPointExportName = reExport.nameMap.get(sourceExportName);
225
+ }
226
+ entryPointMatches.push({
227
+ exportPath,
228
+ entryPointExportName
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;
288
+ }
289
+ const entryPointExportName = element.propertyName ? element.propertyName.text : undefined;
290
+ return {
291
+ exportPath,
292
+ entryPointExportName
293
+ };
294
+ }
295
+ }
296
+ } catch {
297
+ // Ignore parse errors for individual export entry files
91
298
  }
92
299
  }
93
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;
@@ -0,0 +1,375 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
4
+ import { simpleTraverse } from '@typescript-eslint/typescript-estree';
5
+ export const RULE_NAME = 'visit-example-type-import-required';
6
+ const messages = {
7
+ missingTypeofImport: 'visitExample must use typeof import(...) generic type parameter. ' + 'Use visitExample<typeof import("path/to/example.tsx")>(groupId, packageId, exampleId).',
8
+ invalidTypeParameter: 'visitExample generic type parameter must be a typeof import(...) expression.',
9
+ pathMismatch: 'The import path "{{ importPath }}" does not match the expected example file for ' + 'visitExample({{ groupId }}, {{ packageId }}, {{ exampleId }}). ' + 'Expected import to resolve to: {{ expectedPath }}',
10
+ noPackageImports: 'Package imports (e.g., @atlaskit/...) are not allowed in visitExample type parameters. Use a relative import path.',
11
+ typeAliasNotInlined: 'Type aliases for typeof import(...) must be inlined directly into the visitExample call. ' + 'Use visitExample<typeof import("...")>(...) instead of defining a type alias.',
12
+ suggestFixPath: 'Update import path to match visitExample arguments'
13
+ };
14
+ function isTargetFile(filename) {
15
+ return filename.endsWith('.spec.tsx') || filename.endsWith('.spec.ts');
16
+ }
17
+
18
+ /**
19
+ * Extracts the import path string from a TSTypeQuery node of the form `typeof import('...')`.
20
+ * Returns null if the node doesn't match that shape.
21
+ */
22
+ function extractImportPathFromTypeQuery(typeQuery) {
23
+ // TSTypeQuery { exprName: TSImportType { argument: TSLiteralType { literal: Literal } } }
24
+ const {
25
+ exprName
26
+ } = typeQuery;
27
+ if (exprName.type !== AST_NODE_TYPES.TSImportType) {
28
+ return null;
29
+ }
30
+ const {
31
+ argument
32
+ } = exprName;
33
+ if (argument.type === AST_NODE_TYPES.TSLiteralType && argument.literal.type === AST_NODE_TYPES.Literal && typeof argument.literal.value === 'string') {
34
+ return argument.literal.value;
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Builds a map of all TSTypeAliasDeclaration nodes in the file, keyed by name.
40
+ * Each entry records whether the alias is at the top level of the program (file-level).
41
+ */
42
+ function collectTypeAliases(ast) {
43
+ const result = new Map();
44
+
45
+ // Cast through unknown to work around a version mismatch: @typescript-eslint/utils
46
+ // vendors its own copy of @typescript-eslint/types (v7) while the root node_modules
47
+ // has a different version (v5). The TSESTree types are structurally identical at
48
+ // runtime — the cast is safe.
49
+ simpleTraverse(ast, {
50
+ enter(node, parent) {
51
+ // A type alias is "file-level" if its immediate parent is the Program,
52
+ // or if it's the declaration of a top-level ExportNamedDeclaration.
53
+ if (node.type === AST_NODE_TYPES.TSTypeAliasDeclaration) {
54
+ const isFileLevel = (parent === null || parent === void 0 ? void 0 : parent.type) === AST_NODE_TYPES.Program || (parent === null || parent === void 0 ? void 0 : parent.type) === AST_NODE_TYPES.ExportNamedDeclaration;
55
+ result.set(node.id.name, {
56
+ node: node,
57
+ isFileLevel
58
+ });
59
+ }
60
+ }
61
+ });
62
+ return result;
63
+ }
64
+
65
+ /**
66
+ * Resolves a top-level `const foo = 'literal'` declaration to its string value.
67
+ * Returns null for non-const, non-string, or not-found variables.
68
+ */
69
+ function resolveVariableToConstant(programBody, variableName, cache) {
70
+ if (cache.has(variableName)) {
71
+ var _cache$get;
72
+ return (_cache$get = cache.get(variableName)) !== null && _cache$get !== void 0 ? _cache$get : null;
73
+ }
74
+ for (const node of programBody) {
75
+ if (node.type !== AST_NODE_TYPES.VariableDeclaration || node.kind !== 'const') {
76
+ continue;
77
+ }
78
+ for (const declarator of node.declarations) {
79
+ var _declarator$init;
80
+ if (declarator.id.type === AST_NODE_TYPES.Identifier && declarator.id.name === variableName && ((_declarator$init = declarator.init) === null || _declarator$init === void 0 ? void 0 : _declarator$init.type) === AST_NODE_TYPES.Literal && typeof declarator.init.value === 'string') {
81
+ cache.set(variableName, declarator.init.value);
82
+ return declarator.init.value;
83
+ }
84
+ }
85
+ }
86
+ cache.set(variableName, null);
87
+ return null;
88
+ }
89
+ /**
90
+ * Extracts the (groupId, packageId, exampleId) string arguments from a visitExample call.
91
+ * Each argument may be a string literal or a reference to a top-level const string variable.
92
+ * Returns null for any argument that can't be statically resolved.
93
+ */
94
+ function extractCallArgs(node, programBody, variableCache) {
95
+ if (node.arguments.length < 3) {
96
+ return null;
97
+ }
98
+ function resolveArg(arg) {
99
+ if (arg.type === AST_NODE_TYPES.Literal && typeof arg.value === 'string') {
100
+ return arg.value;
101
+ }
102
+ if (arg.type === AST_NODE_TYPES.Identifier) {
103
+ return resolveVariableToConstant(programBody, arg.name, variableCache);
104
+ }
105
+ return null;
106
+ }
107
+ const groupId = resolveArg(node.arguments[0]);
108
+ const packageId = resolveArg(node.arguments[1]);
109
+ const exampleId = resolveArg(node.arguments[2]);
110
+ if (!groupId || !packageId || !exampleId) {
111
+ return null;
112
+ }
113
+ return {
114
+ groupId,
115
+ packageId,
116
+ exampleId
117
+ };
118
+ }
119
+ function getPackagesBasePath(testFilePath) {
120
+ const testFileDir = path.dirname(testFilePath);
121
+ const testFileSegments = testFileDir.split(path.sep);
122
+ const packagesIndex = testFileSegments.findIndex(seg => seg === 'packages');
123
+ if (packagesIndex === -1) {
124
+ return null;
125
+ }
126
+ const baseSegments = testFileSegments.slice(0, packagesIndex + 1);
127
+ const basePath = path.isAbsolute(testFilePath) ? path.resolve('/', ...baseSegments) : path.resolve(process.cwd(), ...baseSegments);
128
+ return {
129
+ basePath,
130
+ packagesIndex
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Resolves the expected example file path from visitExample arguments.
136
+ *
137
+ * visitExample('groupId', 'packageId', 'exampleId') maps to:
138
+ * packages/{groupId}/{packageId}/examples/{exampleId}.tsx
139
+ *
140
+ * Example files may also have a numeric sort prefix, e.g.:
141
+ * packages/{groupId}/{packageId}/examples/00-{exampleId}.tsx
142
+ *
143
+ * We scan the examples directory once and match against all candidates.
144
+ * Falls back to the bare `{exampleId}.tsx` name when the directory can't
145
+ * be read (e.g. in unit-test environments where the files don't exist).
146
+ */
147
+ function resolveExamplePathFromArgs(groupId, packageId, exampleId, testFilePath) {
148
+ const packagesBase = getPackagesBasePath(testFilePath);
149
+ if (!packagesBase) {
150
+ return null;
151
+ }
152
+ const examplesDir = path.resolve(packagesBase.basePath, groupId, packageId, 'examples');
153
+ const fallback = path.resolve(examplesDir, `${exampleId}.tsx`);
154
+
155
+ // Match: exact name OR numeric-prefixed variant, with optional `.examples` infix
156
+ const candidateRe = new RegExp(`^(?:\\d+-)?${exampleId}(?:\\.examples?)?\\.tsx$`);
157
+ try {
158
+ const match = fs.readdirSync(examplesDir).find(f => candidateRe.test(f));
159
+ if (match) {
160
+ return path.resolve(examplesDir, match);
161
+ }
162
+ } catch {
163
+ // Directory doesn't exist or can't be read (e.g. in test environments)
164
+ }
165
+ return fallback;
166
+ }
167
+
168
+ /**
169
+ * Computes a relative import path from one file to another
170
+ */
171
+ function computeRelativeImportPath(fromFile, toFile) {
172
+ const fromDir = path.dirname(fromFile);
173
+ let relativePath = path.relative(fromDir, toFile);
174
+ // Normalize to forward slashes for import statements (standard in JavaScript/TypeScript)
175
+ relativePath = relativePath.replace(/\\/g, '/');
176
+ // Ensure relative imports start with ./ or ../
177
+ if (!relativePath.startsWith('.') && !relativePath.startsWith('/')) {
178
+ relativePath = `./${relativePath}`;
179
+ }
180
+ return relativePath;
181
+ }
182
+ /**
183
+ * Extracts the generic type argument from a `visitExample<...>(...)` call expression
184
+ * using the AST directly (no regex on source text).
185
+ *
186
+ * Returns:
187
+ * { type: 'inline', importPath } — for `visitExample<typeof import('...')>(...)`
188
+ * { type: 'alias', name } — for `visitExample<SomeTypeAlias>(...)`
189
+ * null — if no generic type parameter is present
190
+ */
191
+ function extractGenericType(node) {
192
+ var _params, _ref, _node$typeArguments;
193
+ // `typeArguments` is the current property name; `typeParameters` is the deprecated alias.
194
+ // We fall back to `typeParameters` for compatibility with older parser versions.
195
+ const params = (_params = (_ref = (_node$typeArguments = node.typeArguments) !== null && _node$typeArguments !== void 0 ? _node$typeArguments : node.typeParameters) === null || _ref === void 0 ? void 0 : _ref.params) !== null && _params !== void 0 ? _params : [];
196
+ if (params.length === 0) {
197
+ return null;
198
+ }
199
+ const [typeParam] = params;
200
+
201
+ // `typeof import('...')` → TSTypeQuery { exprName: TSImportType { ... } }
202
+ if (typeParam.type === AST_NODE_TYPES.TSTypeQuery) {
203
+ const importPath = extractImportPathFromTypeQuery(typeParam);
204
+ if (importPath !== null) {
205
+ return {
206
+ type: 'inline',
207
+ importPath
208
+ };
209
+ }
210
+ }
211
+
212
+ // `SomeTypeAlias` → TSTypeReference { typeName: Identifier { name } }
213
+ if (typeParam.type === AST_NODE_TYPES.TSTypeReference && typeParam.typeName.type === AST_NODE_TYPES.Identifier) {
214
+ return {
215
+ type: 'alias',
216
+ name: typeParam.typeName.name
217
+ };
218
+ }
219
+ return null;
220
+ }
221
+ const rule = {
222
+ meta: {
223
+ type: 'problem',
224
+ docs: {
225
+ description: 'Ensures that visitExample uses a typeof import(...) generic and that the import path matches the example file resolved from the call arguments.'
226
+ },
227
+ fixable: 'code',
228
+ messages,
229
+ schema: []
230
+ },
231
+ create(context) {
232
+ const filename = context.filename;
233
+ const ast = context.sourceCode.ast;
234
+ const programBody = ast.body;
235
+
236
+ // Build the type alias map once per file (lazily on first visitExample call)
237
+ let typeAliases = null;
238
+ function getTypeAliases() {
239
+ if (!typeAliases) {
240
+ typeAliases = collectTypeAliases(ast);
241
+ }
242
+ return typeAliases;
243
+ }
244
+ const variableCache = new Map();
245
+ return {
246
+ CallExpression(estreeNode) {
247
+ if (!isTargetFile(filename)) {
248
+ return;
249
+ }
250
+ const node = estreeNode;
251
+ // Only handle `<anything>.visitExample(...)` calls
252
+ if (node.callee.type !== AST_NODE_TYPES.MemberExpression || node.callee.property.type !== AST_NODE_TYPES.Identifier || node.callee.property.name !== 'visitExample') {
253
+ return;
254
+ }
255
+
256
+ // Narrow callee — we've confirmed property is an Identifier above
257
+ const callee = node.callee;
258
+ // reportCallee is typed as estree.Node for context.report compatibility
259
+ const reportCallee = estreeNode.callee;
260
+ const genericType = extractGenericType(node);
261
+
262
+ // ── Case 1: No generic type parameter ────────────────────────────────
263
+ if (genericType === null) {
264
+ const args = extractCallArgs(node, programBody, variableCache);
265
+ context.report({
266
+ node: reportCallee,
267
+ messageId: 'missingTypeofImport',
268
+ fix(fixer) {
269
+ if (!args) {
270
+ return null;
271
+ }
272
+ const examplePath = resolveExamplePathFromArgs(args.groupId, args.packageId, args.exampleId, filename);
273
+ if (!examplePath) {
274
+ return null;
275
+ }
276
+ const importPath = computeRelativeImportPath(filename, examplePath);
277
+ const [start, end] = callee.property.range;
278
+ return fixer.insertTextAfterRange([start, end], `<typeof import('${importPath}')>`);
279
+ }
280
+ });
281
+ return;
282
+ }
283
+
284
+ // ── Case 2: Generic is a type alias reference (`visitExample<Foo>`) ──
285
+ let importPath;
286
+ if (genericType.type === 'alias') {
287
+ const found = getTypeAliases().get(genericType.name);
288
+ if (!found) {
289
+ // Unknown type alias — not a typeof import
290
+ context.report({
291
+ node: reportCallee,
292
+ messageId: 'missingTypeofImport'
293
+ });
294
+ return;
295
+ }
296
+ if (found.isFileLevel) {
297
+ // Top-level `type Foo = typeof import(...)` is disallowed
298
+ context.report({
299
+ node: reportCallee,
300
+ messageId: 'typeAliasNotInlined'
301
+ });
302
+ return;
303
+ }
304
+ const typeAnnotation = found.node.typeAnnotation;
305
+ if (typeAnnotation.type !== AST_NODE_TYPES.TSTypeQuery) {
306
+ context.report({
307
+ node: reportCallee,
308
+ messageId: 'missingTypeofImport'
309
+ });
310
+ return;
311
+ }
312
+ const resolved = extractImportPathFromTypeQuery(typeAnnotation);
313
+ if (!resolved) {
314
+ context.report({
315
+ node: reportCallee,
316
+ messageId: 'missingTypeofImport'
317
+ });
318
+ return;
319
+ }
320
+ importPath = resolved;
321
+ } else {
322
+ // ── Case 3: Inline `typeof import('...')` ────────────────────────
323
+ importPath = genericType.importPath;
324
+ }
325
+
326
+ // Package-scoped imports (e.g. @atlaskit/foo/examples/...) are not allowed
327
+ if (importPath.startsWith('@')) {
328
+ context.report({
329
+ node: reportCallee,
330
+ messageId: 'noPackageImports'
331
+ });
332
+ return;
333
+ }
334
+
335
+ // Validate that the import path matches the arguments
336
+ const args = extractCallArgs(node, programBody, variableCache);
337
+ if (!args) {
338
+ // Dynamic arguments — can't validate statically
339
+ return;
340
+ }
341
+ const expectedPath = resolveExamplePathFromArgs(args.groupId, args.packageId, args.exampleId, filename);
342
+ if (!expectedPath) {
343
+ return;
344
+ }
345
+ const resolvedImport = path.normalize(path.resolve(path.dirname(filename), importPath));
346
+ const resolvedExpected = path.normalize(expectedPath);
347
+
348
+ // Compare without extensions so `.tsx` vs no extension doesn't matter
349
+ if (resolvedImport.replace(/\.(tsx?|jsx?)$/, '') !== resolvedExpected.replace(/\.(tsx?|jsx?)$/, '')) {
350
+ context.report({
351
+ node: estreeNode.arguments[0],
352
+ messageId: 'pathMismatch',
353
+ data: {
354
+ importPath,
355
+ groupId: args.groupId,
356
+ packageId: args.packageId,
357
+ exampleId: args.exampleId,
358
+ expectedPath: resolvedExpected
359
+ },
360
+ fix(fixer) {
361
+ var _node$typeArguments2;
362
+ const correctedPath = computeRelativeImportPath(filename, resolvedExpected);
363
+ const typeParams = (_node$typeArguments2 = node.typeArguments) !== null && _node$typeArguments2 !== void 0 ? _node$typeArguments2 : node.typeParameters;
364
+ if (!(typeParams !== null && typeParams !== void 0 && typeParams.range)) {
365
+ return null;
366
+ }
367
+ return fixer.replaceTextRange(typeParams.range, `<typeof import('${correctedPath}')>`);
368
+ }
369
+ });
370
+ }
371
+ }
372
+ };
373
+ }
374
+ };
375
+ export default rule;
package/dist/esm/index.js CHANGED
@@ -32,11 +32,14 @@ import noSparseCheckout from './rules/no-sparse-checkout';
32
32
  import noDirectDocumentUsage from './rules/no-direct-document-usage';
33
33
  import noSetImmediate from './rules/no-set-immediate';
34
34
  import preferCryptoRandomUuid from './rules/prefer-crypto-random-uuid';
35
+ import noRestrictedFedrampImports from './rules/no-restricted-fedramp-imports';
35
36
  import noBarrelEntryImports from './rules/import/no-barrel-entry-imports';
36
37
  import noBarrelEntryJestMock from './rules/import/no-barrel-entry-jest-mock';
37
38
  import noJestMockBarrelFiles from './rules/import/no-jest-mock-barrel-files';
38
39
  import noRelativeBarrelFileImports from './rules/import/no-relative-barrel-file-imports';
39
40
  import noConversationAssistantBarrelImports from './rules/import/no-conversation-assistant-barrel-imports';
41
+ import visitExampleTypeImportRequired from './rules/visit-example-type-import-required';
42
+ import ensureUseSyncExternalStoreServerSnapshot from './rules/ensure-use-sync-external-store-server-snapshot';
40
43
  import { join, normalize } from 'node:path';
41
44
  import { readFileSync } from 'node:fs';
42
45
  var jiraRoot;
@@ -86,15 +89,19 @@ var rules = {
86
89
  'no-direct-document-usage': noDirectDocumentUsage,
87
90
  'no-set-immediate': noSetImmediate,
88
91
  'prefer-crypto-random-uuid': preferCryptoRandomUuid,
92
+ 'no-restricted-fedramp-imports': noRestrictedFedrampImports,
89
93
  'no-barrel-entry-imports': noBarrelEntryImports,
90
94
  'no-barrel-entry-jest-mock': noBarrelEntryJestMock,
91
95
  'no-jest-mock-barrel-files': noJestMockBarrelFiles,
92
96
  'no-relative-barrel-file-imports': noRelativeBarrelFileImports,
93
- 'no-conversation-assistant-barrel-imports': noConversationAssistantBarrelImports
97
+ 'no-conversation-assistant-barrel-imports': noConversationAssistantBarrelImports,
98
+ 'visit-example-type-import-required': visitExampleTypeImportRequired,
99
+ 'ensure-use-sync-external-store-server-snapshot': ensureUseSyncExternalStoreServerSnapshot
94
100
  };
95
101
  var commonConfig = {
96
102
  '@atlaskit/platform/ensure-test-runner-arguments': 'error',
97
103
  '@atlaskit/platform/ensure-test-runner-nested-count': 'warn',
104
+ '@atlaskit/platform/ensure-use-sync-external-store-server-snapshot': 'error',
98
105
  '@atlaskit/platform/no-invalid-feature-flag-usage': 'error',
99
106
  '@atlaskit/platform/no-invalid-storybook-decorator-usage': 'error',
100
107
  '@atlaskit/platform/ensure-atlassian-team': 'error',