@atlaskit/eslint-plugin-design-system 4.16.1 → 4.16.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @atlaskit/eslint-plugin-design-system
2
2
 
3
+ ## 4.16.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`ed34264c827`](https://bitbucket.org/atlassian/atlassian-frontend/commits/ed34264c827) - Fix errors on tagged template literals for eslint rule ensure-design-token-usage-spacing and handle edgecases ensuring seamless fallback on errors
8
+
9
+ ## 4.16.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [`3db6efeac0d`](https://bitbucket.org/atlassian/atlassian-frontend/commits/3db6efeac0d) - Improves internal configuration of spacing tokens rule.
14
+
3
15
  ## 4.16.1
4
16
 
5
17
  ### Patch Changes
@@ -5,34 +5,67 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.default = void 0;
8
- var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
8
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
10
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
10
11
  var _eslintCodemodUtils = require("eslint-codemod-utils");
12
+ var _createRule = require("../utils/create-rule");
11
13
  var _isNode = require("../utils/is-node");
12
14
  var _utils = require("./utils");
13
15
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
14
16
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
15
- var rule = {
17
+ var rule = (0, _createRule.createRule)({
18
+ defaultOptions: [{
19
+ addons: ['spacing'],
20
+ applyImport: true
21
+ }],
22
+ name: 'ensure-design-token-usage-spacing',
16
23
  meta: {
24
+ schema: {
25
+ type: 'array',
26
+ items: {
27
+ type: 'object',
28
+ properties: {
29
+ applyImport: {
30
+ type: 'boolean'
31
+ },
32
+ addons: {
33
+ type: 'array',
34
+ items: {
35
+ enum: ['spacing', 'typography', 'shape']
36
+ }
37
+ }
38
+ }
39
+ }
40
+ },
17
41
  type: 'problem',
18
42
  fixable: 'code',
19
43
  docs: {
20
44
  description: 'Rule ensures all spacing CSS properties apply a matching spacing token',
21
- recommended: true
45
+ recommended: 'error'
22
46
  },
23
47
  messages: {
24
48
  noRawSpacingValues: 'The use of spacing primitives or tokens is preferred over the direct application of spacing properties.\n\n@meta <<{{payload}}>>',
25
49
  autofixesPossible: 'Automated corrections available for spacing values. Apply autofix to replace values with appropriate tokens'
26
50
  }
27
51
  },
28
- create: function create(context) {
29
- var _context$options$;
30
- var targetCategories = ['spacing'];
31
- var configCategories = (_context$options$ = context.options[0]) === null || _context$options$ === void 0 ? void 0 : _context$options$.addons;
32
- if (Array.isArray(configCategories) && configCategories.includes('typography')) {
33
- targetCategories.push('typography');
52
+ // @ts-expect-error
53
+ create: function create(context, options) {
54
+ var tokenNode = null;
55
+
56
+ // merge configs
57
+ var ruleConfig = _objectSpread(_objectSpread({}, options[0]), {}, {
58
+ addons: (0, _toConsumableArray2.default)(options[0].addons)
59
+ });
60
+ if (!ruleConfig.addons.includes('spacing')) {
61
+ ruleConfig.addons.push('spacing');
34
62
  }
35
63
  return {
64
+ ImportDeclaration: function ImportDeclaration(node) {
65
+ if (node.source.value === '@atlaskit/tokens' && ruleConfig.applyImport) {
66
+ tokenNode = node;
67
+ }
68
+ },
36
69
  // CSSObjectExpression
37
70
  // const styles = css({ color: 'red', margin: '4px' }), styled.div({ color: 'red', margin: '4px' })
38
71
  'CallExpression[callee.name=css] > ObjectExpression, CallExpression[callee.object.name=styled] > ObjectExpression': function CallExpressionCalleeNameCssObjectExpressionCallExpressionCalleeObjectNameStyledObjectExpression(parentNode) {
@@ -66,7 +99,7 @@ var rule = {
66
99
  if (!(0, _eslintCodemodUtils.isNodeOfType)(node.key, 'Identifier')) {
67
100
  return;
68
101
  }
69
- if (!(0, _utils.shouldAnalyzeProperty)(node.key.name, targetCategories)) {
102
+ if (!(0, _utils.shouldAnalyzeProperty)(node.key.name, ruleConfig.addons)) {
70
103
  return;
71
104
  }
72
105
  if ((0, _isNode.isDecendantOfGlobalToken)(node.value)) {
@@ -115,7 +148,7 @@ var rule = {
115
148
  },
116
149
  fix: function fix(fixer) {
117
150
  var _node$loc;
118
- if (!(0, _utils.shouldAnalyzeProperty)(propertyName, targetCategories)) {
151
+ if (!(0, _utils.shouldAnalyzeProperty)(propertyName, ruleConfig.addons)) {
119
152
  return null;
120
153
  }
121
154
  var pixelValueString = "".concat(pixelValue, "px");
@@ -125,9 +158,9 @@ var rule = {
125
158
  return null;
126
159
  }
127
160
  var replacementValue = (0, _utils.getTokenNodeForValue)(propertyName, lookupValue);
128
- return [fixer.insertTextBefore(node, "// TODO Delete this comment after verifying spacing token -> previous value `".concat((0, _eslintCodemodUtils.node)(node.value), "`\n").concat(' '.padStart(((_node$loc = node.loc) === null || _node$loc === void 0 ? void 0 : _node$loc.start.column) || 0))), fixer.replaceText(node, (0, _eslintCodemodUtils.property)(_objectSpread(_objectSpread({}, node), {}, {
161
+ return (!tokenNode && ruleConfig.applyImport ? [(0, _utils.insertTokensImport)(fixer)] : []).concat([fixer.insertTextBefore(node, "// TODO Delete this comment after verifying spacing token -> previous value `".concat((0, _eslintCodemodUtils.node)(node.value), "`\n").concat(' '.padStart(((_node$loc = node.loc) === null || _node$loc === void 0 ? void 0 : _node$loc.start.column) || 0))), fixer.replaceText(node, (0, _eslintCodemodUtils.property)(_objectSpread(_objectSpread({}, node), {}, {
129
162
  value: replacementValue
130
- })).toString())];
163
+ })).toString())]);
131
164
  }
132
165
  });
133
166
  }
@@ -154,11 +187,11 @@ var rule = {
154
187
  if (!allResolvableValues) {
155
188
  return null;
156
189
  }
157
- return fixer.replaceText(node.value, "`".concat(values.map(function (value) {
190
+ return (!tokenNode && ruleConfig.applyImport ? [(0, _utils.insertTokensImport)(fixer)] : []).concat([fixer.replaceText(node.value, "`".concat(values.map(function (value) {
158
191
  var pixelValue = (0, _utils.emToPixels)(value, fontSize);
159
192
  var pixelValueString = "".concat(pixelValue, "px");
160
193
  return "${".concat((0, _utils.getTokenNodeForValue)(propertyName, pixelValueString), "}");
161
- }).join(' '), "`"));
194
+ }).join(' '), "`"))]);
162
195
  } : undefined
163
196
  });
164
197
  });
@@ -172,8 +205,12 @@ var rule = {
172
205
  if (node.type !== 'TaggedTemplateExpression') {
173
206
  return;
174
207
  }
175
- var parentNode = (0, _utils.findParentNodeForLine)(node);
176
208
  var processedCssLines = (0, _utils.processCssNode)(node, context);
209
+ if (!processedCssLines) {
210
+ // if we can't get a processed css we bail
211
+ return;
212
+ }
213
+ var parentNode = (0, _utils.findParentNodeForLine)(node);
177
214
  var globalFontSize = (0, _utils.getFontSizeValueInScope)(processedCssLines);
178
215
  var textForSource = context.getSourceCode().getText(node.quasi);
179
216
  var allReplacedValues = [];
@@ -192,7 +229,7 @@ var rule = {
192
229
  var propertyName = (0, _utils.convertHyphenatedNameToCamelCase)(originalProperty);
193
230
  var isFontFamily = /fontFamily/.test(propertyName);
194
231
  var replacedValuesPerProperty = [originalProperty];
195
- if (!(0, _utils.shouldAnalyzeProperty)(propertyName, targetCategories) || !resolvedCssValues || !(0, _utils.isValidSpacingValue)(resolvedCssValues, globalFontSize)) {
232
+ if (!(0, _utils.shouldAnalyzeProperty)(propertyName, ruleConfig.addons) || !resolvedCssValues || !(0, _utils.isValidSpacingValue)(resolvedCssValues, globalFontSize)) {
196
233
  // in all of these cases no changes should be made to the current property
197
234
  return currentSource;
198
235
  }
@@ -278,13 +315,13 @@ var rule = {
278
315
  node: node,
279
316
  messageId: 'autofixesPossible',
280
317
  fix: function fix(fixer) {
281
- return [fixer.insertTextBefore(parentNode, replacementComments), fixer.replaceText(node.quasi, completeSource)];
318
+ return (!tokenNode && ruleConfig.applyImport ? [(0, _utils.insertTokensImport)(fixer)] : []).concat([fixer.insertTextBefore(parentNode, replacementComments), fixer.replaceText(node.quasi, completeSource)]);
282
319
  }
283
320
  });
284
321
  }
285
322
  }
286
323
  };
287
324
  }
288
- };
325
+ });
289
326
  var _default = rule;
