@atlaskit/eslint-plugin-platform 2.7.2 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/cjs/index.js +19 -3
- package/dist/cjs/rules/ensure-use-sync-external-store-server-snapshot/index.js +41 -0
- package/dist/cjs/rules/feature-gating/valid-gate-name/index.js +60 -0
- package/dist/cjs/rules/import/no-barrel-entry-imports/index.js +1279 -0
- package/dist/cjs/rules/import/no-barrel-entry-jest-mock/index.js +1659 -0
- package/dist/cjs/rules/import/no-conversation-assistant-barrel-imports/index.js +43 -0
- package/dist/cjs/rules/import/no-jest-mock-barrel-files/index.js +1402 -0
- package/dist/cjs/rules/import/no-relative-barrel-file-imports/index.js +781 -0
- package/dist/cjs/rules/import/shared/barrel-parsing.js +511 -0
- package/dist/cjs/rules/import/shared/file-system.js +186 -0
- package/dist/cjs/rules/import/shared/jest-utils.js +244 -0
- package/dist/cjs/rules/import/shared/package-registry.js +263 -0
- package/dist/cjs/rules/import/shared/package-resolution.js +318 -0
- package/dist/cjs/rules/import/shared/perf.js +89 -0
- package/dist/cjs/rules/import/shared/types.js +67 -0
- package/dist/cjs/rules/visit-example-type-import-required/index.js +409 -0
- package/dist/es2019/index.js +19 -3
- package/dist/es2019/rules/ensure-use-sync-external-store-server-snapshot/index.js +43 -0
- package/dist/es2019/rules/feature-gating/valid-gate-name/index.js +52 -0
- package/dist/es2019/rules/import/no-barrel-entry-imports/index.js +1158 -0
- package/dist/es2019/rules/import/no-barrel-entry-jest-mock/index.js +1341 -0
- package/dist/es2019/rules/import/no-conversation-assistant-barrel-imports/index.js +37 -0
- package/dist/es2019/rules/import/no-jest-mock-barrel-files/index.js +1180 -0
- package/dist/es2019/rules/import/no-relative-barrel-file-imports/index.js +742 -0
- package/dist/es2019/rules/import/shared/barrel-parsing.js +433 -0
- package/dist/es2019/rules/import/shared/file-system.js +174 -0
- package/dist/es2019/rules/import/shared/jest-utils.js +203 -0
- package/dist/es2019/rules/import/shared/package-registry.js +240 -0
- package/dist/es2019/rules/import/shared/package-resolution.js +253 -0
- package/dist/es2019/rules/import/shared/perf.js +83 -0
- package/dist/es2019/rules/import/shared/types.js +57 -0
- package/dist/es2019/rules/visit-example-type-import-required/index.js +375 -0
- package/dist/esm/index.js +19 -3
- package/dist/esm/rules/ensure-use-sync-external-store-server-snapshot/index.js +35 -0
- package/dist/esm/rules/feature-gating/valid-gate-name/index.js +53 -0
- package/dist/esm/rules/import/no-barrel-entry-imports/index.js +1272 -0
- package/dist/esm/rules/import/no-barrel-entry-jest-mock/index.js +1650 -0
- package/dist/esm/rules/import/no-conversation-assistant-barrel-imports/index.js +37 -0
- package/dist/esm/rules/import/no-jest-mock-barrel-files/index.js +1392 -0
- package/dist/esm/rules/import/no-relative-barrel-file-imports/index.js +774 -0
- package/dist/esm/rules/import/shared/barrel-parsing.js +500 -0
- package/dist/esm/rules/import/shared/file-system.js +176 -0
- package/dist/esm/rules/import/shared/jest-utils.js +231 -0
- package/dist/esm/rules/import/shared/package-registry.js +256 -0
- package/dist/esm/rules/import/shared/package-resolution.js +306 -0
- package/dist/esm/rules/import/shared/perf.js +80 -0
- package/dist/esm/rules/import/shared/types.js +61 -0
- package/dist/esm/rules/visit-example-type-import-required/index.js +402 -0
- package/dist/types/index.d.ts +28 -2
- package/dist/types/rules/ensure-use-sync-external-store-server-snapshot/index.d.ts +3 -0
- package/dist/types/rules/feature-gating/valid-gate-name/index.d.ts +3 -0
- package/dist/types/rules/import/no-barrel-entry-imports/index.d.ts +9 -0
- package/dist/types/rules/import/no-barrel-entry-jest-mock/index.d.ts +9 -0
- package/dist/types/rules/import/no-conversation-assistant-barrel-imports/index.d.ts +3 -0
- package/dist/types/rules/import/no-jest-mock-barrel-files/index.d.ts +22 -0
- package/dist/types/rules/import/no-relative-barrel-file-imports/index.d.ts +5 -0
- package/dist/types/rules/import/shared/barrel-parsing.d.ts +30 -0
- package/dist/types/rules/import/shared/file-system.d.ts +38 -0
- package/dist/types/rules/import/shared/jest-utils.d.ts +55 -0
- package/dist/types/rules/import/shared/package-registry.d.ts +26 -0
- package/dist/types/rules/import/shared/package-resolution.d.ts +58 -0
- package/dist/types/rules/import/shared/perf.d.ts +13 -0
- package/dist/types/rules/import/shared/types.d.ts +131 -0
- package/dist/types/rules/visit-example-type-import-required/index.d.ts +4 -0
- package/dist/types-ts4.5/index.d.ts +28 -2
- package/dist/types-ts4.5/rules/import/no-barrel-entry-imports/index.d.ts +9 -0
- package/dist/types-ts4.5/rules/import/no-barrel-entry-jest-mock/index.d.ts +9 -0
- package/dist/types-ts4.5/rules/import/no-conversation-assistant-barrel-imports/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/import/no-jest-mock-barrel-files/index.d.ts +22 -0
- package/dist/types-ts4.5/rules/import/no-relative-barrel-file-imports/index.d.ts +5 -0
- package/dist/types-ts4.5/rules/import/shared/barrel-parsing.d.ts +30 -0
- package/dist/types-ts4.5/rules/import/shared/file-system.d.ts +38 -0
- package/dist/types-ts4.5/rules/import/shared/jest-utils.d.ts +55 -0
- package/dist/types-ts4.5/rules/import/shared/package-registry.d.ts +26 -0
- package/dist/types-ts4.5/rules/import/shared/package-resolution.d.ts +58 -0
- package/dist/types-ts4.5/rules/import/shared/perf.d.ts +13 -0
- package/dist/types-ts4.5/rules/import/shared/types.d.ts +131 -0
- package/dist/types-ts4.5/rules/visit-example-type-import-required/index.d.ts +4 -0
- package/package.json +6 -2
- package/dist/cjs/rules/ensure-native-and-af-exports-synced/index.js +0 -158
- package/dist/es2019/rules/ensure-native-and-af-exports-synced/index.js +0 -146
- package/dist/esm/rules/ensure-native-and-af-exports-synced/index.js +0 -151
- /package/dist/types-ts4.5/rules/{ensure-native-and-af-exports-synced → ensure-use-sync-external-store-server-snapshot}/index.d.ts +0 -0
- /package/dist/{types/rules/ensure-native-and-af-exports-synced → types-ts4.5/rules/feature-gating/valid-gate-name}/index.d.ts +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const PERF_ENV_VAR = 'INTERNAL_ESLINT_BARREL_PERF';
|
|
2
|
+
function nowMs() {
|
|
3
|
+
// eslint-disable-next-line no-restricted-globals
|
|
4
|
+
return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
|
|
5
|
+
}
|
|
6
|
+
export function isPerfEnabled() {
|
|
7
|
+
return process.env[PERF_ENV_VAR] === '1' || process.env[PERF_ENV_VAR] === 'true';
|
|
8
|
+
}
|
|
9
|
+
function ensurePerfInitialized({
|
|
10
|
+
fs
|
|
11
|
+
}) {
|
|
12
|
+
if (!fs.cache.perf) {
|
|
13
|
+
fs.cache.perf = {
|
|
14
|
+
installedExitHook: false,
|
|
15
|
+
counters: {},
|
|
16
|
+
timers: {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function ensureExitHookInstalled({
|
|
21
|
+
fs
|
|
22
|
+
}) {
|
|
23
|
+
if (!isPerfEnabled()) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
ensurePerfInitialized({
|
|
27
|
+
fs
|
|
28
|
+
});
|
|
29
|
+
if (fs.cache.perf.installedExitHook) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
fs.cache.perf.installedExitHook = true;
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.error(`[eslint-plugin-internal] perf enabled via ${PERF_ENV_VAR}`);
|
|
35
|
+
process.once('exit', () => {
|
|
36
|
+
var _fs$cache$perf$counte, _fs$cache$perf, _fs$cache$perf$timers, _fs$cache$perf2;
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.error(`[eslint-plugin-internal] perf exit hook fired (${PERF_ENV_VAR})`);
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
console.log(`[eslint-plugin-internal] perf summary (${PERF_ENV_VAR})`);
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log(JSON.stringify({
|
|
43
|
+
counters: (_fs$cache$perf$counte = (_fs$cache$perf = fs.cache.perf) === null || _fs$cache$perf === void 0 ? void 0 : _fs$cache$perf.counters) !== null && _fs$cache$perf$counte !== void 0 ? _fs$cache$perf$counte : {},
|
|
44
|
+
timers: (_fs$cache$perf$timers = (_fs$cache$perf2 = fs.cache.perf) === null || _fs$cache$perf2 === void 0 ? void 0 : _fs$cache$perf2.timers) !== null && _fs$cache$perf$timers !== void 0 ? _fs$cache$perf$timers : {}
|
|
45
|
+
}, null, 2));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function perfInc({
|
|
49
|
+
fs,
|
|
50
|
+
key,
|
|
51
|
+
by = 1
|
|
52
|
+
}) {
|
|
53
|
+
var _perf$counters$key;
|
|
54
|
+
if (!isPerfEnabled()) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
ensureExitHookInstalled({
|
|
58
|
+
fs
|
|
59
|
+
});
|
|
60
|
+
const perf = fs.cache.perf;
|
|
61
|
+
perf.counters[key] = ((_perf$counters$key = perf.counters[key]) !== null && _perf$counters$key !== void 0 ? _perf$counters$key : 0) + by;
|
|
62
|
+
}
|
|
63
|
+
export function perfTime({
|
|
64
|
+
fs,
|
|
65
|
+
key,
|
|
66
|
+
fn
|
|
67
|
+
}) {
|
|
68
|
+
if (!isPerfEnabled()) {
|
|
69
|
+
return fn();
|
|
70
|
+
}
|
|
71
|
+
ensureExitHookInstalled({
|
|
72
|
+
fs
|
|
73
|
+
});
|
|
74
|
+
const perf = fs.cache.perf;
|
|
75
|
+
const start = nowMs();
|
|
76
|
+
try {
|
|
77
|
+
return fn();
|
|
78
|
+
} finally {
|
|
79
|
+
var _perf$timers$key;
|
|
80
|
+
const duration = nowMs() - start;
|
|
81
|
+
perf.timers[key] = ((_perf$timers$key = perf.timers[key]) !== null && _perf$timers$key !== void 0 ? _perf$timers$key : 0) + duration;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Directory entry returned by readdirSync with withFileTypes option.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* State for the package registry cache.
|
|
10
|
+
* This is used to cache package name to directory mappings for efficient lookups.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Cache structure for file system operations.
|
|
15
|
+
* Contains both package registry cache and workspace root cache.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* File system abstraction for testability.
|
|
20
|
+
* This interface allows the core logic to be tested with mock file systems.
|
|
21
|
+
* The cache property holds package resolution state and can be passed as an empty
|
|
22
|
+
* object for tests to ensure fresh state for each test case.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Real file system implementation using Node.js fs module.
|
|
27
|
+
*/
|
|
28
|
+
export const realFileSystem = {
|
|
29
|
+
existsSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
realpathSync,
|
|
32
|
+
statSync,
|
|
33
|
+
readdirSync: (path, options) => readdirSync(path, options),
|
|
34
|
+
execSync: (command, options) => {
|
|
35
|
+
try {
|
|
36
|
+
return execSync(command, {
|
|
37
|
+
...options,
|
|
38
|
+
encoding: 'utf-8'
|
|
39
|
+
}).trim();
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
cache: {}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Information about cross-package re-export origin.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Information about where an export originates.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Context for package resolution operations.
|
|
57
|
+
*/
|
|
@@ -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');
|
|
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
|
@@ -16,7 +16,6 @@ import ensureNoPrivateDependencies from './rules/ensure-no-private-dependencies'
|
|
|
16
16
|
import expandBorderShorthand from './rules/compiled/expand-border-shorthand';
|
|
17
17
|
import noInvalidStorybookDecoratorUsage from './rules/no-invalid-storybook-decorator-usage';
|
|
18
18
|
import ensurePublishValid from './rules/ensure-publish-valid';
|
|
19
|
-
import ensureNativeAndAfExportsSynced from './rules/ensure-native-and-af-exports-synced';
|
|
20
19
|
import noModuleLevelEval from './rules/feature-gating/no-module-level-eval';
|
|
21
20
|
import noModuleLevelEvalNav4 from './rules/feature-gating/no-module-level-eval-nav4';
|
|
22
21
|
import staticFeatureFlags from './rules/feature-gating/static-feature-flags';
|
|
@@ -26,12 +25,20 @@ import preferFG from './rules/feature-gating/prefer-fg';
|
|
|
26
25
|
import noAlias from './rules/feature-gating/no-alias';
|
|
27
26
|
import useEntrypointsInExamples from './rules/use-entrypoints-in-examples';
|
|
28
27
|
import useRecommendedUtils from './rules/feature-gating/use-recommended-utils';
|
|
28
|
+
import validGateName from './rules/feature-gating/valid-gate-name';
|
|
29
29
|
import expandBackgroundShorthand from './rules/compiled/expand-background-shorthand';
|
|
30
30
|
import expandSpacingShorthand from './rules/compiled/expand-spacing-shorthand';
|
|
31
31
|
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 noBarrelEntryImports from './rules/import/no-barrel-entry-imports';
|
|
36
|
+
import noBarrelEntryJestMock from './rules/import/no-barrel-entry-jest-mock';
|
|
37
|
+
import noJestMockBarrelFiles from './rules/import/no-jest-mock-barrel-files';
|
|
38
|
+
import noRelativeBarrelFileImports from './rules/import/no-relative-barrel-file-imports';
|
|
39
|
+
import noConversationAssistantBarrelImports from './rules/import/no-conversation-assistant-barrel-imports';
|
|
40
|
+
import visitExampleTypeImportRequired from './rules/visit-example-type-import-required';
|
|
41
|
+
import ensureUseSyncExternalStoreServerSnapshot from './rules/ensure-use-sync-external-store-server-snapshot';
|
|
35
42
|
import { join, normalize } from 'node:path';
|
|
36
43
|
import { readFileSync } from 'node:fs';
|
|
37
44
|
var jiraRoot;
|
|
@@ -67,7 +74,6 @@ var rules = {
|
|
|
67
74
|
'no-pre-post-install-scripts': noPreAndPostInstallScripts,
|
|
68
75
|
'no-invalid-storybook-decorator-usage': noInvalidStorybookDecoratorUsage,
|
|
69
76
|
'ensure-publish-valid': ensurePublishValid,
|
|
70
|
-
'ensure-native-and-af-exports-synced': ensureNativeAndAfExportsSynced,
|
|
71
77
|
'no-module-level-eval': noModuleLevelEval,
|
|
72
78
|
'no-module-level-eval-nav4': noModuleLevelEvalNav4,
|
|
73
79
|
'static-feature-flags': staticFeatureFlags,
|
|
@@ -77,14 +83,23 @@ var rules = {
|
|
|
77
83
|
'no-alias': noAlias,
|
|
78
84
|
'use-entrypoints-in-examples': useEntrypointsInExamples,
|
|
79
85
|
'use-recommended-utils': useRecommendedUtils,
|
|
86
|
+
'valid-gate-name': validGateName,
|
|
80
87
|
'no-sparse-checkout': noSparseCheckout,
|
|
81
88
|
'no-direct-document-usage': noDirectDocumentUsage,
|
|
82
89
|
'no-set-immediate': noSetImmediate,
|
|
83
|
-
'prefer-crypto-random-uuid': preferCryptoRandomUuid
|
|
90
|
+
'prefer-crypto-random-uuid': preferCryptoRandomUuid,
|
|
91
|
+
'no-barrel-entry-imports': noBarrelEntryImports,
|
|
92
|
+
'no-barrel-entry-jest-mock': noBarrelEntryJestMock,
|
|
93
|
+
'no-jest-mock-barrel-files': noJestMockBarrelFiles,
|
|
94
|
+
'no-relative-barrel-file-imports': noRelativeBarrelFileImports,
|
|
95
|
+
'no-conversation-assistant-barrel-imports': noConversationAssistantBarrelImports,
|
|
96
|
+
'visit-example-type-import-required': visitExampleTypeImportRequired,
|
|
97
|
+
'ensure-use-sync-external-store-server-snapshot': ensureUseSyncExternalStoreServerSnapshot
|
|
84
98
|
};
|
|
85
99
|
var commonConfig = {
|
|
86
100
|
'@atlaskit/platform/ensure-test-runner-arguments': 'error',
|
|
87
101
|
'@atlaskit/platform/ensure-test-runner-nested-count': 'warn',
|
|
102
|
+
'@atlaskit/platform/ensure-use-sync-external-store-server-snapshot': 'error',
|
|
88
103
|
'@atlaskit/platform/no-invalid-feature-flag-usage': 'error',
|
|
89
104
|
'@atlaskit/platform/no-invalid-storybook-decorator-usage': 'error',
|
|
90
105
|
'@atlaskit/platform/ensure-atlassian-team': 'error',
|
|
@@ -110,6 +125,7 @@ var recommendedRules = _objectSpread(_objectSpread({}, commonConfig), {}, {
|
|
|
110
125
|
'@atlaskit/platform/inline-usage': 'error',
|
|
111
126
|
'@atlaskit/platform/prefer-fg': 'error',
|
|
112
127
|
'@atlaskit/platform/no-alias': 'error',
|
|
128
|
+
'@atlaskit/platform/valid-gate-name': 'error',
|
|
113
129
|
// end: feature-gating rules
|
|
114
130
|
'@atlaskit/platform/ensure-feature-flag-registration': 'error'
|
|
115
131
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
+
|
|
3
|
+
var FUNCTION_NAME = 'useSyncExternalStore';
|
|
4
|
+
var rule = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: "Enforce that ".concat(FUNCTION_NAME, " is called with a third argument (getServerSnapshot) for SSR compatibility"),
|
|
9
|
+
recommended: true
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
missingServerSnapshot: "'".concat(FUNCTION_NAME, "' must be called with a third argument (getServerSnapshot). Without it, React will throw during server-side rendering.\n\nIf your component relies on browser-only APIs (e.g. localStorage, WebRTC, WebGL) and must not render on the server, pass `() => null` (or another stable fallback) as the third argument \u2014 this is the correct way to opt out of SSR, not an omission.\n\nPrefer higher-level APIs that wrap ").concat(FUNCTION_NAME, " where available, as they handle SSR concerns for you.\n\nSee the React docs for usage guidance: https://react.dev/reference/react/useSyncExternalStore")
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
create: function create(context) {
|
|
16
|
+
return {
|
|
17
|
+
CallExpression: function CallExpression(node) {
|
|
18
|
+
var callee = node.callee,
|
|
19
|
+
args = node.arguments;
|
|
20
|
+
var isDirectCall = callee.type === 'Identifier' && callee.name === FUNCTION_NAME;
|
|
21
|
+
var isMemberCall = callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && callee.property.name === FUNCTION_NAME;
|
|
22
|
+
if (!isDirectCall && !isMemberCall) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (args.length < 3) {
|
|
26
|
+
context.report({
|
|
27
|
+
node: node,
|
|
28
|
+
messageId: 'missingServerSnapshot'
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
export default rule;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
|
|
2
|
+
import { FEATURE_API_IMPORT_SOURCES } from '../../constants';
|
|
3
|
+
import { isIdentifierImportedFrom } from '../utils';
|
|
4
|
+
var IMPORT_SOURCES = new Set([].concat(_toConsumableArray(FEATURE_API_IMPORT_SOURCES), ['@atlassian/jira-feature-flagging-utils', '@atlassian/jira-feature-gate-component']));
|
|
5
|
+
var FUNCTION_NAMES = new Set(['ff', 'fg', 'getFeatureFlagValue', 'componentWithFF', 'componentWithFG', 'passGate', 'withGate', 'expVal', 'expValEquals', 'UNSAFE_noExposureExp', 'mockExp', 'withExp', 'wasExperimentManuallyExposed']);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Valid gate names must only contain lowercase letters (a-z), numbers (0-9),
|
|
9
|
+
* underscores (_), hyphens (-), and dots (.).
|
|
10
|
+
* No spaces, capital letters, or other characters are allowed.
|
|
11
|
+
*/
|
|
12
|
+
var VALID_GATE_NAME_PATTERN = /^[a-z0-9_.-]+$/;
|
|
13
|
+
function isValidGateName(name) {
|
|
14
|
+
return VALID_GATE_NAME_PATTERN.test(name);
|
|
15
|
+
}
|
|
16
|
+
var rule = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'problem',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Ensure feature gate names contain only lowercase letters, numbers, underscores, and hyphens'
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
invalidGateName: 'Feature gate name "{{name}}" is invalid. Gate names must contain only lowercase letters (a-z), numbers (0-9), underscores (_), hyphens (-), and dots (.).'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
create: function create(context) {
|
|
27
|
+
return {
|
|
28
|
+
'CallExpression[callee.type="Identifier"][arguments.length>0][arguments.0.type="Literal"]': function CallExpressionCalleeTypeIdentifierArgumentsLength0Arguments0TypeLiteral(node) {
|
|
29
|
+
if (node.type !== 'CallExpression') {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (node.callee.type === 'Identifier' && (!FUNCTION_NAMES.has(node.callee.name) || !isIdentifierImportedFrom(node.callee.name, IMPORT_SOURCES, context, node))) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
var nameArgument = node.arguments[0];
|
|
36
|
+
if (nameArgument.type !== 'Literal' || typeof nameArgument.value !== 'string') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
var gateName = nameArgument.value;
|
|
40
|
+
if (!isValidGateName(gateName)) {
|
|
41
|
+
context.report({
|
|
42
|
+
node: nameArgument,
|
|
43
|
+
messageId: 'invalidGateName',
|
|
44
|
+
data: {
|
|
45
|
+
name: gateName
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
export default rule;
|