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