290
327
  exports.default = _default;
@@ -4,6 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
+ exports.cleanComments = cleanComments;
7
8
  exports.emToPixels = exports.convertHyphenatedNameToCamelCase = void 0;
8
9
  exports.findIdentifierInParentScope = findIdentifierInParentScope;
9
10
  exports.findParentNodeForLine = void 0;
@@ -11,7 +12,9 @@ exports.getFontSizeValueInScope = getFontSizeValueInScope;
11
12
  exports.getRawExpression = void 0;
12
13
  exports.getTokenNodeForValue = getTokenNodeForValue;
13
14
  exports.getTokenReplacement = getTokenReplacement;
14
- exports.isSpacingProperty = exports.getValueFromShorthand = exports.getValue = void 0;
15
+ exports.getValueFromShorthand = exports.getValue = void 0;
16
+ exports.insertTokensImport = insertTokensImport;
17
+ exports.isSpacingProperty = void 0;
15
18
  exports.isTokenValueString = isTokenValueString;
16
19
  exports.onlyScaleTokens = exports.isValidSpacingValue = exports.isTypographyProperty = void 0;
17
20
  exports.processCssNode = processCssNode;
@@ -65,6 +68,9 @@ function findIdentifierInParentScope(_ref) {
65
68
  }
66
69
  return null;
67
70
  }
71
+ function insertTokensImport(fixer) {
72
+ return (0, _eslintCodemodUtils.insertAtStartOfFile)(fixer, "".concat((0, _eslintCodemodUtils.insertImportDeclaration)('@atlaskit/tokens', ['token']), "\n"));
73
+ }
68
74
  var isSpacingProperty = function isSpacingProperty(propertyName) {
69
75
  return properties.includes(propertyName);
70
76
  };
@@ -72,10 +78,21 @@ exports.isSpacingProperty = isSpacingProperty;
72
78
  var isTypographyProperty = function isTypographyProperty(propertyName) {
73
79
  return typographyProperties.includes(propertyName);
74
80
  };
81
+
82
+ /**
83
+ * Accomplishes split str by whitespace but preserves expressions in between ${...}
84
+ * even if they might have whitepaces or nested brackets
85
+ * @param str
86
+ * @returns string[]
87
+ * @example
88
+ * Regex has two parts, first attempts to capture anything in between `${...}` in a capture group
89
+ * Whilst allowing nested brackets and non empty characters leading or traling wrapping expression e.g `${gridSize}`, `-${gridSize}px`
90
+ * second part is a white space delimiter
91
+ * For input `-${gridSize / 2}px ${token(...)} 18px -> [`-${gridSize / 2}px`, `${token(...)}`, `18px`]
92
+ */
75
93
  exports.isTypographyProperty = isTypographyProperty;
76
94
  var splitShorthandValues = function splitShorthandValues(str) {
77
- // Regex accomplishes split str by whitespace but ignore spaces in between ${}
78
- return str.split(/(\${[^}]*}\S*)|\s+/g).filter(Boolean);
95
+ return str.split(/(\S*\$\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\S*)|\s+/g).filter(Boolean);
79
96
  };
80
97
  exports.splitShorthandValues = splitShorthandValues;
81
98
  var getValueFromShorthand = function getValueFromShorthand(str) {
@@ -171,7 +188,7 @@ var getRawExpression = function getRawExpression(node, context) {
171
188
  if (!(
172
189
  // if not one of our recognized types or doesn't have a range prop, early return
173
190
 
174
- (0, _eslintCodemodUtils.isNodeOfType)(node, 'Literal') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'Identifier') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'BinaryExpression') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'UnaryExpression') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'TemplateLiteral') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'CallExpression') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'ArrowFunctionExpression')) || !Array.isArray(node.range)) {
191
+ (0, _eslintCodemodUtils.isNodeOfType)(node, 'Literal') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'Identifier') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'BinaryExpression') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'UnaryExpression') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'TemplateLiteral') || (0, _eslintCodemodUtils.isNodeOfType)(node, 'CallExpression')) || !Array.isArray(node.range)) {
175
192
  return null;
176
193
  }
177
194
  var _node$range = (0, _slicedToArray2.default)(node.range, 2),
@@ -353,6 +370,14 @@ function shouldAnalyzeProperty(propertyName, targetOptions) {
353
370
  return false;
354
371
  }
355
372
 
