@atlaskit/eslint-plugin-platform 2.9.2 → 2.10.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 +15 -0
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/rules/compiled/expand-motion-shorthand/index.js +281 -0
- package/dist/cjs/rules/compiled/use-motion-token-values/index.js +506 -0
- package/dist/cjs/rules/editor-example-type-import-required/index.js +321 -0
- package/dist/cjs/rules/import/one-value-export-per-file/index.js +203 -0
- package/dist/es2019/index.js +11 -1
- package/dist/es2019/rules/compiled/expand-motion-shorthand/index.js +239 -0
- package/dist/es2019/rules/compiled/use-motion-token-values/index.js +444 -0
- package/dist/es2019/rules/editor-example-type-import-required/index.js +286 -0
- package/dist/es2019/rules/import/one-value-export-per-file/index.js +191 -0
- package/dist/esm/index.js +11 -1
- package/dist/esm/rules/compiled/expand-motion-shorthand/index.js +275 -0
- package/dist/esm/rules/compiled/use-motion-token-values/index.js +499 -0
- package/dist/esm/rules/editor-example-type-import-required/index.js +314 -0
- package/dist/esm/rules/import/one-value-export-per-file/index.js +196 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/rules/compiled/expand-motion-shorthand/index.d.ts +3 -0
- package/dist/types/rules/compiled/use-motion-token-values/index.d.ts +3 -0
- package/dist/types/rules/editor-example-type-import-required/index.d.ts +4 -0
- package/dist/types/rules/import/one-value-export-per-file/index.d.ts +3 -0
- package/dist/types-ts4.5/index.d.ts +6 -0
- package/dist/types-ts4.5/rules/compiled/expand-motion-shorthand/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/compiled/use-motion-token-values/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/editor-example-type-import-required/index.d.ts +4 -0
- package/dist/types-ts4.5/rules/import/one-value-export-per-file/index.d.ts +3 -0
- package/package.json +2 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
4
|
+
export const RULE_NAME = 'editor-example-type-import-required';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Spec files that are excluded from this rule because they don't use visitExample
|
|
8
|
+
* or have their own test harness that doesn't follow the exampleName fixture pattern.
|
|
9
|
+
*
|
|
10
|
+
* Paths are matched as suffixes of the file path (platform-relative).
|
|
11
|
+
*/
|
|
12
|
+
const EXCLUDED_SPEC_FILES = [
|
|
13
|
+
// Meta-tests for the testing infrastructure itself
|
|
14
|
+
'build/test-tooling/integration-testing/src/examples/__tests__/playwright/example.spec.ts', 'build/test-tooling/integration-testing/src/matchers/__tests__/playwright/to-have-height.spec.ts', 'build/test-tooling/integration-testing/src/matchers/__tests__/playwright/to-have-width.spec.ts',
|
|
15
|
+
// Tests the a11y decorator itself, no visitExample
|
|
16
|
+
'packages/accessibility/axe-integration/a11y-playwright-testing/src/auto-a11y-setup/__tests__/playwright/skip-decorator.spec.ts',
|
|
17
|
+
// Stub test (expect(true).toBe(true)), no visitExample
|
|
18
|
+
'packages/ai-mate/rovo-content-bridge-api/__tests__/playwright/index.spec.tsx',
|
|
19
|
+
// Page-object's visitExample call carries the typeof import(...) generic,
|
|
20
|
+
// so the typed example reference lives in _helpers/page-object.ts rather
|
|
21
|
+
// than in test.use({ exampleName }) in the spec itself.
|
|
22
|
+
'packages/navigation/atlassian-switcher/src/__tests__/playwright/navigate-child-item.spec.ts', 'packages/navigation/atlassian-switcher/src/__tests__/playwright/navigate-link-item.spec.ts', 'packages/navigation/atlassian-switcher/src/__tests__/playwright/navigate-product-item.spec.ts',
|
|
23
|
+
// Spec runs through a page-object (pages/generic-form-renderer.ts) whose
|
|
24
|
+
// visitExample call already carries the typeof import(...) generic. The
|
|
25
|
+
// typed reference therefore lives in the colocated page-object, not in
|
|
26
|
+
// test.use({ exampleName }) in the spec.
|
|
27
|
+
'packages/proforma/proforma-common-core/__tests__/playwright/json-test-cases.spec.ts', 'packages/proforma/proforma-form-renderer/__tests__/playwright/json-test-cases.spec.ts',
|
|
28
|
+
// Spec runs through a page-object that still uses raw page.goto() against
|
|
29
|
+
// /examples.html. Migrating these requires reworking the page-object to
|
|
30
|
+
// route through visitExample<typeof import(...)>(...).
|
|
31
|
+
'packages/proforma/proforma-form-list/__tests__/playwright/form-list.spec.ts', 'packages/proforma/proforma-form-renderer/__tests__/playwright/form-renderer.spec.ts', 'packages/proforma/proforma-translations-editor/__tests__/playwright/translations-editor-with-form.spec.ts',
|
|
32
|
+
// Tests the website itself, not examples
|
|
33
|
+
'website/src/__tests__/playwright/examples.spec.ts', 'website/src/__tests__/playwright/home.spec.ts',
|
|
34
|
+
// react-ufo: uses an `examplePage: string` fixture where the name (e.g. 'basic') is
|
|
35
|
+
// resolved to the example file internally by visitExample — the name does not match
|
|
36
|
+
// the file name (e.g. '01-basic.tsx'), so a typeof import assertion is not possible
|
|
37
|
+
// without refactoring the fixture to use keyof typeof import directly. Specs that
|
|
38
|
+
// happen to also use the inline `visitExample<typeof import(...)>` pattern alongside
|
|
39
|
+
// the fixture pass via the file-level typeof import check above and don't appear here.
|
|
40
|
+
'packages/react-ufo/atlaskit/__tests__/playwright/apply-segments-threshold.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/bad-replacement-node.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/base-10-sections.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/base-100-sections.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/base-3-sections-ssr-timings.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/base-3-sections-unmount.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/css-display-contents.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/custom-cohort-data.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/custom-data.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/data-vc-ignore-if-no-layout-shift.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/full-pixel-horizontal.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/full-pixel.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/fy25_02.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/hold.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/interactions-responsiveness.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/interactions-unknown-element.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/interactions-vc.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/is-opened-in-background.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/is-tab-throttled.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/metric-variants.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/minor-interactions.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/non-visual-style.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/page-visibility-hidden-timestamp.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/payload-integrity.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/post-interaction-late-holds.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/post-interaction-log-always-send.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/replacement-node.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/revisions.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/same-attribute-value-mutation.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/speed-index.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/ssr-placeholder-v3.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/terminal-error.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/third-party-segment-extra-metrics.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/third-party-segment-iframe.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/third-party-segment-timings.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/third-party-segment.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/transition-vc.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/ttai.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/ufo-blindspot-watchdog.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/ufo-errors.spec.ts', 'packages/react-ufo/atlaskit/__tests__/playwright/vc-dirty.spec.ts',
|
|
41
|
+
// editor-performance-metrics: same pattern as react-ufo — uses an `examplePage: string` fixture
|
|
42
|
+
// where the name resolves internally and does not match the example file name.
|
|
43
|
+
'packages/editor/editor-performance-metrics/__tests__/playwright/basic-editor-ttai.spec.ts', 'packages/editor/editor-performance-metrics/__tests__/playwright/basic-react-app.spec.ts', 'packages/editor/editor-performance-metrics/__tests__/playwright/latency-track.spec.ts', 'packages/editor/editor-performance-metrics/__tests__/playwright/ttai-timers.spec.ts', 'packages/editor/editor-performance-metrics/__tests__/playwright/vc-next-attribute-change.spec.ts', 'packages/editor/editor-performance-metrics/__tests__/playwright/vc-next-element-moving.spec.ts', 'packages/editor/editor-performance-metrics/__tests__/playwright/vc-next-moving-node.spec.ts', 'packages/editor/editor-performance-metrics/__tests__/playwright/vc-next-placeholder.spec.ts', 'packages/editor/editor-performance-metrics/__tests__/playwright/vc-next-react-remounting.spec.ts', 'packages/editor/editor-performance-metrics/__tests__/playwright/vc-next-track-user-events.spec.ts', 'packages/editor/editor-performance-metrics/__tests__/playwright/vc-next.spec.ts',
|
|
44
|
+
// generative-ai-modal: the example name is passed dynamically at examplePage.goto({ example: '...' })
|
|
45
|
+
// time rather than via test.use(), so a static typeof import assertion in the spec is not possible.
|
|
46
|
+
'packages/editor/generative-ai-modal/src/ui/screens/Preview/__tests__/playwright/tab-navigation.spec.ts'];
|
|
47
|
+
function isExcluded(filename) {
|
|
48
|
+
const normalised = filename.replace(/\\/g, '/');
|
|
49
|
+
return EXCLUDED_SPEC_FILES.some(excluded => normalised.endsWith(excluded));
|
|
50
|
+
}
|
|
51
|
+
const messages = {
|
|
52
|
+
missingExampleName: 'Playwright spec files must include exampleName with a ' + 'typeof import type assertion in test.use(). ' + "Add: exampleName: 'testing' as keyof typeof import('../../../examples/testing.tsx') ",
|
|
53
|
+
missingTypeAssertion: 'exampleName must include a typeof import type assertion for the static import graph. ' + "Use: exampleName: '{{ value }}' as keyof typeof import('{{ expectedPath }}') ",
|
|
54
|
+
pathMismatch: 'The import path "{{ importPath }}" does not resolve to the expected example file ' + "for exampleName '{{ exampleName }}'. Expected: {{ expectedPath }}"
|
|
55
|
+
};
|
|
56
|
+
function isTargetFile(filename) {
|
|
57
|
+
return (filename.endsWith('.spec.tsx') || filename.endsWith('.spec.ts')) && !isExcluded(filename);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolves the example file path from the spec file's location and the example name.
|
|
62
|
+
* Editor specs follow: packages/{groupId}/{packageId}/src/__tests__/playwright/*.spec.ts
|
|
63
|
+
* Examples live at: packages/{groupId}/{packageId}/examples/{exampleName}.tsx
|
|
64
|
+
*/
|
|
65
|
+
function resolveExamplePath(testFilePath, exampleName) {
|
|
66
|
+
const testFileDir = path.dirname(testFilePath);
|
|
67
|
+
const segments = testFileDir.split(path.sep);
|
|
68
|
+
const packagesIndex = segments.findIndex(seg => seg === 'packages');
|
|
69
|
+
if (packagesIndex === -1) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const groupId = segments[packagesIndex + 1];
|
|
73
|
+
const packageId = segments[packagesIndex + 2];
|
|
74
|
+
if (!groupId || !packageId) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const basePath = path.isAbsolute(testFilePath) ? path.resolve('/', ...segments.slice(0, packagesIndex + 1)) : path.resolve(process.cwd(), ...segments.slice(0, packagesIndex + 1));
|
|
78
|
+
const examplesDir = path.resolve(basePath, groupId, packageId, 'examples');
|
|
79
|
+
const candidateRe = new RegExp(`^(?:\\d+-)?${exampleName}(?:\\.examples?)?\\.tsx$`);
|
|
80
|
+
try {
|
|
81
|
+
const match = fs.readdirSync(examplesDir).find(f => candidateRe.test(f));
|
|
82
|
+
if (match) {
|
|
83
|
+
return path.resolve(examplesDir, match);
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Directory doesn't exist (e.g. test environments)
|
|
87
|
+
}
|
|
88
|
+
return path.resolve(examplesDir, `${exampleName}.tsx`);
|
|
89
|
+
}
|
|
90
|
+
function computeRelativeImportPath(fromFile, toFile) {
|
|
91
|
+
const fromDir = path.dirname(fromFile);
|
|
92
|
+
let relativePath = path.relative(fromDir, toFile);
|
|
93
|
+
relativePath = relativePath.replace(/\\/g, '/');
|
|
94
|
+
if (!relativePath.startsWith('.') && !relativePath.startsWith('/')) {
|
|
95
|
+
relativePath = `./${relativePath}`;
|
|
96
|
+
}
|
|
97
|
+
return relativePath;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a property value has a `keyof typeof import(...)` type assertion.
|
|
102
|
+
* Handles both:
|
|
103
|
+
* 'name' as keyof typeof import('...')
|
|
104
|
+
*/
|
|
105
|
+
function extractTypeofImportPath(node) {
|
|
106
|
+
if (node.type !== AST_NODE_TYPES.TSAsExpression) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const typeAnnotation = node.typeAnnotation;
|
|
110
|
+
if (typeAnnotation.type === AST_NODE_TYPES.TSTypeOperator && typeAnnotation.operator === 'keyof') {
|
|
111
|
+
return extractFromTypeQuery(typeAnnotation.typeAnnotation);
|
|
112
|
+
}
|
|
113
|
+
if (typeAnnotation.type === AST_NODE_TYPES.TSUnionType) {
|
|
114
|
+
for (const member of typeAnnotation.types) {
|
|
115
|
+
if (member.type === AST_NODE_TYPES.TSTypeOperator && member.operator === 'keyof') {
|
|
116
|
+
const result = extractFromTypeQuery(member.typeAnnotation);
|
|
117
|
+
if (result) {
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function extractFromTypeQuery(node) {
|
|
126
|
+
if (!node || node.type !== AST_NODE_TYPES.TSTypeQuery) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const {
|
|
130
|
+
exprName
|
|
131
|
+
} = node;
|
|
132
|
+
if (exprName.type !== AST_NODE_TYPES.TSImportType) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const {
|
|
136
|
+
argument
|
|
137
|
+
} = exprName;
|
|
138
|
+
if (argument.type === AST_NODE_TYPES.TSLiteralType && argument.literal.type === AST_NODE_TYPES.Literal && typeof argument.literal.value === 'string') {
|
|
139
|
+
return argument.literal.value;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Extract the string value from a property value, ignoring type assertions.
|
|
146
|
+
*/
|
|
147
|
+
function getStringValue(node) {
|
|
148
|
+
if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') {
|
|
149
|
+
return node.value;
|
|
150
|
+
}
|
|
151
|
+
if (node.type === AST_NODE_TYPES.TSAsExpression) {
|
|
152
|
+
return getStringValue(node.expression);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const rule = {
|
|
157
|
+
meta: {
|
|
158
|
+
type: 'problem',
|
|
159
|
+
docs: {
|
|
160
|
+
description: 'Ensures that editor spec files using @af/editor-libra include exampleName with a ' + 'typeof import type assertion in test.use() for the static import graph (factsMap).'
|
|
161
|
+
},
|
|
162
|
+
fixable: 'code',
|
|
163
|
+
messages,
|
|
164
|
+
schema: []
|
|
165
|
+
},
|
|
166
|
+
create(context) {
|
|
167
|
+
const filename = context.filename;
|
|
168
|
+
if (!isTargetFile(filename)) {
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
'Program:exit'(estreeNode) {
|
|
173
|
+
const program = estreeNode;
|
|
174
|
+
|
|
175
|
+
// Single AST walk: collect all test.use() calls AND detect whether the
|
|
176
|
+
// file contains any `typeof import('...')` reference at all (a TSImportType
|
|
177
|
+
// node). Either signal is sufficient evidence that the spec ties at least
|
|
178
|
+
// one example file into its TypeScript import graph.
|
|
179
|
+
const testUseCalls = [];
|
|
180
|
+
let hasAnyTypeofImport = false;
|
|
181
|
+
const visited = new Set();
|
|
182
|
+
const queue = [program];
|
|
183
|
+
while (queue.length > 0) {
|
|
184
|
+
const node = queue.shift();
|
|
185
|
+
if (visited.has(node)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
visited.add(node);
|
|
189
|
+
if (node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === 'use' && node.arguments.length > 0 && node.arguments[0].type === AST_NODE_TYPES.ObjectExpression) {
|
|
190
|
+
testUseCalls.push(node);
|
|
191
|
+
}
|
|
192
|
+
if (node.type === AST_NODE_TYPES.TSImportType) {
|
|
193
|
+
hasAnyTypeofImport = true;
|
|
194
|
+
}
|
|
195
|
+
for (const key of Object.keys(node)) {
|
|
196
|
+
if (key === 'parent') {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const child = node[key];
|
|
200
|
+
if (Array.isArray(child)) {
|
|
201
|
+
for (const item of child) {
|
|
202
|
+
if (item && typeof item.type === 'string') {
|
|
203
|
+
queue.push(item);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} else if (child && typeof child.type === 'string') {
|
|
207
|
+
queue.push(child);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Any `typeof import('...')` anywhere in the spec — including the
|
|
213
|
+
// `page.visitExample<typeof import('...')>(...)` and
|
|
214
|
+
// `page.visitMockedExample<typeof import('...')>(...)` patterns used
|
|
215
|
+
// outside of test.use() — satisfies the same goal as the canonical
|
|
216
|
+
// `test.use({ exampleName: '...' as keyof typeof import('...') })`
|
|
217
|
+
// pattern: the example file is referenced from the spec's TypeScript
|
|
218
|
+
// import graph.
|
|
219
|
+
if (hasAnyTypeofImport) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check if any test.use() call anywhere in the file has exampleName with a typeof import assertion
|
|
224
|
+
const fileHasExampleName = testUseCalls.some(call => {
|
|
225
|
+
const obj = call.arguments[0];
|
|
226
|
+
return obj.properties.some(prop => {
|
|
227
|
+
if (prop.type !== AST_NODE_TYPES.Property || prop.key.type !== AST_NODE_TYPES.Identifier || prop.key.name !== 'exampleName') {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
return extractTypeofImportPath(prop.value) !== null;
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
if (fileHasExampleName) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Find the first test.use() call that has exampleName without the typeof import assertion
|
|
238
|
+
// (if any), otherwise use the first test.use() call
|
|
239
|
+
let targetCall = testUseCalls[0];
|
|
240
|
+
let existingExampleNameProp = null;
|
|
241
|
+
for (const call of testUseCalls) {
|
|
242
|
+
const obj = call.arguments[0];
|
|
243
|
+
const prop = obj.properties.find(p => p.type === AST_NODE_TYPES.Property && p.key.type === AST_NODE_TYPES.Identifier && p.key.name === 'exampleName');
|
|
244
|
+
if (prop) {
|
|
245
|
+
targetCall = call;
|
|
246
|
+
existingExampleNameProp = prop;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!targetCall) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const objectArg = targetCall.arguments[0];
|
|
254
|
+
|
|
255
|
+
// Determine the example name to use for the import path
|
|
256
|
+
const exampleNameValue = existingExampleNameProp ? getStringValue(existingExampleNameProp.value) : null;
|
|
257
|
+
const defaultName = exampleNameValue !== null && exampleNameValue !== void 0 ? exampleNameValue : 'testing';
|
|
258
|
+
const examplePath = resolveExamplePath(filename, defaultName);
|
|
259
|
+
if (!examplePath) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const importPath = computeRelativeImportPath(filename, examplePath);
|
|
263
|
+
context.report({
|
|
264
|
+
node: targetCall,
|
|
265
|
+
messageId: 'missingExampleName',
|
|
266
|
+
fix(fixer) {
|
|
267
|
+
// If exampleName exists but lacks typeof import, replace its value
|
|
268
|
+
if (existingExampleNameProp) {
|
|
269
|
+
return fixer.replaceText(existingExampleNameProp.value, `'${defaultName}' as keyof typeof import('${importPath}') `);
|
|
270
|
+
}
|
|
271
|
+
// Otherwise insert exampleName as the first property
|
|
272
|
+
const firstProp = objectArg.properties[0];
|
|
273
|
+
if (!firstProp) {
|
|
274
|
+
return fixer.replaceText(targetCall.arguments[0], `{\n\texampleName: '${defaultName}' as keyof typeof import('${importPath}'),\n}`);
|
|
275
|
+
}
|
|
276
|
+
const sourceCode = context.sourceCode;
|
|
277
|
+
const token = sourceCode.getFirstToken(firstProp);
|
|
278
|
+
const indent = token ? '\t'.repeat(token.loc.start.column) : '\t';
|
|
279
|
+
return fixer.insertTextBefore(firstProp, `exampleName: '${defaultName}' as keyof typeof import('${importPath}') ,\n${indent}`);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
export default rule;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
function getPropertyName(node) {
|
|
2
|
+
if (!node) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
if (node.type === 'Identifier') {
|
|
6
|
+
return node.name;
|
|
7
|
+
}
|
|
8
|
+
if (node.type === 'Literal') {
|
|
9
|
+
return String(node.value);
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function isPrimitiveLiteral(declarator) {
|
|
14
|
+
function isPrimitiveExpression(node) {
|
|
15
|
+
if (!node) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
switch (node.type) {
|
|
19
|
+
case 'Literal':
|
|
20
|
+
return typeof node.value === 'string' || typeof node.value === 'number' || typeof node.value === 'boolean' || node.value === null;
|
|
21
|
+
case 'TemplateLiteral':
|
|
22
|
+
return node.expressions.length === 0;
|
|
23
|
+
case 'Identifier':
|
|
24
|
+
return node.name === 'undefined';
|
|
25
|
+
case 'UnaryExpression':
|
|
26
|
+
return ['+', '-', '~', '!'].includes(node.operator) && isPrimitiveExpression(node.argument);
|
|
27
|
+
case 'BinaryExpression':
|
|
28
|
+
return isPrimitiveExpression(node.left) && isPrimitiveExpression(node.right);
|
|
29
|
+
case 'TSAsExpression':
|
|
30
|
+
case 'TSTypeAssertion':
|
|
31
|
+
case 'TSNonNullExpression':
|
|
32
|
+
return isPrimitiveExpression(node.expression);
|
|
33
|
+
default:
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return isPrimitiveExpression(declarator.init);
|
|
38
|
+
}
|
|
39
|
+
function collectBindingExports(node) {
|
|
40
|
+
if (!node) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
switch (node.type) {
|
|
44
|
+
case 'Identifier':
|
|
45
|
+
return [{
|
|
46
|
+
name: node.name,
|
|
47
|
+
loc: node.loc
|
|
48
|
+
}];
|
|
49
|
+
case 'ObjectPattern':
|
|
50
|
+
return node.properties.flatMap(property => {
|
|
51
|
+
if (property.type === 'RestElement') {
|
|
52
|
+
return collectBindingExports(property.argument);
|
|
53
|
+
}
|
|
54
|
+
return collectBindingExports(property.value);
|
|
55
|
+
});
|
|
56
|
+
case 'ArrayPattern':
|
|
57
|
+
return node.elements.flatMap(element => collectBindingExports(element));
|
|
58
|
+
case 'AssignmentPattern':
|
|
59
|
+
return collectBindingExports(node.left);
|
|
60
|
+
case 'RestElement':
|
|
61
|
+
return collectBindingExports(node.argument);
|
|
62
|
+
default:
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function getDefaultExportName(node) {
|
|
67
|
+
const declaration = node.declaration;
|
|
68
|
+
if ((declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') && declaration.id) {
|
|
69
|
+
return declaration.id.name;
|
|
70
|
+
}
|
|
71
|
+
return 'default';
|
|
72
|
+
}
|
|
73
|
+
function getDefaultExportLoc(node) {
|
|
74
|
+
const declaration = node.declaration;
|
|
75
|
+
if ((declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') && declaration.id) {
|
|
76
|
+
return declaration.id.loc;
|
|
77
|
+
}
|
|
78
|
+
return node.loc;
|
|
79
|
+
}
|
|
80
|
+
function collectDeclarationExports(declaration, allowPrimitiveExports) {
|
|
81
|
+
if (!declaration) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
switch (declaration.type) {
|
|
85
|
+
case 'VariableDeclaration':
|
|
86
|
+
return declaration.declarations.flatMap(declarator => {
|
|
87
|
+
if (allowPrimitiveExports && isPrimitiveLiteral(declarator)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
return collectBindingExports(declarator.id);
|
|
91
|
+
});
|
|
92
|
+
case 'FunctionDeclaration':
|
|
93
|
+
case 'ClassDeclaration':
|
|
94
|
+
return declaration.id ? [{
|
|
95
|
+
name: declaration.id.name,
|
|
96
|
+
loc: declaration.id.loc
|
|
97
|
+
}] : [];
|
|
98
|
+
case 'TSEnumDeclaration':
|
|
99
|
+
return [{
|
|
100
|
+
name: declaration.id.name,
|
|
101
|
+
loc: declaration.id.loc
|
|
102
|
+
}];
|
|
103
|
+
case 'TSInterfaceDeclaration':
|
|
104
|
+
case 'TSTypeAliasDeclaration':
|
|
105
|
+
return [];
|
|
106
|
+
default:
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function collectNamedSpecifierExports(node) {
|
|
111
|
+
if (node.exportKind === 'type') {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
return node.specifiers.flatMap(specifier => {
|
|
115
|
+
if (specifier.type !== 'ExportSpecifier' || specifier.exportKind === 'type') {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const exportedName = getPropertyName(specifier.exported);
|
|
119
|
+
return exportedName ? [{
|
|
120
|
+
name: exportedName,
|
|
121
|
+
loc: specifier.exported.loc
|
|
122
|
+
}] : [];
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const rule = {
|
|
126
|
+
meta: {
|
|
127
|
+
type: 'suggestion',
|
|
128
|
+
docs: {
|
|
129
|
+
description: 'Disallow more than one local value export per file.',
|
|
130
|
+
category: 'Best Practices',
|
|
131
|
+
recommended: false
|
|
132
|
+
},
|
|
133
|
+
schema: [{
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
allowPrimitiveExports: {
|
|
137
|
+
type: 'boolean',
|
|
138
|
+
description: 'When true, primitive value exports (strings, numbers, booleans, null, undefined, and template literals) are ignored when counting local value exports.'
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
additionalProperties: false
|
|
142
|
+
}],
|
|
143
|
+
messages: {
|
|
144
|
+
multipleValueExports: 'This file exports {{count}} local values ({{names}}). Keep one value export per file https://hello.atlassian.net/wiki/spaces/DevInfra/pages/6809881812/One+Export+Per+File'
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
create(context) {
|
|
148
|
+
var _, _options$allowPrimiti;
|
|
149
|
+
const options = (_ = context.options[0]) !== null && _ !== void 0 ? _ : {};
|
|
150
|
+
const allowPrimitiveExports = (_options$allowPrimiti = options.allowPrimitiveExports) !== null && _options$allowPrimiti !== void 0 ? _options$allowPrimiti : false;
|
|
151
|
+
const valueExports = [];
|
|
152
|
+
return {
|
|
153
|
+
ExportDefaultDeclaration(node) {
|
|
154
|
+
const exportNode = node;
|
|
155
|
+
valueExports.push({
|
|
156
|
+
name: getDefaultExportName(exportNode),
|
|
157
|
+
loc: getDefaultExportLoc(exportNode)
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
ExportNamedDeclaration(node) {
|
|
161
|
+
const exportNode = node;
|
|
162
|
+
|
|
163
|
+
// Re-export-only barrel syntax is intentionally ignored.
|
|
164
|
+
if (exportNode.source) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
valueExports.push(...collectDeclarationExports(exportNode.declaration, allowPrimitiveExports));
|
|
168
|
+
valueExports.push(...collectNamedSpecifierExports(exportNode));
|
|
169
|
+
},
|
|
170
|
+
'Program:exit'(node) {
|
|
171
|
+
if (valueExports.length <= 1) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const sampleNames = valueExports.slice(0, 5).map(valueExport => valueExport.name).join(', ');
|
|
175
|
+
const names = valueExports.length > 5 ? `${sampleNames}, and ${valueExports.length - 5} more` : sampleNames;
|
|
176
|
+
valueExports.forEach(valueExport => {
|
|
177
|
+
context.report({
|
|
178
|
+
node,
|
|
179
|
+
loc: valueExport.loc,
|
|
180
|
+
messageId: 'multipleValueExports',
|
|
181
|
+
data: {
|
|
182
|
+
count: String(valueExports.length),
|
|
183
|
+
names
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
export default rule;
|
package/dist/esm/index.js
CHANGED
|
@@ -29,6 +29,8 @@ 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 noCssPropInObjectSpread from './rules/compiled/no-css-prop-in-object-spread';
|
|
32
|
+
import useMotionTokenValues from './rules/compiled/use-motion-token-values';
|
|
33
|
+
import expandMotionShorthand from './rules/compiled/expand-motion-shorthand';
|
|
32
34
|
import noSparseCheckout from './rules/no-sparse-checkout';
|
|
33
35
|
import noDirectDocumentUsage from './rules/no-direct-document-usage';
|
|
34
36
|
import noSetImmediate from './rules/no-set-immediate';
|
|
@@ -39,7 +41,9 @@ import noBarrelEntryJestMock from './rules/import/no-barrel-entry-jest-mock';
|
|
|
39
41
|
import noJestMockBarrelFiles from './rules/import/no-jest-mock-barrel-files';
|
|
40
42
|
import noRelativeBarrelFileImports from './rules/import/no-relative-barrel-file-imports';
|
|
41
43
|
import noConversationAssistantBarrelImports from './rules/import/no-conversation-assistant-barrel-imports';
|
|
44
|
+
import oneValueExportPerFile from './rules/import/one-value-export-per-file';
|
|
42
45
|
import visitExampleTypeImportRequired from './rules/visit-example-type-import-required';
|
|
46
|
+
import editorExampleTypeImportRequired from './rules/editor-example-type-import-required';
|
|
43
47
|
import ensureUseSyncExternalStoreServerSnapshot from './rules/ensure-use-sync-external-store-server-snapshot';
|
|
44
48
|
import noXcssInCx from './rules/no-xcss-in-cx';
|
|
45
49
|
import { join, normalize } from 'node:path';
|
|
@@ -98,9 +102,13 @@ var rules = {
|
|
|
98
102
|
'no-jest-mock-barrel-files': noJestMockBarrelFiles,
|
|
99
103
|
'no-relative-barrel-file-imports': noRelativeBarrelFileImports,
|
|
100
104
|
'no-conversation-assistant-barrel-imports': noConversationAssistantBarrelImports,
|
|
105
|
+
'one-value-export-per-file': oneValueExportPerFile,
|
|
101
106
|
'visit-example-type-import-required': visitExampleTypeImportRequired,
|
|
102
107
|
'no-xcss-in-cx': noXcssInCx,
|
|
103
|
-
'
|
|
108
|
+
'editor-example-type-import-required': editorExampleTypeImportRequired,
|
|
109
|
+
'ensure-use-sync-external-store-server-snapshot': ensureUseSyncExternalStoreServerSnapshot,
|
|
110
|
+
'use-motion-token-values': useMotionTokenValues,
|
|
111
|
+
'expand-motion-shorthand': expandMotionShorthand
|
|
104
112
|
};
|
|
105
113
|
var commonConfig = {
|
|
106
114
|
'@atlaskit/platform/ensure-test-runner-arguments': 'error',
|
|
@@ -118,6 +126,8 @@ var commonConfig = {
|
|
|
118
126
|
'@atlaskit/platform/expand-background-shorthand': 'error',
|
|
119
127
|
'@atlaskit/platform/expand-spacing-shorthand': 'error',
|
|
120
128
|
'@atlaskit/platform/no-css-prop-in-object-spread': 'error',
|
|
129
|
+
'@atlaskit/platform/use-motion-token-values': 'warn',
|
|
130
|
+
'@atlaskit/platform/expand-motion-shorthand': 'warn',
|
|
121
131
|
'@compiled/jsx-pragma': ['error', {
|
|
122
132
|
importSources: ['@atlaskit/css'],
|
|
123
133
|
onlyRunIfImportingCompiled: true,
|