@atlaskit/eslint-plugin-platform 2.9.1 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cjs/index.js +15 -3
  3. package/dist/cjs/rules/compiled/expand-motion-shorthand/index.js +281 -0
  4. package/dist/cjs/rules/compiled/no-css-prop-in-object-spread/index.js +162 -0
  5. package/dist/cjs/rules/compiled/use-motion-token-values/index.js +506 -0
  6. package/dist/cjs/rules/editor-example-type-import-required/index.js +321 -0
  7. package/dist/cjs/rules/no-xcss-in-cx/index.js +221 -0
  8. package/dist/cjs/rules/visit-example-type-import-required/index.js +23 -13
  9. package/dist/es2019/index.js +15 -3
  10. package/dist/es2019/rules/compiled/expand-motion-shorthand/index.js +239 -0
  11. package/dist/es2019/rules/compiled/no-css-prop-in-object-spread/index.js +136 -0
  12. package/dist/es2019/rules/compiled/use-motion-token-values/index.js +444 -0
  13. package/dist/es2019/rules/editor-example-type-import-required/index.js +286 -0
  14. package/dist/es2019/rules/no-xcss-in-cx/index.js +187 -0
  15. package/dist/es2019/rules/visit-example-type-import-required/index.js +23 -14
  16. package/dist/esm/index.js +15 -3
  17. package/dist/esm/rules/compiled/expand-motion-shorthand/index.js +275 -0
  18. package/dist/esm/rules/compiled/no-css-prop-in-object-spread/index.js +156 -0
  19. package/dist/esm/rules/compiled/use-motion-token-values/index.js +499 -0
  20. package/dist/esm/rules/editor-example-type-import-required/index.js +314 -0
  21. package/dist/esm/rules/no-xcss-in-cx/index.js +216 -0
  22. package/dist/esm/rules/visit-example-type-import-required/index.js +23 -13
  23. package/dist/types/index.d.ts +282 -243
  24. package/dist/types/rules/compiled/expand-motion-shorthand/index.d.ts +3 -0
  25. package/dist/types/rules/compiled/no-css-prop-in-object-spread/index.d.ts +3 -0
  26. package/dist/types/rules/compiled/use-motion-token-values/index.d.ts +3 -0
  27. package/dist/types/rules/editor-example-type-import-required/index.d.ts +4 -0
  28. package/dist/types/rules/no-xcss-in-cx/index.d.ts +31 -0
  29. package/dist/types-ts4.5/index.d.ts +226 -211
  30. package/dist/types-ts4.5/rules/compiled/expand-motion-shorthand/index.d.ts +3 -0
  31. package/dist/types-ts4.5/rules/compiled/no-css-prop-in-object-spread/index.d.ts +3 -0
  32. package/dist/types-ts4.5/rules/compiled/use-motion-token-values/index.d.ts +3 -0
  33. package/dist/types-ts4.5/rules/editor-example-type-import-required/index.d.ts +4 -0
  34. package/dist/types-ts4.5/rules/no-xcss-in-cx/index.d.ts +31 -0
  35. package/package.json +2 -1
