@atlaskit/eslint-plugin-platform 2.9.2 → 2.9.3
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 +8 -0
- package/dist/cjs/index.js +9 -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/es2019/index.js +9 -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/esm/index.js +9 -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/types/index.d.ts +4 -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-ts4.5/index.d.ts +4 -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/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;
|
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';
|
|
@@ -40,6 +42,7 @@ 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';
|
|
42
44
|
import visitExampleTypeImportRequired from './rules/visit-example-type-import-required';
|
|
45
|
+
import editorExampleTypeImportRequired from './rules/editor-example-type-import-required';
|
|
43
46
|
import ensureUseSyncExternalStoreServerSnapshot from './rules/ensure-use-sync-external-store-server-snapshot';
|
|
44
47
|
import noXcssInCx from './rules/no-xcss-in-cx';
|
|
45
48
|
import { join, normalize } from 'node:path';
|
|
@@ -100,7 +103,10 @@ var rules = {
|
|
|
100
103
|
'no-conversation-assistant-barrel-imports': noConversationAssistantBarrelImports,
|
|
101
104
|
'visit-example-type-import-required': visitExampleTypeImportRequired,
|
|
102
105
|
'no-xcss-in-cx': noXcssInCx,
|
|
103
|
-
'
|
|
106
|
+
'editor-example-type-import-required': editorExampleTypeImportRequired,
|
|
107
|
+
'ensure-use-sync-external-store-server-snapshot': ensureUseSyncExternalStoreServerSnapshot,
|
|
108
|
+
'use-motion-token-values': useMotionTokenValues,
|
|
109
|
+
'expand-motion-shorthand': expandMotionShorthand
|
|
104
110
|
};
|
|
105
111
|
var commonConfig = {
|
|
106
112
|
'@atlaskit/platform/ensure-test-runner-arguments': 'error',
|
|
@@ -118,6 +124,8 @@ var commonConfig = {
|
|
|
118
124
|
'@atlaskit/platform/expand-background-shorthand': 'error',
|
|
119
125
|
'@atlaskit/platform/expand-spacing-shorthand': 'error',
|
|
120
126
|
'@atlaskit/platform/no-css-prop-in-object-spread': 'error',
|
|
127
|
+
'@atlaskit/platform/use-motion-token-values': 'warn',
|
|
128
|
+
'@atlaskit/platform/expand-motion-shorthand': 'warn',
|
|
121
129
|
'@compiled/jsx-pragma': ['error', {
|
|
122
130
|
importSources: ['@atlaskit/css'],
|
|
123
131
|
onlyRunIfImportingCompiled: true,
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
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; } } }; }
|
|
2
|
+
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; } }
|
|
3
|
+
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; }
|
|
4
|
+
var EASING_KEYWORDS = ['ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear', 'step-start', 'step-end'];
|
|
5
|
+
var KEYWORD_VALUES = ['none', 'all', 'inherit', 'initial', 'unset'];
|
|
6
|
+
var isDuration = function isDuration(token) {
|
|
7
|
+
return /^\d+(?:\.\d+)?m?s$/.test(token);
|
|
8
|
+
};
|
|
9
|
+
var isEasing = function isEasing(token) {
|
|
10
|
+
return EASING_KEYWORDS.includes(token) || token.startsWith('cubic-bezier(') || token.startsWith('steps(');
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Tokenizes a CSS shorthand value string, respecting function boundaries.
|
|
14
|
+
* e.g. 'opacity 200ms cubic-bezier(0.4, 0, 0, 1) 0ms' →
|
|
15
|
+
* ['opacity', '200ms', 'cubic-bezier(0.4, 0, 0, 1)', '0ms']
|
|
16
|
+
* Splits on whitespace only when not inside parentheses.
|
|
17
|
+
*/
|
|
18
|
+
var tokenizeShorthand = function tokenizeShorthand(value) {
|
|
19
|
+
var tokens = [];
|
|
20
|
+
var depth = 0;
|
|
21
|
+
var current = '';
|
|
22
|
+
for (var i = 0; i < value.length; i++) {
|
|
23
|
+
var ch = value[i];
|
|
24
|
+
if (ch === '(') {
|
|
25
|
+
depth++;
|
|
26
|
+
current += ch;
|
|
27
|
+
} else if (ch === ')') {
|
|
28
|
+
depth--;
|
|
29
|
+
current += ch;
|
|
30
|
+
} else if (/\s/.test(ch) && depth === 0) {
|
|
31
|
+
if (current.length > 0) {
|
|
32
|
+
tokens.push(current);
|
|
33
|
+
current = '';
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
current += ch;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (current.length > 0) {
|
|
40
|
+
tokens.push(current);
|
|
41
|
+
}
|
|
42
|
+
return tokens;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Splits on top-level commas (outside function parens) — preserves cubic-bezier(...) commas.
|
|
46
|
+
var splitOnTopLevelCommas = function splitOnTopLevelCommas(value) {
|
|
47
|
+
var parts = [];
|
|
48
|
+
var depth = 0;
|
|
49
|
+
var current = '';
|
|
50
|
+
var _iterator = _createForOfIteratorHelper(value),
|
|
51
|
+
_step;
|
|
52
|
+
try {
|
|
53
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
54
|
+
var ch = _step.value;
|
|
55
|
+
if (ch === '(') {
|
|
56
|
+
depth++;
|
|
57
|
+
current += ch;
|
|
58
|
+
} else if (ch === ')') {
|
|
59
|
+
depth--;
|
|
60
|
+
current += ch;
|
|
61
|
+
} else if (ch === ',' && depth === 0) {
|
|
62
|
+
parts.push(current.trim());
|
|
63
|
+
current = '';
|
|
64
|
+
} else {
|
|
65
|
+
current += ch;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
_iterator.e(err);
|
|
70
|
+
} finally {
|
|
71
|
+
_iterator.f();
|
|
72
|
+
}
|
|
73
|
+
if (current.trim().length > 0) {
|
|
74
|
+
parts.push(current.trim());
|
|
75
|
+
}
|
|
76
|
+
return parts;
|
|
77
|
+
};
|
|
78
|
+
var parseTransition = function parseTransition(value) {
|
|
79
|
+
var parts = tokenizeShorthand(value.trim());
|
|
80
|
+
var result = {};
|
|
81
|
+
var durationCount = 0;
|
|
82
|
+
var _iterator2 = _createForOfIteratorHelper(parts),
|
|
83
|
+
_step2;
|
|
84
|
+
try {
|
|
85
|
+
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
|
|
86
|
+
var part = _step2.value;
|
|
87
|
+
if (isDuration(part)) {
|
|
88
|
+
if (durationCount === 0) {
|
|
89
|
+
result.transitionDuration = part;
|
|
90
|
+
} else {
|
|
91
|
+
result.transitionDelay = part;
|
|
92
|
+
}
|
|
93
|
+
durationCount++;
|
|
94
|
+
} else if (isEasing(part)) {
|
|
95
|
+
result.transitionTimingFunction = part;
|
|
96
|
+
} else {
|
|
97
|
+
result.transitionProperty = part;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
_iterator2.e(err);
|
|
102
|
+
} finally {
|
|
103
|
+
_iterator2.f();
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
};
|
|
107
|
+
var parseAnimation = function parseAnimation(value) {
|
|
108
|
+
var parts = tokenizeShorthand(value.trim());
|
|
109
|
+
var result = {};
|
|
110
|
+
var durationCount = 0;
|
|
111
|
+
var _iterator3 = _createForOfIteratorHelper(parts),
|
|
112
|
+
_step3;
|
|
113
|
+
try {
|
|
114
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
115
|
+
var part = _step3.value;
|
|
116
|
+
if (isDuration(part)) {
|
|
117
|
+
if (durationCount === 0) {
|
|
118
|
+
result.animationDuration = part;
|
|
119
|
+
} else {
|
|
120
|
+
result.animationDelay = part;
|
|
121
|
+
}
|
|
122
|
+
durationCount++;
|
|
123
|
+
} else if (isEasing(part)) {
|
|
124
|
+
result.animationTimingFunction = part;
|
|
125
|
+
} else if (part === 'infinite' || /^\d+(\.\d+)?$/.test(part)) {
|
|
126
|
+
result.animationIterationCount = part;
|
|
127
|
+
} else if (['normal', 'reverse', 'alternate', 'alternate-reverse'].includes(part)) {
|
|
128
|
+
result.animationDirection = part;
|
|
129
|
+
} else if (['none', 'forwards', 'backwards', 'both'].includes(part)) {
|
|
130
|
+
result.animationFillMode = part;
|
|
131
|
+
} else if (['running', 'paused'].includes(part)) {
|
|
132
|
+
result.animationPlayState = part;
|
|
133
|
+
} else {
|
|
134
|
+
result.animationName = part;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
_iterator3.e(err);
|
|
139
|
+
} finally {
|
|
140
|
+
_iterator3.f();
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Combine sub-property values across comma-separated transitions/animations.
|
|
146
|
+
// If no segment explicitly set this sub-property, omit it entirely.
|
|
147
|
+
// Otherwise, fill missing slots with the CSS spec default to preserve positional alignment.
|
|
148
|
+
var combineSubPropertyValues = function combineSubPropertyValues(segments, subProperty, defaultValue) {
|
|
149
|
+
if (segments.every(function (s) {
|
|
150
|
+
return s[subProperty] === undefined;
|
|
151
|
+
})) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
return segments.map(function (s) {
|
|
155
|
+
var _s$subProperty;
|
|
156
|
+
return (_s$subProperty = s[subProperty]) !== null && _s$subProperty !== void 0 ? _s$subProperty : defaultValue;
|
|
157
|
+
}).join(', ');
|
|
158
|
+
};
|
|
159
|
+
var buildTransitionFix = function buildTransitionFix(segments, indent) {
|
|
160
|
+
var lines = [];
|
|
161
|
+
var property = combineSubPropertyValues(segments, 'transitionProperty', 'all');
|
|
162
|
+
var duration = combineSubPropertyValues(segments, 'transitionDuration', '0s');
|
|
163
|
+
var timing = combineSubPropertyValues(segments, 'transitionTimingFunction', 'ease');
|
|
164
|
+
var delay = combineSubPropertyValues(segments, 'transitionDelay', '0s');
|
|
165
|
+
if (property !== undefined) lines.push("transitionProperty: '".concat(property, "'"));
|
|
166
|
+
if (duration !== undefined) lines.push("transitionDuration: '".concat(duration, "'"));
|
|
167
|
+
if (timing !== undefined) lines.push("transitionTimingFunction: '".concat(timing, "'"));
|
|
168
|
+
if (delay !== undefined) lines.push("transitionDelay: '".concat(delay, "'"));
|
|
169
|
+
return lines.join(",\n".concat(indent));
|
|
170
|
+
};
|
|
171
|
+
var buildAnimationFix = function buildAnimationFix(segments, indent) {
|
|
172
|
+
var lines = [];
|
|
173
|
+
var name = combineSubPropertyValues(segments, 'animationName', 'none');
|
|
174
|
+
var duration = combineSubPropertyValues(segments, 'animationDuration', '0s');
|
|
175
|
+
var timing = combineSubPropertyValues(segments, 'animationTimingFunction', 'ease');
|
|
176
|
+
var delay = combineSubPropertyValues(segments, 'animationDelay', '0s');
|
|
177
|
+
var iter = combineSubPropertyValues(segments, 'animationIterationCount', '1');
|
|
178
|
+
var direction = combineSubPropertyValues(segments, 'animationDirection', 'normal');
|
|
179
|
+
var fill = combineSubPropertyValues(segments, 'animationFillMode', 'none');
|
|
180
|
+
var playState = combineSubPropertyValues(segments, 'animationPlayState', 'running');
|
|
181
|
+
if (name !== undefined) lines.push("animationName: '".concat(name, "'"));
|
|
182
|
+
if (duration !== undefined) lines.push("animationDuration: '".concat(duration, "'"));
|
|
183
|
+
if (timing !== undefined) lines.push("animationTimingFunction: '".concat(timing, "'"));
|
|
184
|
+
if (delay !== undefined) lines.push("animationDelay: '".concat(delay, "'"));
|
|
185
|
+
if (iter !== undefined) lines.push("animationIterationCount: '".concat(iter, "'"));
|
|
186
|
+
if (direction !== undefined) lines.push("animationDirection: '".concat(direction, "'"));
|
|
187
|
+
if (fill !== undefined) lines.push("animationFillMode: '".concat(fill, "'"));
|
|
188
|
+
if (playState !== undefined) lines.push("animationPlayState: '".concat(playState, "'"));
|
|
189
|
+
return lines.join(",\n".concat(indent));
|
|
190
|
+
};
|
|
191
|
+
var TRANSITION_SUB_PROPERTIES = ['transitionProperty', 'transitionDuration', 'transitionTimingFunction', 'transitionDelay'];
|
|
192
|
+
var ANIMATION_SUB_PROPERTIES = ['animationName', 'animationDuration', 'animationTimingFunction', 'animationDelay', 'animationIterationCount', 'animationDirection', 'animationFillMode', 'animationPlayState'];
|
|
193
|
+
var executeExpandTransitionRule = function executeExpandTransitionRule(context, node, property) {
|
|
194
|
+
var _context$sourceCode, _node$loc;
|
|
195
|
+
if (node.value.type === 'CallExpression') {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (node.value.type === 'TemplateLiteral') {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (node.value.type !== 'Literal' || typeof node.value.value !== 'string') {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
var rawValue = node.value.value;
|
|
205
|
+
if (KEYWORD_VALUES.includes(rawValue)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
var subProperties = property === 'transition' ? TRANSITION_SUB_PROPERTIES : ANIMATION_SUB_PROPERTIES;
|
|
209
|
+
|
|
210
|
+
// Extract leading whitespace to preserve indentation style (tabs vs spaces)
|
|
211
|
+
var sourceCode = (_context$sourceCode = context.sourceCode) !== null && _context$sourceCode !== void 0 ? _context$sourceCode : context.getSourceCode();
|
|
212
|
+
var nodeStart = (_node$loc = node.loc) === null || _node$loc === void 0 ? void 0 : _node$loc.start;
|
|
213
|
+
var indent = '\t';
|
|
214
|
+
if (nodeStart) {
|
|
215
|
+
var _sourceCode$lines;
|
|
216
|
+
var lineText = (_sourceCode$lines = sourceCode.lines[nodeStart.line - 1]) !== null && _sourceCode$lines !== void 0 ? _sourceCode$lines : '';
|
|
217
|
+
var leadingWhitespace = lineText.match(/^(\s*)/);
|
|
218
|
+
if (leadingWhitespace) {
|
|
219
|
+
indent = leadingWhitespace[1];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
var segmentStrings = splitOnTopLevelCommas(rawValue);
|
|
223
|
+
if (property === 'transition') {
|
|
224
|
+
var segments = segmentStrings.map(parseTransition);
|
|
225
|
+
var fixText = buildTransitionFix(segments, indent);
|
|
226
|
+
context.report({
|
|
227
|
+
node: node,
|
|
228
|
+
messageId: 'expandTransitionShorthand',
|
|
229
|
+
data: {
|
|
230
|
+
property: property,
|
|
231
|
+
subProperties: subProperties.join(', ')
|
|
232
|
+
},
|
|
233
|
+
fix: function fix(fixer) {
|
|
234
|
+
return fixer.replaceText(node, fixText);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
} else {
|
|
238
|
+
var _segments = segmentStrings.map(parseAnimation);
|
|
239
|
+
var _fixText = buildAnimationFix(_segments, indent);
|
|
240
|
+
context.report({
|
|
241
|
+
node: node,
|
|
242
|
+
messageId: 'expandTransitionShorthand',
|
|
243
|
+
data: {
|
|
244
|
+
property: property,
|
|
245
|
+
subProperties: subProperties.join(', ')
|
|
246
|
+
},
|
|
247
|
+
fix: function fix(fixer) {
|
|
248
|
+
return fixer.replaceText(node, _fixText);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
export var expandTransitionShorthand = {
|
|
254
|
+
meta: {
|
|
255
|
+
type: 'suggestion',
|
|
256
|
+
fixable: 'code',
|
|
257
|
+
docs: {
|
|
258
|
+
url: 'https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/platform/eslint-plugin/src/rules/compiled/expand-transition-shorthand/'
|
|
259
|
+
},
|
|
260
|
+
messages: {
|
|
261
|
+
expandTransitionShorthand: "Use {{ subProperties }} instead of the '{{ property }}' shorthand so that individual values can be replaced with motion tokens."
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
create: function create(context) {
|
|
265
|
+
return {
|
|
266
|
+
'Property[key.name="transition"]': function PropertyKeyNameTransition(node) {
|
|
267
|
+
executeExpandTransitionRule(context, node, 'transition');
|
|
268
|
+
},
|
|
269
|
+
'Property[key.name="animation"]': function PropertyKeyNameAnimation(node) {
|
|
270
|
+
executeExpandTransitionRule(context, node, 'animation');
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
export default expandTransitionShorthand;
|