373
+ /**
374
+ * Function that removes JS comments from a string of code,
375
+ * sometimes makers will have single or multiline comments in their tagged template literals styles, this can mess with our parsing logic
376
+ */
377
+ function cleanComments(str) {
378
+ return str.replace(/\/\*([\s\S]*?)\*\//g, '').replace(/\/\/(.*)/g, '');
379
+ }
380
+
356
381
  /**
357
382
  * Returns an array of tuples representing a processed css within `TaggedTemplateExpression` node.
358
383
  * each element of the array is a tuple `[string, string]`,
@@ -370,10 +395,14 @@ function processCssNode(node, context) {
370
395
  return "".concat(q.value.raw).concat(node.quasi.expressions[i] ? getValue(node.quasi.expressions[i], context) : '');
371
396
  }).join('');
372
397
  var rawString = node.quasi.quasis.map(function (q, i) {
373
- return "".concat(q.value.raw).concat(node.quasi.expressions[i] ? "${".concat(getRawExpression(node.quasi.expressions[i], context), "}") : '');
398
+ return "".concat(q.value.raw).concat(node.quasi.expressions[i] ? getRawExpression(node.quasi.expressions[i], context) ? "${".concat(getRawExpression(node.quasi.expressions[i], context), "}") : null : '');
374
399
  }).join('');
375
- var cssProperties = splitCssProperties(combinedString);
376
- var unalteredCssProperties = splitCssProperties(rawString);
400
+ var cssProperties = splitCssProperties(cleanComments(combinedString));
401
+ var unalteredCssProperties = splitCssProperties(cleanComments(rawString));
402
+ if (cssProperties.length !== unalteredCssProperties.length) {
403
+ // this means something wen't wrong with the parsing, the original lines can't be reconciliated with the processed lines
404
+ return undefined;
405
+ }
377
406
  return cssProperties.map(function (cssProperty, index) {
378
407
  return [cssProperty, unalteredCssProperties[index]];
379
408
  });
@@ -437,9 +466,21 @@ function getFontSizeValueInScope(cssProperties) {
437
466
  function splitCssProperties(styleString) {
438
467
  return styleString.split('\n').filter(function (line) {
439
468
  return !line.trim().startsWith('@');
469
+ })
470
+ // sometimes makers will end a css line with `;` that's output from a function expression
471
+ // since we'll rely on `;` to split each line, we need to ensure it's there
472
+ .map(function (line) {
473
+ return line.endsWith(';') ? line : "".concat(line, ";");
440
474
  }).join('\n').replace(/\n/g, '').split(/;|(?<!\$){|(?<!\${.+?)}/) // don't split on template literal expressions i.e. `${...}`
441
- .map(function (el) {
475
+ // filters lines that are completely null, this could be from function expressions that output both property and value
476
+ .filter(function (line) {
477
+ return line.trim() !== 'null' && line.trim() !== 'null;';
478
+ }).map(function (el) {
442
479
  return el.trim() || '';
480
+ })
481
+ // we won't be able to reason about lines that don't have colon (:)
482
+ .filter(function (line) {
483
+ return line.split(':').length === 2;
443
484
  }).filter(Boolean);
444
485
  }
445
486
 
@@ -9,9 +9,7 @@ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/sli
9
9
  var _fs = _interopRequireDefault(require("fs"));
10
10
  var _path = _interopRequireDefault(require("path"));
11
11
  var _utils = require("@typescript-eslint/utils");
12
- var createRule = _utils.ESLintUtils.RuleCreator(function (name) {
13
- return name;
14
- });
12
+ var _createRule = require("../utils/create-rule");
15
13
  var noDeprecatedJSXAttributeMessageId = 'noDeprecatedJSXAttributes';
16
14
  exports.noDeprecatedJSXAttributeMessageId = noDeprecatedJSXAttributeMessageId;
17
15
  var isNodeOfType = function isNodeOfType(node, nodeType) {
@@ -38,7 +36,7 @@ var getConfig = function getConfig() {
38
36
  };
39
37
  var name = 'no-deprecated-apis';
40
38
  exports.name = name;
41
- var rule = createRule({
39
+ var rule = (0, _createRule.createRule)({
42
40
  name: name,
43
41
  defaultOptions: [{
44
42
  deprecatedConfig: getConfig()
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createRule = void 0;
7
+ var _utils = require("@typescript-eslint/utils");
8
+ var createRule = _utils.ESLintUtils.RuleCreator(function (name) {
9
+ return "https://atlassian.design/components/eslint-plugin-design-system/examples#".concat(name);
10
+ });
11
+ exports.createRule = createRule;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-design-system",
3
- "version": "4.16.1",
3
+ "version": "4.16.3",
4
4
  "sideEffects": false
5
5
  }
@@ -1,29 +1,62 @@
1
1
  /* eslint-disable @atlassian/tangerine/import/entry-points */
2
2
 
3
3
  import { isNodeOfType, node as nodeFn, property } from 'eslint-codemod-utils';
4
+ import { createRule } from '../utils/create-rule';
4
5
  import { isDecendantOfGlobalToken } from '../utils/is-node';
5
- import { convertHyphenatedNameToCamelCase, emToPixels, findParentNodeForLine, getFontSizeValueInScope, getTokenNodeForValue, getTokenReplacement, getValue, getValueFromShorthand, isTokenValueString, isTypographyProperty, isValidSpacingValue, processCssNode, shouldAnalyzeProperty, spacingValueToToken, splitShorthandValues, typographyValueToToken } from './utils';
6
- const rule = {
6
+ import { convertHyphenatedNameToCamelCase, emToPixels, findParentNodeForLine, getFontSizeValueInScope, getTokenNodeForValue, getTokenReplacement, getValue, getValueFromShorthand, insertTokensImport, isTokenValueString, isTypographyProperty, isValidSpacingValue, processCssNode, shouldAnalyzeProperty, spacingValueToToken, splitShorthandValues, typographyValueToToken } from './utils';
7
+ const rule = createRule({
8
+ defaultOptions: [{
9
+ addons: ['spacing'],
10
+ applyImport: true
11
+ }],
12
+ name: 'ensure-design-token-usage-spacing',
7
13
  meta: {
14
+ schema: {
15
+ type: 'array',
16
+ items: {
17
+ type: 'object',
18
+ properties: {
19
+ applyImport: {
20
+ type: 'boolean'
21
+ },
22
+ addons: {
23
+ type: 'array',
24
+ items: {
25
+ enum: ['spacing', 'typography', 'shape']
26
+ }
27
+ }
28
+ }
29
+ }
30
+ },
8
31
  type: 'problem',
9
32
  fixable: 'code',
10
33
  docs: {
11
34
  description: 'Rule ensures all spacing CSS properties apply a matching spacing token',
12
- recommended: true
35
+ recommended: 'error'
13
36
  },
14
37
  messages: {
15
38
  noRawSpacingValues: 'The use of spacing primitives or tokens is preferred over the direct application of spacing properties.\n\n@meta <<{{payload}}>>',
16
39
  autofixesPossible: 'Automated corrections available for spacing values. Apply autofix to replace values with appropriate tokens'
17
40
  }
18
41
  },
19
- create(context) {
20
- var _context$options$;
21
- const targetCategories = ['spacing'];
22
- const configCategories = (_context$options$ = context.options[0]) === null || _context$options$ === void 0 ? void 0 : _context$options$.addons;
23
- if (Array.isArray(configCategories) && configCategories.includes('typography')) {
24
- targetCategories.push('typography');
42
+ // @ts-expect-error
43
+ create(context, options) {
44
+ let tokenNode = null;
45
+
46
+ // merge configs
47
+ const ruleConfig = {
48
+ ...options[0],
49
+ addons: [...options[0].addons]
50
+ };
51
+ if (!ruleConfig.addons.includes('spacing')) {
52
+ ruleConfig.addons.push('spacing');
25
53
  }
26
54
  return {
55
+ ImportDeclaration(node) {
56
+ if (node.source.value === '@atlaskit/tokens' && ruleConfig.applyImport) {
57
+ tokenNode = node;
58
+ }
59
+ },
27
60
  // CSSObjectExpression
28
61
  // const styles = css({ color: 'red', margin: '4px' }), styled.div({ color: 'red', margin: '4px' })
29
62
  'CallExpression[callee.name=css] > ObjectExpression, CallExpression[callee.object.name=styled] > ObjectExpression': parentNode => {
@@ -57,7 +90,7 @@ const rule = {
57
90
  if (!isNodeOfType(node.key, 'Identifier')) {
58
91
  return;
59
92
  }
60
- if (!shouldAnalyzeProperty(node.key.name, targetCategories)) {
93
+ if (!shouldAnalyzeProperty(node.key.name, ruleConfig.addons)) {
61
94
  return;
62
95
  }
63
96
  if (isDecendantOfGlobalToken(node.value)) {
@@ -105,7 +138,7 @@ const rule = {
105
138
  },
106
139
  fix: fixer => {
107
140
  var _node$loc;
108
- if (!shouldAnalyzeProperty(propertyName, targetCategories)) {
141
+ if (!shouldAnalyzeProperty(propertyName, ruleConfig.addons)) {
109
142
  return null;
110
143
  }
111
144
  const pixelValueString = `${pixelValue}px`;
@@ -115,10 +148,10 @@ const rule = {
115
148
  return null;
116
149
  }
117
150
  const replacementValue = getTokenNodeForValue(propertyName, lookupValue);
118
- return [fixer.insertTextBefore(node, `// TODO Delete this comment after verifying spacing token -> previous value \`${nodeFn(node.value)}\`\n${' '.padStart(((_node$loc = node.loc) === null || _node$loc === void 0 ? void 0 : _node$loc.start.column) || 0)}`), fixer.replaceText(node, property({
151
+ return (!tokenNode && ruleConfig.applyImport ? [insertTokensImport(fixer)] : []).concat([fixer.insertTextBefore(node, `// TODO Delete this comment after verifying spacing token -> previous value \`${nodeFn(node.value)}\`\n${' '.padStart(((_node$loc = node.loc) === null || _node$loc === void 0 ? void 0 : _node$loc.start.column) || 0)}`), fixer.replaceText(node, property({
119
152
  ...node,
120
153
  value: replacementValue
121
- }).toString())];
154
+ }).toString())]);
122
155
  }
123
156
  });
124
157
  }
@@ -143,11 +176,11 @@ const rule = {
143
176
  if (!allResolvableValues) {
144
177
  return null;
145
178
  }
146
- return fixer.replaceText(node.value, `\`${values.map(value => {
179
+ return (!tokenNode && ruleConfig.applyImport ? [insertTokensImport(fixer)] : []).concat([fixer.replaceText(node.value, `\`${values.map(value => {
147
180
  const pixelValue = emToPixels(value, fontSize);
148
181
  const pixelValueString = `${pixelValue}px`;
149
182
  return `\${${getTokenNodeForValue(propertyName, pixelValueString)}}`;
150
- }).join(' ')}\``);
183
+ }).join(' ')}\``)]);
151
184
  } : undefined
152
185
  });
153
186
  });
