@atlaskit/eslint-plugin-platform 2.8.0 → 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 +9 -0
- package/dist/cjs/index.js +6 -1
- package/dist/cjs/rules/ensure-use-sync-external-store-server-snapshot/index.js +41 -0
- package/dist/cjs/rules/import/no-barrel-entry-imports/index.js +475 -67
- package/dist/cjs/rules/import/no-barrel-entry-jest-mock/index.js +387 -112
- package/dist/cjs/rules/import/no-jest-mock-barrel-files/index.js +3 -2
- package/dist/cjs/rules/import/no-relative-barrel-file-imports/index.js +7 -3
- package/dist/cjs/rules/import/shared/jest-utils.js +62 -9
- package/dist/cjs/rules/import/shared/package-resolution.js +156 -23
- package/dist/cjs/rules/visit-example-type-import-required/index.js +409 -0
- package/dist/es2019/index.js +6 -1
- package/dist/es2019/rules/ensure-use-sync-external-store-server-snapshot/index.js +43 -0
- package/dist/es2019/rules/import/no-barrel-entry-imports/index.js +372 -15
- package/dist/es2019/rules/import/no-barrel-entry-jest-mock/index.js +245 -17
- package/dist/es2019/rules/import/no-jest-mock-barrel-files/index.js +3 -2
- package/dist/es2019/rules/import/no-relative-barrel-file-imports/index.js +7 -3
- package/dist/es2019/rules/import/shared/jest-utils.js +44 -0
- package/dist/es2019/rules/import/shared/package-resolution.js +97 -5
- package/dist/es2019/rules/visit-example-type-import-required/index.js +375 -0
- package/dist/esm/index.js +6 -1
- package/dist/esm/rules/ensure-use-sync-external-store-server-snapshot/index.js +35 -0
- package/dist/esm/rules/import/no-barrel-entry-imports/index.js +475 -67
- package/dist/esm/rules/import/no-barrel-entry-jest-mock/index.js +388 -113
- package/dist/esm/rules/import/no-jest-mock-barrel-files/index.js +3 -2
- package/dist/esm/rules/import/no-relative-barrel-file-imports/index.js +7 -3
- package/dist/esm/rules/import/shared/jest-utils.js +61 -9
- package/dist/esm/rules/import/shared/package-resolution.js +156 -25
- package/dist/esm/rules/visit-example-type-import-required/index.js +402 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/rules/ensure-use-sync-external-store-server-snapshot/index.d.ts +3 -0
- package/dist/types/rules/import/shared/jest-utils.d.ts +8 -0
- package/dist/types/rules/import/shared/package-resolution.d.ts +22 -2
- package/dist/types/rules/visit-example-type-import-required/index.d.ts +4 -0
- package/dist/types-ts4.5/index.d.ts +12 -0
- package/dist/types-ts4.5/rules/ensure-use-sync-external-store-server-snapshot/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/import/shared/jest-utils.d.ts +8 -0
- package/dist/types-ts4.5/rules/import/shared/package-resolution.d.ts +22 -2
- package/dist/types-ts4.5/rules/visit-example-type-import-required/index.d.ts +4 -0
- package/package.json +3 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.default = exports.RULE_NAME = void 0;
|
|
8
|
+
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
9
|
+
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
|
|
10
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
11
|
+
var _path = _interopRequireDefault(require("path"));
|
|
12
|
+
var _utils = require("@typescript-eslint/utils");
|
|
13
|
+
var _typescriptEstree = require("@typescript-eslint/typescript-estree");
|
|
14
|
+
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
|
|
15
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
16
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
17
|
+
var RULE_NAME = exports.RULE_NAME = 'visit-example-type-import-required';
|
|
18
|
+
var messages = {
|
|
19
|
+
missingTypeofImport: 'visitExample must use typeof import(...) generic type parameter. ' + 'Use visitExample<typeof import("path/to/example.tsx")>(groupId, packageId, exampleId).',
|
|
20
|
+
invalidTypeParameter: 'visitExample generic type parameter must be a typeof import(...) expression.',
|
|
21
|
+
pathMismatch: 'The import path "{{ importPath }}" does not match the expected example file for ' + 'visitExample({{ groupId }}, {{ packageId }}, {{ exampleId }}). ' + 'Expected import to resolve to: {{ expectedPath }}',
|
|
22
|
+
noPackageImports: 'Package imports (e.g., @atlaskit/...) are not allowed in visitExample type parameters. Use a relative import path.',
|
|
23
|
+
typeAliasNotInlined: 'Type aliases for typeof import(...) must be inlined directly into the visitExample call. ' + 'Use visitExample<typeof import("...")>(...) instead of defining a type alias.',
|
|
24
|
+
suggestFixPath: 'Update import path to match visitExample arguments'
|
|
25
|
+
};
|
|
26
|
+
function isTargetFile(filename) {
|
|
27
|
+
return filename.endsWith('.spec.tsx');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extracts the import path string from a TSTypeQuery node of the form `typeof import('...')`.
|
|
32
|
+
* Returns null if the node doesn't match that shape.
|
|
33
|
+
*/
|
|
34
|
+
function extractImportPathFromTypeQuery(typeQuery) {
|
|
35
|
+
// TSTypeQuery { exprName: TSImportType { argument: TSLiteralType { literal: Literal } } }
|
|
36
|
+
var exprName = typeQuery.exprName;
|
|
37
|
+
if (exprName.type !== _utils.AST_NODE_TYPES.TSImportType) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
var argument = exprName.argument;
|
|
41
|
+
if (argument.type === _utils.AST_NODE_TYPES.TSLiteralType && argument.literal.type === _utils.AST_NODE_TYPES.Literal && typeof argument.literal.value === 'string') {
|
|
42
|
+
return argument.literal.value;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Builds a map of all TSTypeAliasDeclaration nodes in the file, keyed by name.
|
|
48
|
+
* Each entry records whether the alias is at the top level of the program (file-level).
|
|
49
|
+
*/
|
|
50
|
+
function collectTypeAliases(ast) {
|
|
51
|
+
var result = new Map();
|
|
52
|
+
|
|
53
|
+
// Cast through unknown to work around a version mismatch: @typescript-eslint/utils
|
|
54
|
+
// vendors its own copy of @typescript-eslint/types (v7) while the root node_modules
|
|
55
|
+
// has a different version (v5). The TSESTree types are structurally identical at
|
|
56
|
+
// runtime — the cast is safe.
|
|
57
|
+
(0, _typescriptEstree.simpleTraverse)(ast, {
|
|
58
|
+
enter: function enter(node, parent) {
|
|
59
|
+
// A type alias is "file-level" if its immediate parent is the Program,
|
|
60
|
+
// or if it's the declaration of a top-level ExportNamedDeclaration.
|
|
61
|
+
if (node.type === _utils.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
62
|
+
var isFileLevel = (parent === null || parent === void 0 ? void 0 : parent.type) === _utils.AST_NODE_TYPES.Program || (parent === null || parent === void 0 ? void 0 : parent.type) === _utils.AST_NODE_TYPES.ExportNamedDeclaration;
|
|
63
|
+
result.set(node.id.name, {
|
|
64
|
+
node: node,
|
|
65
|
+
isFileLevel: isFileLevel
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolves a top-level `const foo = 'literal'` declaration to its string value.
|
|
75
|
+
* Returns null for non-const, non-string, or not-found variables.
|
|
76
|
+
*/
|
|
77
|
+
function resolveVariableToConstant(programBody, variableName, cache) {
|
|
78
|
+
if (cache.has(variableName)) {
|
|
79
|
+
var _cache$get;
|
|
80
|
+
return (_cache$get = cache.get(variableName)) !== null && _cache$get !== void 0 ? _cache$get : null;
|
|
81
|
+
}
|
|
82
|
+
var _iterator = _createForOfIteratorHelper(programBody),
|
|
83
|
+
_step;
|
|
84
|
+
try {
|
|
85
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
86
|
+
var node = _step.value;
|
|
87
|
+
if (node.type !== _utils.AST_NODE_TYPES.VariableDeclaration || node.kind !== 'const') {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
var _iterator2 = _createForOfIteratorHelper(node.declarations),
|
|
91
|
+
_step2;
|
|
92
|
+
try {
|
|
93
|
+
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
|
|
94
|
+
var _declarator$init;
|
|
95
|
+
var declarator = _step2.value;
|
|
96
|
+
if (declarator.id.type === _utils.AST_NODE_TYPES.Identifier && declarator.id.name === variableName && ((_declarator$init = declarator.init) === null || _declarator$init === void 0 ? void 0 : _declarator$init.type) === _utils.AST_NODE_TYPES.Literal && typeof declarator.init.value === 'string') {
|
|
97
|
+
cache.set(variableName, declarator.init.value);
|
|
98
|
+
return declarator.init.value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
_iterator2.e(err);
|
|
103
|
+
} finally {
|
|
104
|
+
_iterator2.f();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
_iterator.e(err);
|
|
109
|
+
} finally {
|
|
110
|
+
_iterator.f();
|
|
111
|
+
}
|
|
112
|
+
cache.set(variableName, null);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extracts the (groupId, packageId, exampleId) string arguments from a visitExample call.
|
|
117
|
+
* Each argument may be a string literal or a reference to a top-level const string variable.
|
|
118
|
+
* Returns null for any argument that can't be statically resolved.
|
|
119
|
+
*/
|
|
120
|
+
function extractCallArgs(node, programBody, variableCache) {
|
|
121
|
+
if (node.arguments.length < 3) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function resolveArg(arg) {
|
|
125
|
+
if (arg.type === _utils.AST_NODE_TYPES.Literal && typeof arg.value === 'string') {
|
|
126
|
+
return arg.value;
|
|
127
|
+
}
|
|
128
|
+
if (arg.type === _utils.AST_NODE_TYPES.Identifier) {
|
|
129
|
+
return resolveVariableToConstant(programBody, arg.name, variableCache);
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
var groupId = resolveArg(node.arguments[0]);
|
|
134
|
+
var packageId = resolveArg(node.arguments[1]);
|
|
135
|
+
var exampleId = resolveArg(node.arguments[2]);
|
|
136
|
+
if (!groupId || !packageId || !exampleId) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
groupId: groupId,
|
|
141
|
+
packageId: packageId,
|
|
142
|
+
exampleId: exampleId
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function getPackagesBasePath(testFilePath) {
|
|
146
|
+
var testFileDir = _path.default.dirname(testFilePath);
|
|
147
|
+
var testFileSegments = testFileDir.split(_path.default.sep);
|
|
148
|
+
var packagesIndex = testFileSegments.findIndex(function (seg) {
|
|
149
|
+
return seg === 'packages';
|
|
150
|
+
});
|
|
151
|
+
if (packagesIndex === -1) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
var baseSegments = testFileSegments.slice(0, packagesIndex + 1);
|
|
155
|
+
var basePath = _path.default.isAbsolute(testFilePath) ? _path.default.resolve.apply(_path.default, ['/'].concat((0, _toConsumableArray2.default)(baseSegments))) : _path.default.resolve.apply(_path.default, [process.cwd()].concat((0, _toConsumableArray2.default)(baseSegments)));
|
|
156
|
+
return {
|
|
157
|
+
basePath: basePath,
|
|
158
|
+
packagesIndex: packagesIndex
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolves the expected example file path from visitExample arguments.
|
|
164
|
+
*
|
|
165
|
+
* visitExample('groupId', 'packageId', 'exampleId') maps to:
|
|
166
|
+
* packages/{groupId}/{packageId}/examples/{exampleId}.tsx
|
|
167
|
+
*
|
|
168
|
+
* Example files may also have a numeric sort prefix, e.g.:
|
|
169
|
+
* packages/{groupId}/{packageId}/examples/00-{exampleId}.tsx
|
|
170
|
+
*
|
|
171
|
+
* We scan the examples directory once and match against all candidates.
|
|
172
|
+
* Falls back to the bare `{exampleId}.tsx` name when the directory can't
|
|
173
|
+
* be read (e.g. in unit-test environments where the files don't exist).
|
|
174
|
+
*/
|
|
175
|
+
function resolveExamplePathFromArgs(groupId, packageId, exampleId, testFilePath) {
|
|
176
|
+
var packagesBase = getPackagesBasePath(testFilePath);
|
|
177
|
+
if (!packagesBase) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
var examplesDir = _path.default.resolve(packagesBase.basePath, groupId, packageId, 'examples');
|
|
181
|
+
var fallback = _path.default.resolve(examplesDir, "".concat(exampleId, ".tsx"));
|
|
182
|
+
|
|
183
|
+
// Match: exact name OR numeric-prefixed variant, with optional `.examples` infix
|
|
184
|
+
var candidateRe = new RegExp("^(?:\\d+-)?".concat(exampleId, "(?:\\.examples?)?\\.tsx$"));
|
|
185
|
+
try {
|
|
186
|
+
var match = _fs.default.readdirSync(examplesDir).find(function (f) {
|
|
187
|
+
return candidateRe.test(f);
|
|
188
|
+
});
|
|
189
|
+
if (match) {
|
|
190
|
+
return _path.default.resolve(examplesDir, match);
|
|
191
|
+
}
|
|
192
|
+
} catch (_unused) {
|
|
193
|
+
// Directory doesn't exist or can't be read (e.g. in test environments)
|
|
194
|
+
}
|
|
195
|
+
return fallback;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Computes a relative import path from one file to another
|
|
200
|
+
*/
|
|
201
|
+
function computeRelativeImportPath(fromFile, toFile) {
|
|
202
|
+
var fromDir = _path.default.dirname(fromFile);
|
|
203
|
+
var relativePath = _path.default.relative(fromDir, toFile);
|
|
204
|
+
// Normalize to forward slashes for import statements (standard in JavaScript/TypeScript)
|
|
205
|
+
relativePath = relativePath.replace(/\\/g, '/');
|
|
206
|
+
// Ensure relative imports start with ./ or ../
|
|
207
|
+
if (!relativePath.startsWith('.') && !relativePath.startsWith('/')) {
|
|
208
|
+
relativePath = "./".concat(relativePath);
|
|
209
|
+
}
|
|
210
|
+
return relativePath;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Extracts the generic type argument from a `visitExample<...>(...)` call expression
|
|
214
|
+
* using the AST directly (no regex on source text).
|
|
215
|
+
*
|
|
216
|
+
* Returns:
|
|
217
|
+
* { type: 'inline', importPath } — for `visitExample<typeof import('...')>(...)`
|
|
218
|
+
* { type: 'alias', name } — for `visitExample<SomeTypeAlias>(...)`
|
|
219
|
+
* null — if no generic type parameter is present
|
|
220
|
+
*/
|
|
221
|
+
function extractGenericType(node) {
|
|
222
|
+
var _params, _ref, _node$typeArguments;
|
|
223
|
+
// `typeArguments` is the current property name; `typeParameters` is the deprecated alias.
|
|
224
|
+
// We fall back to `typeParameters` for compatibility with older parser versions.
|
|
225
|
+
var 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 : [];
|
|
226
|
+
if (params.length === 0) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
var _params2 = (0, _slicedToArray2.default)(params, 1),
|
|
230
|
+
typeParam = _params2[0];
|
|
231
|
+
|
|
232
|
+
// `typeof import('...')` → TSTypeQuery { exprName: TSImportType { ... } }
|
|
233
|
+
if (typeParam.type === _utils.AST_NODE_TYPES.TSTypeQuery) {
|
|
234
|
+
var importPath = extractImportPathFromTypeQuery(typeParam);
|
|
235
|
+
if (importPath !== null) {
|
|
236
|
+
return {
|
|
237
|
+
type: 'inline',
|
|
238
|
+
importPath: importPath
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// `SomeTypeAlias` → TSTypeReference { typeName: Identifier { name } }
|
|
244
|
+
if (typeParam.type === _utils.AST_NODE_TYPES.TSTypeReference && typeParam.typeName.type === _utils.AST_NODE_TYPES.Identifier) {
|
|
245
|
+
return {
|
|
246
|
+
type: 'alias',
|
|
247
|
+
name: typeParam.typeName.name
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
var rule = {
|
|
253
|
+
meta: {
|
|
254
|
+
type: 'problem',
|
|
255
|
+
docs: {
|
|
256
|
+
description: 'Ensures that visitExample uses a typeof import(...) generic and that the import path matches the example file resolved from the call arguments.'
|
|
257
|
+
},
|
|
258
|
+
fixable: 'code',
|
|
259
|
+
messages: messages,
|
|
260
|
+
schema: []
|
|
261
|
+
},
|
|
262
|
+
create: function create(context) {
|
|
263
|
+
var filename = context.filename;
|
|
264
|
+
var ast = context.sourceCode.ast;
|
|
265
|
+
var programBody = ast.body;
|
|
266
|
+
|
|
267
|
+
// Build the type alias map once per file (lazily on first visitExample call)
|
|
268
|
+
var typeAliases = null;
|
|
269
|
+
function getTypeAliases() {
|
|
270
|
+
if (!typeAliases) {
|
|
271
|
+
typeAliases = collectTypeAliases(ast);
|
|
272
|
+
}
|
|
273
|
+
return typeAliases;
|
|
274
|
+
}
|
|
275
|
+
var variableCache = new Map();
|
|
276
|
+
return {
|
|
277
|
+
CallExpression: function CallExpression(estreeNode) {
|
|
278
|
+
if (!isTargetFile(filename)) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
var node = estreeNode;
|
|
282
|
+
// Only handle `<anything>.visitExample(...)` calls
|
|
283
|
+
if (node.callee.type !== _utils.AST_NODE_TYPES.MemberExpression || node.callee.property.type !== _utils.AST_NODE_TYPES.Identifier || node.callee.property.name !== 'visitExample') {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Narrow callee — we've confirmed property is an Identifier above
|
|
288
|
+
var callee = node.callee;
|
|
289
|
+
// reportCallee is typed as estree.Node for context.report compatibility
|
|
290
|
+
var reportCallee = estreeNode.callee;
|
|
291
|
+
var genericType = extractGenericType(node);
|
|
292
|
+
|
|
293
|
+
// ── Case 1: No generic type parameter ────────────────────────────────
|
|
294
|
+
if (genericType === null) {
|
|
295
|
+
var _args = extractCallArgs(node, programBody, variableCache);
|
|
296
|
+
context.report({
|
|
297
|
+
node: reportCallee,
|
|
298
|
+
messageId: 'missingTypeofImport',
|
|
299
|
+
fix: function fix(fixer) {
|
|
300
|
+
if (!_args) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
var examplePath = resolveExamplePathFromArgs(_args.groupId, _args.packageId, _args.exampleId, filename);
|
|
304
|
+
if (!examplePath) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
var importPath = computeRelativeImportPath(filename, examplePath);
|
|
308
|
+
var _ref2 = callee.property.range,
|
|
309
|
+
_ref3 = (0, _slicedToArray2.default)(_ref2, 2),
|
|
310
|
+
start = _ref3[0],
|
|
311
|
+
end = _ref3[1];
|
|
312
|
+
return fixer.insertTextAfterRange([start, end], "<typeof import('".concat(importPath, "')>"));
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Case 2: Generic is a type alias reference (`visitExample<Foo>`) ──
|
|
319
|
+
var importPath;
|
|
320
|
+
if (genericType.type === 'alias') {
|
|
321
|
+
var found = getTypeAliases().get(genericType.name);
|
|
322
|
+
if (!found) {
|
|
323
|
+
// Unknown type alias — not a typeof import
|
|
324
|
+
context.report({
|
|
325
|
+
node: reportCallee,
|
|
326
|
+
messageId: 'missingTypeofImport'
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (found.isFileLevel) {
|
|
331
|
+
// Top-level `type Foo = typeof import(...)` is disallowed
|
|
332
|
+
context.report({
|
|
333
|
+
node: reportCallee,
|
|
334
|
+
messageId: 'typeAliasNotInlined'
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
var typeAnnotation = found.node.typeAnnotation;
|
|
339
|
+
if (typeAnnotation.type !== _utils.AST_NODE_TYPES.TSTypeQuery) {
|
|
340
|
+
context.report({
|
|
341
|
+
node: reportCallee,
|
|
342
|
+
messageId: 'missingTypeofImport'
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
var resolved = extractImportPathFromTypeQuery(typeAnnotation);
|
|
347
|
+
if (!resolved) {
|
|
348
|
+
context.report({
|
|
349
|
+
node: reportCallee,
|
|
350
|
+
messageId: 'missingTypeofImport'
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
importPath = resolved;
|
|
355
|
+
} else {
|
|
356
|
+
// ── Case 3: Inline `typeof import('...')` ────────────────────────
|
|
357
|
+
importPath = genericType.importPath;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Package-scoped imports (e.g. @atlaskit/foo/examples/...) are not allowed
|
|
361
|
+
if (importPath.startsWith('@')) {
|
|
362
|
+
context.report({
|
|
363
|
+
node: reportCallee,
|
|
364
|
+
messageId: 'noPackageImports'
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Validate that the import path matches the arguments
|
|
370
|
+
var args = extractCallArgs(node, programBody, variableCache);
|
|
371
|
+
if (!args) {
|
|
372
|
+
// Dynamic arguments — can't validate statically
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
var expectedPath = resolveExamplePathFromArgs(args.groupId, args.packageId, args.exampleId, filename);
|
|
376
|
+
if (!expectedPath) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
var resolvedImport = _path.default.normalize(_path.default.resolve(_path.default.dirname(filename), importPath));
|
|
380
|
+
var resolvedExpected = _path.default.normalize(expectedPath);
|
|
381
|
+
|
|
382
|
+
// Compare without extensions so `.tsx` vs no extension doesn't matter
|
|
383
|
+
if (resolvedImport.replace(/\.(tsx?|jsx?)$/, '') !== resolvedExpected.replace(/\.(tsx?|jsx?)$/, '')) {
|
|
384
|
+
context.report({
|
|
385
|
+
node: estreeNode.arguments[0],
|
|
386
|
+
messageId: 'pathMismatch',
|
|
387
|
+
data: {
|
|
388
|
+
importPath: importPath,
|
|
389
|
+
groupId: args.groupId,
|
|
390
|
+
packageId: args.packageId,
|
|
391
|
+
exampleId: args.exampleId,
|
|
392
|
+
expectedPath: resolvedExpected
|
|
393
|
+
},
|
|
394
|
+
fix: function fix(fixer) {
|
|
395
|
+
var _node$typeArguments2;
|
|
396
|
+
var correctedPath = computeRelativeImportPath(filename, resolvedExpected);
|
|
397
|
+
var typeParams = (_node$typeArguments2 = node.typeArguments) !== null && _node$typeArguments2 !== void 0 ? _node$typeArguments2 : node.typeParameters;
|
|
398
|
+
if (!(typeParams !== null && typeParams !== void 0 && typeParams.range)) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return fixer.replaceTextRange(typeParams.range, "<typeof import('".concat(correctedPath, "')>"));
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var _default = exports.default = rule;
|
package/dist/es2019/index.js
CHANGED
|
@@ -34,6 +34,8 @@ import noBarrelEntryJestMock from './rules/import/no-barrel-entry-jest-mock';
|
|
|
34
34
|
import noJestMockBarrelFiles from './rules/import/no-jest-mock-barrel-files';
|
|
35
35
|
import noRelativeBarrelFileImports from './rules/import/no-relative-barrel-file-imports';
|
|
36
36
|
import noConversationAssistantBarrelImports from './rules/import/no-conversation-assistant-barrel-imports';
|
|
37
|
+
import visitExampleTypeImportRequired from './rules/visit-example-type-import-required';
|
|
38
|
+
import ensureUseSyncExternalStoreServerSnapshot from './rules/ensure-use-sync-external-store-server-snapshot';
|
|
37
39
|
import { join, normalize } from 'node:path';
|
|
38
40
|
import { readFileSync } from 'node:fs';
|
|
39
41
|
let jiraRoot;
|
|
@@ -87,11 +89,14 @@ const rules = {
|
|
|
87
89
|
'no-barrel-entry-jest-mock': noBarrelEntryJestMock,
|
|
88
90
|
'no-jest-mock-barrel-files': noJestMockBarrelFiles,
|
|
89
91
|
'no-relative-barrel-file-imports': noRelativeBarrelFileImports,
|
|
90
|
-
'no-conversation-assistant-barrel-imports': noConversationAssistantBarrelImports
|
|
92
|
+
'no-conversation-assistant-barrel-imports': noConversationAssistantBarrelImports,
|
|
93
|
+
'visit-example-type-import-required': visitExampleTypeImportRequired,
|
|
94
|
+
'ensure-use-sync-external-store-server-snapshot': ensureUseSyncExternalStoreServerSnapshot
|
|
91
95
|
};
|
|
92
96
|
const commonConfig = {
|
|
93
97
|
'@atlaskit/platform/ensure-test-runner-arguments': 'error',
|
|
94
98
|
'@atlaskit/platform/ensure-test-runner-nested-count': 'warn',
|
|
99
|
+
'@atlaskit/platform/ensure-use-sync-external-store-server-snapshot': 'error',
|
|
95
100
|
'@atlaskit/platform/no-invalid-feature-flag-usage': 'error',
|
|
96
101
|
'@atlaskit/platform/no-invalid-storybook-decorator-usage': 'error',
|
|
97
102
|
'@atlaskit/platform/ensure-atlassian-team': 'error',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
+
|
|
3
|
+
const FUNCTION_NAME = 'useSyncExternalStore';
|
|
4
|
+
const rule = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: `Enforce that ${FUNCTION_NAME} is called with a third argument (getServerSnapshot) for SSR compatibility`,
|
|
9
|
+
recommended: true
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
missingServerSnapshot: `'${FUNCTION_NAME}' must be called with a third argument (getServerSnapshot). Without it, React will throw during server-side rendering.
|
|
13
|
+
|
|
14
|
+
If 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 — this is the correct way to opt out of SSR, not an omission.
|
|
15
|
+
|
|
16
|
+
Prefer higher-level APIs that wrap ${FUNCTION_NAME} where available, as they handle SSR concerns for you.
|
|
17
|
+
|
|
18
|
+
See the React docs for usage guidance: https://react.dev/reference/react/useSyncExternalStore`
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
CallExpression(node) {
|
|
24
|
+
const {
|
|
25
|
+
callee,
|
|
26
|
+
arguments: args
|
|
27
|
+
} = node;
|
|
28
|
+
const isDirectCall = callee.type === 'Identifier' && callee.name === FUNCTION_NAME;
|
|
29
|
+
const isMemberCall = callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && callee.property.name === FUNCTION_NAME;
|
|
30
|
+
if (!isDirectCall && !isMemberCall) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (args.length < 3) {
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
messageId: 'missingServerSnapshot'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
export default rule;
|