@@ -0,0 +1,444 @@
1
+ import tokenDefaultValues from '@atlaskit/tokens/token-default-values';
2
+ const DURATION_TOKEN_NAMES = ['motion.duration.instant', 'motion.duration.xxshort', 'motion.duration.xshort', 'motion.duration.short', 'motion.duration.medium', 'motion.duration.long', 'motion.duration.xlong', 'motion.duration.xxlong'];
3
+ function parseDurationMs(value) {
4
+ const ms = value.match(/^(\d+(?:\.\d+)?)ms$/);
5
+ if (ms) {
6
+ return parseFloat(ms[1]);
7
+ }
8
+ const s = value.match(/^(\d+(?:\.\d+)?)s$/);
9
+ if (s) {
10
+ return parseFloat(s[1]) * 1000;
11
+ }
12
+ return null;
13
+ }
14
+ const DURATION_TOKENS = DURATION_TOKEN_NAMES.map(name => {
15
+ const rawValue = tokenDefaultValues[name];
16
+ const ms = parseDurationMs(rawValue);
17
+ if (ms === null) {
18
+ throw new Error(`use-motion-token-values: could not parse duration for token ${name}: ${rawValue}`);
19
+ }
20
+ return {
21
+ ms,
22
+ token: name
23
+ };
24
+ }).sort((a, b) => a.ms - b.ms);
25
+ const EASING_TOKEN_NAMES = ['motion.easing.in.practical', 'motion.easing.inout.bold', 'motion.easing.out.practical', 'motion.easing.out.bold'];
26
+ function parseCubicBezierParams(value) {
27
+ const match = value.match(/^cubic-bezier\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/);
28
+ if (!match) {
29
+ return null;
30
+ }
31
+ return [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]), parseFloat(match[4])];
32
+ }
33
+ const EASING_TOKENS = EASING_TOKEN_NAMES.map(name => {
34
+ const rawValue = tokenDefaultValues[name];
35
+ const params = parseCubicBezierParams(rawValue);
36
+ if (!params) {
37
+ throw new Error(`use-motion-token-values: could not parse cubic-bezier for token ${name}: ${rawValue}`);
38
+ }
39
+ return {
40
+ value: rawValue,
41
+ token: name,
42
+ params
43
+ };
44
+ });
45
+
46
+ // Splits on top-level commas (outside function parens) — preserves cubic-bezier(...) commas.
47
+ function splitOnTopLevelCommas(value) {
48
+ const parts = [];
49
+ let depth = 0;
50
+ let current = '';
51
+ for (const ch of value) {
52
+ if (ch === '(') {
53
+ depth++;
54
+ current += ch;
55
+ } else if (ch === ')') {
56
+ depth--;
57
+ current += ch;
58
+ } else if (ch === ',' && depth === 0) {
59
+ parts.push(current.trim());
60
+ current = '';
61
+ } else {
62
+ current += ch;
63
+ }
64
+ }
65
+ if (current.trim().length > 0) {
66
+ parts.push(current.trim());
67
+ }
68
+ return parts;
69
+ }
70
+ const DURATION_PROPERTIES = new Set(['transitionDuration', 'animationDuration']);
71
+ const EASING_PROPERTIES = new Set(['transitionTimingFunction', 'animationTimingFunction']);
72
+
73
+ // Explicit semantic mappings for CSS keyword easings to motion tokens.
74
+ // Pinned by design intent, confirmed with design system team (Alex + Akshay).
75
+ const CSS_KEYWORD_EASING_TOKEN_MAP = {
76
+ ease: 'motion.easing.out.practical',
77
+ 'ease-out': 'motion.easing.out.practical',
78
+ 'ease-in': 'motion.easing.in.practical',
79
+ 'ease-in-out': 'motion.easing.inout.bold'
80
+ // linear (0,0,1,1) — warn only, no autofix (per Akshay: too generic, no good token match)
81
+ };
82
+
83
+ // Non-curve easing values with no meaningful cubic-bezier representation — skip entirely
84
+ const SKIP_EASING_VALUES = new Set(['step-start', 'step-end', 'inherit', 'initial', 'unset', 'none']);
85
+ function euclideanDistance(a, b) {
86
+ return Math.sqrt(a.reduce((sum, val, i) => sum + Math.pow(val - b[i], 2), 0));
87
+ }
88
+
89
+ // Maximum Euclidean distance for easing autofix — beyond this threshold, we report-only
90
+ const EASING_AUTOFIX_THRESHOLD = 0.5;
91
+ function findClosestEasingToken(params) {
92
+ let minDist = Infinity;
93
+ let closest = EASING_TOKENS[0];
94
+ for (const entry of EASING_TOKENS) {
95
+ const dist = euclideanDistance(params, entry.params);
96
+ if (dist < minDist) {
97
+ minDist = dist;
98
+ closest = entry;
99
+ }
100
+ }
101
+ if (minDist > EASING_AUTOFIX_THRESHOLD) {
102
+ return null;
103
+ }
104
+ return {
105
+ token: closest.token,
106
+ value: closest.value,
107
+ dist: minDist
108
+ };
109
+ }
110
+ function findClosestDurationTokens(ms) {
111
+ const exact = DURATION_TOKENS.find(t => t.ms === ms);
112
+ if (exact) {
113
+ return [exact];
114
+ }
115
+ let minDist = Infinity;
116
+ for (const entry of DURATION_TOKENS) {
117
+ const dist = Math.abs(entry.ms - ms);
118
+ if (dist < minDist) {
119
+ minDist = dist;
120
+ }
121
+ }
122
+ const closest = DURATION_TOKENS.filter(t => Math.abs(t.ms - ms) === minDist);
123
+ return closest;
124
+ }
125
+ export const useMotionTokenValues = {
126
+ meta: {
127
+ type: 'suggestion',
128
+ fixable: 'code',
129
+ docs: {
130
+ url: 'https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/platform/eslint-plugin/src/rules/compiled/use-motion-token-values/'
131
+ },
132
+ messages: {
133
+ useMotionDurationToken: "Use a motion duration token instead of the hard-coded value '{{ value }}'. Replace with {{ suggestion }}.",
134
+ useMotionDurationTokenNearest: "No exact token match for '{{ value }}'. Nearest: {{ suggestion1 }} or {{ suggestion2 }}.",
135
+ useMotionEasingToken: "Use a motion easing token instead of the hard-coded value '{{ value }}'. Replace with {{ suggestion }}.",
136
+ useMotionEasingTokenUnknown: "Use a motion easing token from @atlaskit/tokens instead of the hard-coded value '{{ value }}'."
137
+ }
138
+ },
139
+ create(context) {
140
+ let tokensImportNode = null;
141
+ let hasTokenSpecifier = false;
142
+ function buildTokenCall(tokenName, fallback) {
143
+ return `token('${tokenName}', '${fallback}')`;
144
+ }
145
+ function getImportFix(fixer) {
146
+ var _context$sourceCode;
147
+ if (hasTokenSpecifier) {
148
+ return [];
149
+ }
150
+ if (tokensImportNode) {
151
+ // @atlaskit/tokens is imported but without `token` — add `token` to existing import
152
+ const lastSpecifier = tokensImportNode.specifiers[tokensImportNode.specifiers.length - 1];
153
+ if (lastSpecifier) {
154
+ return [fixer.insertTextAfter(lastSpecifier, ', token')];
155
+ }
156
+ // Empty import — replace the whole declaration
157
+ return [fixer.replaceText(tokensImportNode, `import { token } from '@atlaskit/tokens';`)];
158
+ }
159
+ const sourceCode = (_context$sourceCode = context.sourceCode) !== null && _context$sourceCode !== void 0 ? _context$sourceCode : context.getSourceCode();
160
+ const programBody = sourceCode.ast.body;
161
+ // Insert after the last existing import, or at top if no imports exist
162
+ const lastImport = [...programBody].reverse().find(n => n.type === 'ImportDeclaration');
163
+ if (lastImport) {
164
+ return [fixer.insertTextAfter(lastImport, `\nimport { token } from '@atlaskit/tokens';`)];
165
+ }
166
+ if (programBody.length > 0) {
167
+ return [fixer.insertTextBefore(programBody[0], `import { token } from '@atlaskit/tokens';\n`)];
168
+ }
169
+ return [];
170
+ }
171
+
172
+ // Returns autofix string for a single duration value, or null if ambiguous (equidistant)
173
+ function resolveDurationToken(value) {
174
+ const ms = parseDurationMs(value);
175
+ if (ms === null) {
176
+ return null;
177
+ }
178
+ const result = findClosestDurationTokens(ms);
179
+ if (result.length === 1) {
180
+ return buildTokenCall(result[0].token, value);
181
+ }
182
+ return null;
183
+ }
184
+ function handleDurationProperty(node, rawValue) {
185
+ const segments = splitOnTopLevelCommas(rawValue);
186
+
187
+ // Single value path keeps the existing equidistant message
188
+ if (segments.length === 1) {
189
+ const ms = parseDurationMs(rawValue);
190
+ if (ms === null) {
191
+ return;
192
+ }
193
+ const result = findClosestDurationTokens(ms);
194
+ if (result.length === 1) {
195
+ const suggestion = buildTokenCall(result[0].token, rawValue);
196
+ context.report({
197
+ node,
198
+ messageId: 'useMotionDurationToken',
199
+ data: {
200
+ value: rawValue,
201
+ suggestion
202
+ },
203
+ fix(fixer) {
204
+ return [...getImportFix(fixer), fixer.replaceText(node.value, suggestion)];
205
+ }
206
+ });
207
+ } else {
208
+ const suggestion1 = buildTokenCall(result[0].token, rawValue);
209
+ const suggestion2 = buildTokenCall(result[1].token, rawValue);
210
+ context.report({
211
+ node,
212
+ messageId: 'useMotionDurationTokenNearest',
213
+ data: {
214
+ value: rawValue,
215
+ suggestion1: `${suggestion1} (${result[0].ms}ms)`,
216
+ suggestion2: `${suggestion2} (${result[1].ms}ms)`
217
+ }
218
+ });
219
+ }
220
+ return;
221
+ }
222
+
223
+ // Multi-value path: every segment must resolve to a single token for autofix
224
+ const resolved = segments.map(resolveDurationToken);
225
+ if (resolved.some(s => s === null)) {
226
+ return;
227
+ }
228
+ // Build a template literal: `${token(...)}, ${token(...)}`
229
+ const templateLiteral = '`' + resolved.map(s => `\${${s}}`).join(', ') + '`';
230
+ context.report({
231
+ node,
232
+ messageId: 'useMotionDurationToken',
233
+ data: {
234
+ value: rawValue,
235
+ suggestion: templateLiteral
236
+ },
237
+ fix(fixer) {
238
+ return [...getImportFix(fixer), fixer.replaceText(node.value, templateLiteral)];
239
+ }
240
+ });
241
+ }
242
+
243
+ // Returns autofix string for a single easing value, or null if no token suggestion is possible
244
+ function resolveEasingToken(value) {
245
+ const trimmed = value.trim();
246
+ if (SKIP_EASING_VALUES.has(trimmed)) {
247
+ return null;
248
+ }
249
+ if (trimmed in CSS_KEYWORD_EASING_TOKEN_MAP) {
250
+ return buildTokenCall(CSS_KEYWORD_EASING_TOKEN_MAP[trimmed], trimmed);
251
+ }
252
+ // linear has no curve (0,0,1,1) — warn only, no autofix
253
+ if (trimmed === 'linear') {
254
+ return null;
255
+ }
256
+ if (trimmed.startsWith('linear(')) {
257
+ // linear() is used for spring animations — motion.easing.spring is experimental, skip
258
+ return null;
259
+ }
260
+ const params = parseCubicBezierParams(trimmed);
261
+ if (!params) {
262
+ return null;
263
+ }
264
+ const exact = EASING_TOKENS.find(t => t.value === trimmed);
265
+ if (exact) {
266
+ return buildTokenCall(exact.token, trimmed);
267
+ }
268
+ const closest = findClosestEasingToken(params);
269
+ return closest ? buildTokenCall(closest.token, trimmed) : null;
270
+ }
271
+ function handleEasingProperty(node, rawValue) {
272
+ const segments = splitOnTopLevelCommas(rawValue);
273
+
274
+ // Multi-value path: resolve each segment, autofix only if all resolve cleanly
275
+ if (segments.length > 1) {
276
+ const resolved = segments.map(resolveEasingToken);
277
+ if (resolved.some(s => s === null)) {
278
+ return;
279
+ }
280
+ const templateLiteral = '`' + resolved.map(s => `\${${s}}`).join(', ') + '`';
281
+ context.report({
282
+ node,
283
+ messageId: 'useMotionEasingToken',
284
+ data: {
285
+ value: rawValue,
286
+ suggestion: templateLiteral
287
+ },
288
+ fix(fixer) {
289
+ return [...getImportFix(fixer), fixer.replaceText(node.value, templateLiteral)];
290
+ }
291
+ });
292
+ return;
293
+ }
294
+ const trimmed = rawValue.trim();
295
+ if (SKIP_EASING_VALUES.has(trimmed)) {
296
+ return;
297
+ }
298
+
299
+ // CSS keyword easings: convert to cubic-bezier equivalent and find closest token
300
+ if (trimmed in CSS_KEYWORD_EASING_TOKEN_MAP) {
301
+ const suggestion = buildTokenCall(CSS_KEYWORD_EASING_TOKEN_MAP[trimmed], trimmed);
302
+ context.report({
303
+ node,
304
+ messageId: 'useMotionEasingToken',
305
+ data: {
306
+ value: trimmed,
307
+ suggestion
308
+ },
309
+ fix(fixer) {
310
+ return [...getImportFix(fixer), fixer.replaceText(node.value, suggestion)];
311
+ }
312
+ });
313
+ return;
314
+ }
315
+ // linear has no curve (0,0,1,1) — warn only, no autofix
316
+ if (trimmed === 'linear') {
317
+ context.report({
318
+ node,
319
+ messageId: 'useMotionEasingTokenUnknown',
320
+ data: {
321
+ value: trimmed
322
+ }
323
+ });
324
+ return;
325
+ }
326
+ if (trimmed.startsWith('linear(')) {
327
+ // linear() is used for spring animations — motion.easing.spring is experimental, skip
328
+ return;
329
+ }
330
+ const params = parseCubicBezierParams(trimmed);
331
+ if (!params) {
332
+ context.report({
333
+ node,
334
+ messageId: 'useMotionEasingTokenUnknown',
335
+ data: {
336
+ value: rawValue
337
+ }
338
+ });
339
+ return;
340
+ }
341
+ const exact = EASING_TOKENS.find(t => t.value === trimmed);
342
+ if (exact) {
343
+ const suggestion = buildTokenCall(exact.token, rawValue);
344
+ context.report({
345
+ node,
346
+ messageId: 'useMotionEasingToken',
347
+ data: {
348
+ value: rawValue,
349
+ suggestion
350
+ },
351
+ fix(fixer) {
352
+ return [...getImportFix(fixer), fixer.replaceText(node.value, suggestion)];
353
+ }
354
+ });
355
+ return;
356
+ }
357
+ const closest = findClosestEasingToken(params);
358
+ if (closest) {
359
+ const suggestion = buildTokenCall(closest.token, rawValue);
360
+ context.report({
361
+ node,
362
+ messageId: 'useMotionEasingToken',
363
+ data: {
364
+ value: rawValue,
365
+ suggestion
366
+ },
367
+ fix(fixer) {
368
+ return [...getImportFix(fixer), fixer.replaceText(node.value, suggestion)];
369
+ }
370
+ });
371
+ } else {
372
+ context.report({
373
+ node,
374
+ messageId: 'useMotionEasingTokenUnknown',
375
+ data: {
376
+ value: rawValue
377
+ }
378
+ });
379
+ }
380
+ }
381
+ function handleProperty(node) {
382
+ const key = node.key;
383
+ if (key.type !== 'Identifier') {
384
+ return;
385
+ }
386
+ const isDuration = DURATION_PROPERTIES.has(key.name);
387
+ const isEasing = EASING_PROPERTIES.has(key.name);
388
+ if (!isDuration && !isEasing) {
389
+ return;
390
+ }
391
+ const value = node.value;
392
+ if (value.type === 'TemplateLiteral') {
393
+ // Only handle no-interpolation template literals (e.g. `200ms`) — treat as string
394
+ const tl = value;
395
+ if (tl.expressions.length === 0 && tl.quasis.length === 1) {
396
+ var _tl$quasis$0$value$co;
397
+ const rawValue = (_tl$quasis$0$value$co = tl.quasis[0].value.cooked) !== null && _tl$quasis$0$value$co !== void 0 ? _tl$quasis$0$value$co : tl.quasis[0].value.raw;
398
+ if (isDuration) {
399
+ handleDurationProperty(node, rawValue);
400
+ } else {
401
+ handleEasingProperty(node, rawValue);
402
+ }
403
+ }
404
+ return;
405
+ }
406
+ if (value.type === 'CallExpression') {
407
+ const ce = value;
408
+ if (ce.callee.type === 'Identifier' && ce.callee.name === 'token') {
409
+ return;
410
+ }
411
+ return;
412
+ }
413
+ if (value.type === 'Literal') {
414
+ const lit = value;
415
+ let rawValue;
416
+ if (typeof lit.value === 'string') {
417
+ rawValue = lit.value;
418
+ } else if (typeof lit.value === 'number') {
419
+ // Treat bare numbers as ms
420
+ rawValue = `${lit.value}ms`;
421
+ } else {
422
+ return;
423
+ }
424
+ if (isDuration) {
425
+ handleDurationProperty(node, rawValue);
426
+ } else {
427
+ handleEasingProperty(node, rawValue);
428
+ }
429
+ }
430
+ }
431
+ return {
432
+ ImportDeclaration(node) {
433
+ if (node.source.value === '@atlaskit/tokens') {
434
+ tokensImportNode = node;
435
+ hasTokenSpecifier = node.specifiers.some(s => s.type === 'ImportSpecifier' && s.local.name === 'token');
436
+ }
437
+ },
438
+ Property(node) {
439
+ handleProperty(node);
440
+ }
441
+ };
442
+ }
443
+ };
444
+ export default useMotionTokenValues;