@@ -161,8 +194,12 @@ const rule = {
161
194
  if (node.type !== 'TaggedTemplateExpression') {
162
195
  return;
163
196
  }
164
- const parentNode = findParentNodeForLine(node);
165
197
  const processedCssLines = processCssNode(node, context);
198
+ if (!processedCssLines) {
199
+ // if we can't get a processed css we bail
200
+ return;
201
+ }
202
+ const parentNode = findParentNodeForLine(node);
166
203
  const globalFontSize = getFontSizeValueInScope(processedCssLines);
167
204
  const textForSource = context.getSourceCode().getText(node.quasi);
168
205
  const allReplacedValues = [];
@@ -172,7 +209,7 @@ const rule = {
172
209
  const propertyName = convertHyphenatedNameToCamelCase(originalProperty);
173
210
  const isFontFamily = /fontFamily/.test(propertyName);
174
211
  const replacedValuesPerProperty = [originalProperty];
175
- if (!shouldAnalyzeProperty(propertyName, targetCategories) || !resolvedCssValues || !isValidSpacingValue(resolvedCssValues, globalFontSize)) {
212
+ if (!shouldAnalyzeProperty(propertyName, ruleConfig.addons) || !resolvedCssValues || !isValidSpacingValue(resolvedCssValues, globalFontSize)) {
176
213
  // in all of these cases no changes should be made to the current property
177
214
  return currentSource;
178
215
  }
@@ -252,12 +289,12 @@ const rule = {
252
289
  node,
253
290
  messageId: 'autofixesPossible',
254
291
  fix: fixer => {
255
- return [fixer.insertTextBefore(parentNode, replacementComments), fixer.replaceText(node.quasi, completeSource)];
292
+ return (!tokenNode && ruleConfig.applyImport ? [insertTokensImport(fixer)] : []).concat([fixer.insertTextBefore(parentNode, replacementComments), fixer.replaceText(node.quasi, completeSource)]);
256
293
  }
257
294
  });
258
295
  }
259
296
  }
260
297
  };
261
298
  }
262
- };
299
+ });
263
300
  export default rule;
@@ -1,4 +1,4 @@
1
- import { callExpression, identifier, isNodeOfType, literal } from 'eslint-codemod-utils';
1
+ import { callExpression, identifier, insertAtStartOfFile, insertImportDeclaration, isNodeOfType, literal } from 'eslint-codemod-utils';
2
2
  import { spacing as spacingScale, typography as typographyTokens } from '@atlaskit/tokens/tokens-raw';
3
3
  const typographyProperties = ['fontSize', 'fontWeight', 'fontFamily', 'lineHeight'];
4
4
  const properties = ['padding', 'paddingBlock', 'paddingInline', 'paddingLeft', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingInline', 'paddingInlineStart', 'paddingInlineEnd', 'paddingBlock', 'paddingBlockStart', 'paddingBlockEnd', 'marginLeft', 'marginTop', 'marginRight', 'marginBottom', 'marginInline', 'marginInlineStart', 'marginInlineEnd', 'marginBlock', 'marginBlockStart', 'marginBlockEnd', 'margin', 'gap', 'rowGap', 'gridRowGap', 'columnGap', 'gridColumnGap'];
@@ -30,15 +30,29 @@ export function findIdentifierInParentScope({
30
30
  }
31
31
  return null;
32
32
  }
33
+ export function insertTokensImport(fixer) {
34
+ return insertAtStartOfFile(fixer, `${insertImportDeclaration('@atlaskit/tokens', ['token'])}\n`);
35
+ }
33
36
  export const isSpacingProperty = propertyName => {
34
37
  return properties.includes(propertyName);
35
38
  };
36
39
  export const isTypographyProperty = propertyName => {
37
40
  return typographyProperties.includes(propertyName);
38
41
  };
42
+
43
+ /**
44
+ * Accomplishes split str by whitespace but preserves expressions in between ${...}
45
+ * even if they might have whitepaces or nested brackets
46
+ * @param str
47
+ * @returns string[]
48
+ * @example
49
+ * Regex has two parts, first attempts to capture anything in between `${...}` in a capture group
50
+ * Whilst allowing nested brackets and non empty characters leading or traling wrapping expression e.g `${gridSize}`, `-${gridSize}px`
51
+ * second part is a white space delimiter
52
+ * For input `-${gridSize / 2}px ${token(...)} 18px -> [`-${gridSize / 2}px`, `${token(...)}`, `18px`]
53
+ */
39
54
  export const splitShorthandValues = str => {
40
- // Regex accomplishes split str by whitespace but ignore spaces in between ${}
41
- return str.split(/(\${[^}]*}\S*)|\s+/g).filter(Boolean);
55
+ return str.split(/(\S*\$\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\S*)|\s+/g).filter(Boolean);
42
56
  };
43
57
  export const getValueFromShorthand = str => {
44
58
  const valueString = String(str);
@@ -119,7 +133,7 @@ export const getRawExpression = (node, context) => {
119
133
  if (!(
120
134
  // if not one of our recognized types or doesn't have a range prop, early return
121
135
 
122
- isNodeOfType(node, 'Literal') || isNodeOfType(node, 'Identifier') || isNodeOfType(node, 'BinaryExpression') || isNodeOfType(node, 'UnaryExpression') || isNodeOfType(node, 'TemplateLiteral') || isNodeOfType(node, 'CallExpression') || isNodeOfType(node, 'ArrowFunctionExpression')) || !Array.isArray(node.range)) {
136
+ isNodeOfType(node, 'Literal') || isNodeOfType(node, 'Identifier') || isNodeOfType(node, 'BinaryExpression') || isNodeOfType(node, 'UnaryExpression') || isNodeOfType(node, 'TemplateLiteral') || isNodeOfType(node, 'CallExpression')) || !Array.isArray(node.range)) {
123
137
  return null;
124
138
  }
125
139
  const [start, end] = node.range;
@@ -293,6 +307,14 @@ export function shouldAnalyzeProperty(propertyName, targetOptions) {
293
307
  return false;
294
308
  }
295
309
 
310
+ /**
311
+ * Function that removes JS comments from a string of code,
312
+ * sometimes makers will have single or multiline comments in their tagged template literals styles, this can mess with our parsing logic
313
+ */
314
+ export function cleanComments(str) {
315
+ return str.replace(/\/\*([\s\S]*?)\*\//g, '').replace(/\/\/(.*)/g, '');
316
+ }
317
+
296
318
  /**
297
319
  * Returns an array of tuples representing a processed css within `TaggedTemplateExpression` node.
298
320
  * each element of the array is a tuple `[string, string]`,
@@ -310,10 +332,14 @@ export function processCssNode(node, context) {
310
332
  return `${q.value.raw}${node.quasi.expressions[i] ? getValue(node.quasi.expressions[i], context) : ''}`;
311
333
  }).join('');
312
334
  const rawString = node.quasi.quasis.map((q, i) => {
313
- return `${q.value.raw}${node.quasi.expressions[i] ? `\${${getRawExpression(node.quasi.expressions[i], context)}}` : ''}`;
335
+ return `${q.value.raw}${node.quasi.expressions[i] ? getRawExpression(node.quasi.expressions[i], context) ? `\${${getRawExpression(node.quasi.expressions[i], context)}}` : null : ''}`;
314
336
  }).join('');
315
- const cssProperties = splitCssProperties(combinedString);
316
- const unalteredCssProperties = splitCssProperties(rawString);
337
+ const cssProperties = splitCssProperties(cleanComments(combinedString));
338
+ const unalteredCssProperties = splitCssProperties(cleanComments(rawString));
339
+ if (cssProperties.length !== unalteredCssProperties.length) {
340
+ // this means something wen't wrong with the parsing, the original lines can't be reconciliated with the processed lines
341
+ return undefined;
342
+ }
317
343
  return cssProperties.map((cssProperty, index) => [cssProperty, unalteredCssProperties[index]]);
318
344
  }
319
345
 
@@ -365,8 +391,14 @@ export function getFontSizeValueInScope(cssProperties) {
365
391
  * @param styleString string of css properties
366
392
  */
367
393
  export function splitCssProperties(styleString) {
368
- return styleString.split('\n').filter(line => !line.trim().startsWith('@')).join('\n').replace(/\n/g, '').split(/;|(?<!\$){|(?<!\${.+?)}/) // don't split on template literal expressions i.e. `${...}`
369
- .map(el => el.trim() || '').filter(Boolean);
394
+ return styleString.split('\n').filter(line => !line.trim().startsWith('@'))
395
+ // sometimes makers will end a css line with `;` that's output from a function expression
396
+ // since we'll rely on `;` to split each line, we need to ensure it's there
397
+ .map(line => line.endsWith(';') ? line : `${line};`).join('\n').replace(/\n/g, '').split(/;|(?<!\$){|(?<!\${.+?)}/) // don't split on template literal expressions i.e. `${...}`
398
+ // filters lines that are completely null, this could be from function expressions that output both property and value
399
+ .filter(line => line.trim() !== 'null' && line.trim() !== 'null;').map(el => el.trim() || '')
400
+ // we won't be able to reason about lines that don't have colon (:)
401
+ .filter(line => line.split(':').length === 2).filter(Boolean);
370
402
  }
371
403
 
372
404
  /**
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { AST_NODE_TYPES, ASTUtils, ESLintUtils } from '@typescript-eslint/utils';
4
- const createRule = ESLintUtils.RuleCreator(name => name);
3
+ import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils';
4
+ import { createRule } from '../utils/create-rule';
5
5
  export const noDeprecatedJSXAttributeMessageId = 'noDeprecatedJSXAttributes';
6
6
  const isNodeOfType = (node, nodeType) => ASTUtils.isNodeOfType(nodeType)(node);
7
7
  const isImportDeclaration = programStatement => {
@@ -0,0 +1,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export const createRule = ESLintUtils.RuleCreator(name => `https://atlassian.design/components/eslint-plugin-design-system/examples#${name}`);
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-design-system",
3
- "version": "4.16.1",
3
+ "version": "4.16.3",
4
4
  "sideEffects": false
5
5
  }
@@ -1,33 +1,66 @@
1
- import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
3
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
4
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
4
5
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
5
6
  /* eslint-disable @atlassian/tangerine/import/entry-points */
6
7
 
7
8
  import { isNodeOfType, node as nodeFn, property } from 'eslint-codemod-utils';
9
+ import { createRule } from '../utils/create-rule';
8
10
  import { isDecendantOfGlobalToken } from '../utils/is-node';
9
- import { convertHyphenatedNameToCamelCase, emToPixels, findParentNodeForLine, getFontSizeValueInScope, getTokenNodeForValue, getTokenReplacement, getValue, getValueFromShorthand, isTokenValueString, isTypographyProperty, isValidSpacingValue, processCssNode, shouldAnalyzeProperty, spacingValueToToken, splitShorthandValues, typographyValueToToken } from './utils';
10
- var rule = {
11
+ import { convertHyphenatedNameToCamelCase, emToPixels, findParentNodeForLine, getFontSizeValueInScope, getTokenNodeForValue, getTokenReplacement, getValue, getValueFromShorthand, insertTokensImport, isTokenValueString, isTypographyProperty, isValidSpacingValue, processCssNode, shouldAnalyzeProperty, spacingValueToToken, splitShorthandValues, typographyValueToToken } from './utils';
12
+ var rule = createRule({
13
+ defaultOptions: [{
14
+ addons: ['spacing'],
15
+ applyImport: true
16
+ }],
17
+ name: 'ensure-design-token-usage-spacing',
11
18
  meta: {
19
+ schema: {
20
+ type: 'array',
21
+ items: {
22
+ type: 'object',
23
+ properties: {
24
+ applyImport: {
25
+ type: 'boolean'
26
+ },
27
+ addons: {
28
+ type: 'array',
29
+ items: {
30
+ enum: ['spacing', 'typography', 'shape']
31
+ }
32
+ }
33
+ }
34
+ }
35
+ },
12
36
  type: 'problem',
13
37
  fixable: 'code',
14
38
  docs: {
15
39
  description: 'Rule ensures all spacing CSS properties apply a matching spacing token',
16
- recommended: true
40
+ recommended: 'error'
17
41
  },
18
42
  messages: {
19
43
  noRawSpacingValues: 'The use of spacing primitives or tokens is preferred over the direct application of spacing properties.\n\n@meta <<{{payload}}>>',
20
44
  autofixesPossible: 'Automated corrections available for spacing values. Apply autofix to replace values with appropriate tokens'
21
45
  }
22
46
  },
23
- create: function create(context) {
24
- var _context$options$;
25
- var targetCategories = ['spacing'];
26
- var configCategories = (_context$options$ = context.options[0]) === null || _context$options$ === void 0 ? void 0 : _context$options$.addons;
27
- if (Array.isArray(configCategories) && configCategories.includes('typography')) {
28
- targetCategories.push('typography');
47
+ // @ts-expect-error
48
+ create: function create(context, options) {
49
+ var tokenNode = null;
50
+
51
+ // merge configs
52
+ var ruleConfig = _objectSpread(_objectSpread({}, options[0]), {}, {
53
+ addons: _toConsumableArray(options[0].addons)
54
+ });
55
+ if (!ruleConfig.addons.includes('spacing')) {
56
+ ruleConfig.addons.push('spacing');
29
57
  }
30
58
  return {
59
+ ImportDeclaration: function ImportDeclaration(node) {
60
+ if (node.source.value === '@atlaskit/tokens' && ruleConfig.applyImport) {
61
+ tokenNode = node;
62
+ }
63
+ },
31
64
  // CSSObjectExpression
32
65
  // const styles = css({ color: 'red', margin: '4px' }), styled.div({ color: 'red', margin: '4px' })
33
66
  'CallExpression[callee.name=css] > ObjectExpression, CallExpression[callee.object.name=styled] > ObjectExpression': function CallExpressionCalleeNameCssObjectExpressionCallExpressionCalleeObjectNameStyledObjectExpression(parentNode) {
@@ -61,7 +94,7 @@ var rule = {
61
94
  if (!isNodeOfType(node.key, 'Identifier')) {
62
95
  return;
63
96
  }
64
- if (!shouldAnalyzeProperty(node.key.name, targetCategories)) {
97
+ if (!shouldAnalyzeProperty(node.key.name, ruleConfig.addons)) {
65
98
  return;
66
99
  }
67
100
  if (isDecendantOfGlobalToken(node.value)) {
@@ -110,7 +143,7 @@ var rule = {
110
143
  },
111
144
  fix: function fix(fixer) {
112
145
  var _node$loc;
113
- if (!shouldAnalyzeProperty(propertyName, targetCategories)) {
146
+ if (!shouldAnalyzeProperty(propertyName, ruleConfig.addons)) {
114
147
  return null;
115
148
  }
116
149
  var pixelValueString = "".concat(pixelValue, "px");
@@ -120,9 +153,9 @@ var rule = {
120
153
  return null;
121
154
  }
122
155
  var replacementValue = getTokenNodeForValue(propertyName, lookupValue);
123
- return [fixer.insertTextBefore(node, "// TODO Delete this comment after verifying spacing token -> previous value `".concat(nodeFn(node.value), "`\n").concat(' '.padStart(((_node$loc = node.loc) === null || _node$loc === void 0 ? void 0 : _node$loc.start.column) || 0))), fixer.replaceText(node, property(_objectSpread(_objectSpread({}, node), {}, {
156
+ return (!tokenNode && ruleConfig.applyImport ? [insertTokensImport(fixer)] : []).concat([fixer.insertTextBefore(node, "// TODO Delete this comment after verifying spacing token -> previous value `".concat(nodeFn(node.value), "`\n").concat(' '.padStart(((_node$loc = node.loc) === null || _node$loc === void 0 ? void 0 : _node$loc.start.column) || 0))), fixer.replaceText(node, property(_objectSpread(_objectSpread({}, node), {}, {
124
157
  value: replacementValue
125
- })).toString())];
158
+ })).toString())]);
126
159
  }
127
160
  });
128
161
  }
@@ -149,11 +182,11 @@ var rule = {
149
182
  if (!allResolvableValues) {
150
183
  return null;
151
184
  }
152
- return fixer.replaceText(node.value, "`".concat(values.map(function (value) {
185
+ return (!tokenNode && ruleConfig.applyImport ? [insertTokensImport(fixer)] : []).concat([fixer.replaceText(node.value, "`".concat(values.map(function (value) {
153
186
  var pixelValue = emToPixels(value, fontSize);
154
187
  var pixelValueString = "".concat(pixelValue, "px");
155
188
  return "${".concat(getTokenNodeForValue(propertyName, pixelValueString), "}");
156
- }).join(' '), "`"));
189
+ }).join(' '), "`"))]);
157
190
  } : undefined
158
191
  });
159
192
  });
@@ -167,8 +200,12 @@ var rule = {
167
200
  if (node.type !== 'TaggedTemplateExpression') {
168
201
  return;
169
202
  }
170
- var parentNode = findParentNodeForLine(node);
171
203
  var processedCssLines = processCssNode(node, context);
204
+ if (!processedCssLines) {
205
+ // if we can't get a processed css we bail
206
+ return;
207
+ }
208
+ var parentNode = findParentNodeForLine(node);
172
209
  var globalFontSize = getFontSizeValueInScope(processedCssLines);
173
210
  var textForSource = context.getSourceCode().getText(node.quasi);
174
211
  var allReplacedValues = [];
@@ -187,7 +224,7 @@ var rule = {
187
224
  var propertyName = convertHyphenatedNameToCamelCase(originalProperty);
188
225
  var isFontFamily = /fontFamily/.test(propertyName);
189
226
  var replacedValuesPerProperty = [originalProperty];
190
- if (!shouldAnalyzeProperty(propertyName, targetCategories) || !resolvedCssValues || !isValidSpacingValue(resolvedCssValues, globalFontSize)) {
227
+ if (!shouldAnalyzeProperty(propertyName, ruleConfig.addons) || !resolvedCssValues || !isValidSpacingValue(resolvedCssValues, globalFontSize)) {
191
228
  // in all of these cases no changes should be made to the current property
192
229
  return currentSource;
193
230
  }
@@ -273,12 +310,12 @@ var rule = {
273
310
  node: node,
274
311
  messageId: 'autofixesPossible',
275
312
  fix: function fix(fixer) {
276
- return [fixer.insertTextBefore(parentNode, replacementComments), fixer.replaceText(node.quasi, completeSource)];
313
+ return (!tokenNode && ruleConfig.applyImport ? [insertTokensImport(fixer)] : []).concat([fixer.insertTextBefore(parentNode, replacementComments), fixer.replaceText(node.quasi, completeSource)]);
277
314
  }
278
315
  });
279
316
  }
280
317
  }
281
318
  };
282
319
  }
283
- };
320
+ });
284
321
  export default rule;
@@ -1,5 +1,5 @@
1
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
- import { callExpression, identifier, isNodeOfType, literal } from 'eslint-codemod-utils';
2
+ import { callExpression, identifier, insertAtStartOfFile, insertImportDeclaration, isNodeOfType, literal } from 'eslint-codemod-utils';
3
3
  import { spacing as spacingScale, typography as typographyTokens } from '@atlaskit/tokens/tokens-raw';
4
4
  var typographyProperties = ['fontSize', 'fontWeight', 'fontFamily', 'lineHeight'];
5
5
  var properties = ['padding', 'paddingBlock', 'paddingInline', 'paddingLeft', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingInline', 'paddingInlineStart', 'paddingInlineEnd', 'paddingBlock', 'paddingBlockStart', 'paddingBlockEnd', 'marginLeft', 'marginTop', 'marginRight', 'marginBottom', 'marginInline', 'marginInlineStart', 'marginInlineEnd', 'marginBlock', 'marginBlockStart', 'marginBlockEnd', 'margin', 'gap', 'rowGap', 'gridRowGap', 'columnGap', 'gridColumnGap'];
@@ -40,15 +40,29 @@ export function findIdentifierInParentScope(_ref) {
40
40
  }
41
41
  return null;
42
42
  }
43
+ export function insertTokensImport(fixer) {
44
+ return insertAtStartOfFile(fixer, "".concat(insertImportDeclaration('@atlaskit/tokens', ['token']), "\n"));
45
+ }
43
46
  export var isSpacingProperty = function isSpacingProperty(propertyName) {
44
47
  return properties.includes(propertyName);
45
48
  };
46
49
  export var isTypographyProperty = function isTypographyProperty(propertyName) {
47
50
  return typographyProperties.includes(propertyName);
48
51
  };
52
+
53
+ /**
54
+ * Accomplishes split str by whitespace but preserves expressions in between ${...}
55
+ * even if they might have whitepaces or nested brackets
56
+ * @param str
57
+ * @returns string[]
58
+ * @example
59
+ * Regex has two parts, first attempts to capture anything in between `${...}` in a capture group
60
+ * Whilst allowing nested brackets and non empty characters leading or traling wrapping expression e.g `${gridSize}`, `-${gridSize}px`
61
+ * second part is a white space delimiter
62
+ * For input `-${gridSize / 2}px ${token(...)} 18px -> [`-${gridSize / 2}px`, `${token(...)}`, `18px`]
63
+ */
49
64
  export var splitShorthandValues = function splitShorthandValues(str) {
50
- // Regex accomplishes split str by whitespace but ignore spaces in between ${}
51
- return str.split(/(\${[^}]*}\S*)|\s+/g).filter(Boolean);
65
+ return str.split(/(\S*\$\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\S*)|\s+/g).filter(Boolean);
52
66
  };
53
67
  export var getValueFromShorthand = function getValueFromShorthand(str) {
54
68
  var valueString = String(str);
@@ -141,7 +155,7 @@ export var getRawExpression = function getRawExpression(node, context) {
141
155
  if (!(
142
156
  // if not one of our recognized types or doesn't have a range prop, early return
143
157
 
144
- isNodeOfType(node, 'Literal') || isNodeOfType(node, 'Identifier') || isNodeOfType(node, 'BinaryExpression') || isNodeOfType(node, 'UnaryExpression') || isNodeOfType(node, 'TemplateLiteral') || isNodeOfType(node, 'CallExpression') || isNodeOfType(node, 'ArrowFunctionExpression')) || !Array.isArray(node.range)) {
158
+ isNodeOfType(node, 'Literal') || isNodeOfType(node, 'Identifier') || isNodeOfType(node, 'BinaryExpression') || isNodeOfType(node, 'UnaryExpression') || isNodeOfType(node, 'TemplateLiteral') || isNodeOfType(node, 'CallExpression')) || !Array.isArray(node.range)) {
145
159
  return null;
146
160
  }
147
161
  var _node$range = _slicedToArray(node.range, 2),
@@ -317,6 +331,14 @@ export function shouldAnalyzeProperty(propertyName, targetOptions) {
317
331
  return false;
318
332
  }
319
333
 
334
+ /**
335
+ * Function that removes JS comments from a string of code,
336
+ * sometimes makers will have single or multiline comments in their tagged template literals styles, this can mess with our parsing logic
337
+ */
338
+ export function cleanComments(str) {
339
+ return str.replace(/\/\*([\s\S]*?)\*\//g, '').replace(/\/\/(.*)/g, '');
340
+ }
341
+
320
342
  /**
321
343
  * Returns an array of tuples representing a processed css within `TaggedTemplateExpression` node.
322
344
  * each element of the array is a tuple `[string, string]`,
@@ -334,10 +356,14 @@ export function processCssNode(node, context) {
334
356
  return "".concat(q.value.raw).concat(node.quasi.expressions[i] ? getValue(node.quasi.expressions[i], context) : '');
335
357
  }).join('');
336
358
  var rawString = node.quasi.quasis.map(function (q, i) {
337
- return "".concat(q.value.raw).concat(node.quasi.expressions[i] ? "${".concat(getRawExpression(node.quasi.expressions[i], context), "}") : '');
359
+ return "".concat(q.value.raw).concat(node.quasi.expressions[i] ? getRawExpression(node.quasi.expressions[i], context) ? "${".concat(getRawExpression(node.quasi.expressions[i], context), "}") : null : '');
338
360
  }).join('');
339
- var cssProperties = splitCssProperties(combinedString);
340
- var unalteredCssProperties = splitCssProperties(rawString);
361
+ var cssProperties = splitCssProperties(cleanComments(combinedString));
362
+ var unalteredCssProperties = splitCssProperties(cleanComments(rawString));
363
+ if (cssProperties.length !== unalteredCssProperties.length) {
364
+ // this means something wen't wrong with the parsing, the original lines can't be reconciliated with the processed lines
365
+ return undefined;
366
+ }
341
367
  return cssProperties.map(function (cssProperty, index) {
342
368
  return [cssProperty, unalteredCssProperties[index]];
343
369
  });
@@ -401,9 +427,21 @@ export function getFontSizeValueInScope(cssProperties) {
401
427
  export function splitCssProperties(styleString) {
402
428
  return styleString.split('\n').filter(function (line) {
403
429
  return !line.trim().startsWith('@');
430
+ })
431
+ // sometimes makers will end a css line with `;` that's output from a function expression
432
+ // since we'll rely on `;` to split each line, we need to ensure it's there
433
+ .map(function (line) {
434
+ return line.endsWith(';') ? line : "".concat(line, ";");
404
435
  }).join('\n').replace(/\n/g, '').split(/;|(?<!\$){|(?<!\${.+?)}/) // don't split on template literal expressions i.e. `${...}`
405
- .map(function (el) {
436
+ // filters lines that are completely null, this could be from function expressions that output both property and value
437
+ .filter(function (line) {
438
+ return line.trim() !== 'null' && line.trim() !== 'null;';
439
+ }).map(function (el) {
406
440
  return el.trim() || '';
441
+ })
442
+ // we won't be able to reason about lines that don't have colon (:)
443
+ .filter(function (line) {
444
+ return line.split(':').length === 2;
407
445
  }).filter(Boolean);
408
446
  }
409
447
 
@@ -1,10 +1,8 @@
1
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
- import { AST_NODE_TYPES, ASTUtils, ESLintUtils } from '@typescript-eslint/utils';
5
- var createRule = ESLintUtils.RuleCreator(function (name) {
6
- return name;
7
- });
4
+ import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils';
5
+ import { createRule } from '../utils/create-rule';
8
6
  export var noDeprecatedJSXAttributeMessageId = 'noDeprecatedJSXAttributes';
9
7
  var isNodeOfType = function isNodeOfType(node, nodeType) {
10
8
  return ASTUtils.isNodeOfType(nodeType)(node);
@@ -0,0 +1,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export var createRule = ESLintUtils.RuleCreator(function (name) {
3
+ return "https://atlassian.design/components/eslint-plugin-design-system/examples#".concat(name);
4
+ });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-design-system",
3
- "version": "4.16.1",
3
+ "version": "4.16.3",
4
4
  "sideEffects": false
5
5
  }
@@ -9,7 +9,10 @@ export declare const rules: {
9
9
  'no-banned-imports': import("eslint").Rule.RuleModule;
10
10
  'no-unsafe-design-token-usage': import("eslint").Rule.RuleModule;
11
11
  'use-visually-hidden': import("eslint").Rule.RuleModule;
12
- 'ensure-design-token-usage-spacing': import("eslint").Rule.RuleModule;
12
+ 'ensure-design-token-usage-spacing': import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noRawSpacingValues" | "autofixesPossible", [{
13
+ addons: ("spacing" | "typography" | "shape")[];
14
+ applyImport?: boolean | undefined;
15
+ }], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
13
16
  };
14
17
  export declare const configs: {
15
18
  recommended: {
@@ -1,3 +1,7 @@
1
- import type { Rule } from 'eslint';
2
- declare const rule: Rule.RuleModule;
1
+ declare type Addon = 'spacing' | 'typography' | 'shape';
2
+ declare type RuleConfig = {
3
+ addons: Addon[];
4
+ applyImport?: boolean;
5
+ };
6
+ declare const rule: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noRawSpacingValues" | "autofixesPossible", [RuleConfig], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
7
  export default rule;
@@ -1,7 +1,7 @@
1
1
  import type { Rule, Scope } from 'eslint';
2
2
  import { EslintNode, TaggedTemplateExpression } from 'eslint-codemod-utils';
3
3
  export declare type ProcessedCSSLines = [string, string][];
4
- export declare type TargetOptions = ('spacing' | 'typography')[];
4
+ export declare type TargetOptions = ('spacing' | 'typography' | 'shape')[];
5
5
  /**
6
6
  * Currently we have a wide range of experimental spacing tokens that we are testing.
7
7
  * We only want transforms to apply to the stable scale values, not the rest.
@@ -70,8 +70,20 @@ export declare function findIdentifierInParentScope({ scope, identifierName, }:
70
70
  scope: Scope.Scope;
71
71
  identifierName: string;
72
72
  }): Scope.Variable | null;
73
+ export declare function insertTokensImport(fixer: Rule.RuleFixer): Rule.Fix;
73
74
  export declare const isSpacingProperty: (propertyName: string) => boolean;
74
75
  export declare const isTypographyProperty: (propertyName: string) => boolean;
76
+ /**
77
+ * Accomplishes split str by whitespace but preserves expressions in between ${...}
78
+ * even if they might have whitepaces or nested brackets
79
+ * @param str
80
+ * @returns string[]
81
+ * @example
82
+ * Regex has two parts, first attempts to capture anything in between `${...}` in a capture group
83
+ * Whilst allowing nested brackets and non empty characters leading or traling wrapping expression e.g `${gridSize}`, `-${gridSize}px`
84
+ * second part is a white space delimiter
85
+ * For input `-${gridSize / 2}px ${token(...)} 18px -> [`-${gridSize / 2}px`, `${token(...)}`, `18px`]
86
+ */
75
87
  export declare const splitShorthandValues: (str: string) => string[];
76
88
  export declare const getValueFromShorthand: (str: unknown) => any[];
77
89
  export declare const getValue: (node: EslintNode, context: Rule.RuleContext) => string | number | any[] | null | undefined;
@@ -96,6 +108,11 @@ export declare const findParentNodeForLine: (node: Rule.Node) => Rule.Node;
96
108
  * ```
97
109
  */
98
110
  export declare function shouldAnalyzeProperty(propertyName: string, targetOptions: TargetOptions): boolean;
111
+ /**
112
+ * Function that removes JS comments from a string of code,
113
+ * sometimes makers will have single or multiline comments in their tagged template literals styles, this can mess with our parsing logic
114
+ */
115
+ export declare function cleanComments(str: string): string;
99
116
  /**
100
117
  * Returns an array of tuples representing a processed css within `TaggedTemplateExpression` node.
101
118
  * each element of the array is a tuple `[string, string]`,
@@ -108,7 +125,7 @@ export declare function shouldAnalyzeProperty(propertyName: string, targetOption
108
125
  * `[['padding: 8', 'padding: ${gridSize()}'], ['margin: 6', 'margin: 6px' ]]`
109
126
  * ```
110
127
  */
111
- export declare function processCssNode(node: TaggedTemplateExpression & Rule.NodeParentExtension, context: Rule.RuleContext): ProcessedCSSLines;
128
+ export declare function processCssNode(node: TaggedTemplateExpression & Rule.NodeParentExtension, context: Rule.RuleContext): ProcessedCSSLines | undefined;
112
129
  /**
113
130
  * Returns a token node for a given value including fallbacks.
114
131
  * @param propertyName camelCase CSS property
@@ -0,0 +1,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const createRule: <TOptions extends readonly unknown[], TMessageIds extends string, TRuleListener extends import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener = import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>({ name, meta, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<TOptions, TMessageIds, TRuleListener>>) => import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<TMessageIds, TOptions, TRuleListener>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-design-system",
3
3
  "description": "The essential plugin for use with the Atlassian Design System.",
4
- "version": "4.16.1",
4
+ "version": "4.16.3",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org/"