@atlaskit/eslint-plugin-design-system 8.17.0 → 8.18.1

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 (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -0
  3. package/constellation/index/usage.mdx +22 -0
  4. package/dist/cjs/presets/all.codegen.js +2 -1
  5. package/dist/cjs/presets/recommended.codegen.js +2 -1
  6. package/dist/cjs/rules/index.codegen.js +3 -1
  7. package/dist/cjs/rules/use-heading-level-in-spotlight-card/index.js +63 -0
  8. package/dist/cjs/rules/use-href-in-link-item/index.js +2 -1
  9. package/dist/cjs/rules/use-href-in-link-item/utils.js +1 -25
  10. package/dist/cjs/rules/use-primitives/index.js +40 -35
  11. package/dist/cjs/rules/use-primitives/transformers/css-to-xcss.js +10 -3
  12. package/dist/cjs/rules/use-primitives/transformers/index.js +8 -1
  13. package/dist/cjs/rules/use-primitives/transformers/styled-component-to-primitive.js +68 -0
  14. package/dist/cjs/rules/use-primitives/utils/convert-ast-object-expression-to-js-object.js +16 -0
  15. package/dist/cjs/rules/use-primitives/utils/find-valid-jsx-usage-to-transform.js +39 -0
  16. package/dist/cjs/rules/use-primitives/utils/find-valid-styled-component-call.js +52 -0
  17. package/dist/cjs/rules/use-primitives/utils/index.js +21 -0
  18. package/dist/cjs/rules/use-primitives/utils/is-valid-css-properties-to-transform.js +38 -0
  19. package/dist/cjs/rules/use-primitives/utils/is-valid-tag-name.js +2 -2
  20. package/dist/cjs/rules/use-primitives/utils/update-jsx-element-name.js +1 -1
  21. package/dist/cjs/rules/utils/get-import-name.js +30 -0
  22. package/dist/es2019/presets/all.codegen.js +2 -1
  23. package/dist/es2019/presets/recommended.codegen.js +2 -1
  24. package/dist/es2019/rules/index.codegen.js +3 -1
  25. package/dist/es2019/rules/use-heading-level-in-spotlight-card/index.js +49 -0
  26. package/dist/es2019/rules/use-href-in-link-item/index.js +3 -2
  27. package/dist/es2019/rules/use-href-in-link-item/utils.js +0 -24
  28. package/dist/es2019/rules/use-primitives/index.js +41 -37
  29. package/dist/es2019/rules/use-primitives/transformers/css-to-xcss.js +9 -6
  30. package/dist/es2019/rules/use-primitives/transformers/index.js +2 -1
  31. package/dist/es2019/rules/use-primitives/transformers/styled-component-to-primitive.js +59 -0
  32. package/dist/es2019/rules/use-primitives/utils/convert-ast-object-expression-to-js-object.js +14 -0
  33. package/dist/es2019/rules/use-primitives/utils/find-valid-jsx-usage-to-transform.js +32 -0
  34. package/dist/es2019/rules/use-primitives/utils/find-valid-styled-component-call.js +47 -0
  35. package/dist/es2019/rules/use-primitives/utils/index.js +3 -0
  36. package/dist/es2019/rules/use-primitives/utils/is-valid-css-properties-to-transform.js +32 -0
  37. package/dist/es2019/rules/use-primitives/utils/is-valid-tag-name.js +1 -1
  38. package/dist/es2019/rules/use-primitives/utils/update-jsx-element-name.js +1 -1
  39. package/dist/es2019/rules/utils/get-import-name.js +24 -0
  40. package/dist/esm/presets/all.codegen.js +2 -1
  41. package/dist/esm/presets/recommended.codegen.js +2 -1
  42. package/dist/esm/rules/index.codegen.js +3 -1
  43. package/dist/esm/rules/use-heading-level-in-spotlight-card/index.js +57 -0
  44. package/dist/esm/rules/use-href-in-link-item/index.js +3 -2
  45. package/dist/esm/rules/use-href-in-link-item/utils.js +0 -24
  46. package/dist/esm/rules/use-primitives/index.js +41 -37
  47. package/dist/esm/rules/use-primitives/transformers/css-to-xcss.js +9 -2
  48. package/dist/esm/rules/use-primitives/transformers/index.js +2 -1
  49. package/dist/esm/rules/use-primitives/transformers/styled-component-to-primitive.js +61 -0
  50. package/dist/esm/rules/use-primitives/utils/convert-ast-object-expression-to-js-object.js +16 -0
  51. package/dist/esm/rules/use-primitives/utils/find-valid-jsx-usage-to-transform.js +34 -0
  52. package/dist/esm/rules/use-primitives/utils/find-valid-styled-component-call.js +47 -0
  53. package/dist/esm/rules/use-primitives/utils/index.js +3 -0
  54. package/dist/esm/rules/use-primitives/utils/is-valid-css-properties-to-transform.js +32 -0
  55. package/dist/esm/rules/use-primitives/utils/is-valid-tag-name.js +1 -1
  56. package/dist/esm/rules/use-primitives/utils/update-jsx-element-name.js +1 -1
  57. package/dist/esm/rules/utils/get-import-name.js +24 -0
  58. package/dist/types/index.codegen.d.ts +2 -0
  59. package/dist/types/presets/all.codegen.d.ts +2 -1
  60. package/dist/types/presets/recommended.codegen.d.ts +2 -1
  61. package/dist/types/rules/index.codegen.d.ts +1 -0
  62. package/dist/types/rules/use-heading-level-in-spotlight-card/index.d.ts +4 -0
  63. package/dist/types/rules/use-href-in-link-item/utils.d.ts +0 -1
  64. package/dist/types/rules/use-primitives/transformers/css-to-xcss.d.ts +2 -1
  65. package/dist/types/rules/use-primitives/transformers/index.d.ts +1 -0
  66. package/dist/types/rules/use-primitives/transformers/styled-component-to-primitive.d.ts +13 -0
  67. package/dist/types/rules/use-primitives/utils/convert-ast-object-expression-to-js-object.d.ts +1 -1
  68. package/dist/types/rules/use-primitives/utils/find-valid-jsx-usage-to-transform.d.ts +7 -0
  69. package/dist/types/rules/use-primitives/utils/find-valid-styled-component-call.d.ts +7 -0
  70. package/dist/types/rules/use-primitives/utils/index.d.ts +3 -0
  71. package/dist/types/rules/use-primitives/utils/is-valid-css-properties-to-transform.d.ts +3 -0
  72. package/dist/types/rules/use-primitives/utils/is-valid-tag-name.d.ts +1 -0
  73. package/dist/types/rules/use-primitives/utils/update-jsx-element-name.d.ts +1 -1
  74. package/dist/types/rules/utils/get-import-name.d.ts +2 -0
  75. package/dist/types-ts4.5/index.codegen.d.ts +2 -0
  76. package/dist/types-ts4.5/presets/all.codegen.d.ts +2 -1
  77. package/dist/types-ts4.5/presets/recommended.codegen.d.ts +2 -1
  78. package/dist/types-ts4.5/rules/index.codegen.d.ts +1 -0
  79. package/dist/types-ts4.5/rules/use-heading-level-in-spotlight-card/index.d.ts +4 -0
  80. package/dist/types-ts4.5/rules/use-href-in-link-item/utils.d.ts +0 -1
  81. package/dist/types-ts4.5/rules/use-primitives/transformers/css-to-xcss.d.ts +2 -1
  82. package/dist/types-ts4.5/rules/use-primitives/transformers/index.d.ts +1 -0
  83. package/dist/types-ts4.5/rules/use-primitives/transformers/styled-component-to-primitive.d.ts +13 -0
  84. package/dist/types-ts4.5/rules/use-primitives/utils/convert-ast-object-expression-to-js-object.d.ts +1 -1
  85. package/dist/types-ts4.5/rules/use-primitives/utils/find-valid-jsx-usage-to-transform.d.ts +7 -0
  86. package/dist/types-ts4.5/rules/use-primitives/utils/find-valid-styled-component-call.d.ts +7 -0
  87. package/dist/types-ts4.5/rules/use-primitives/utils/index.d.ts +3 -0
  88. package/dist/types-ts4.5/rules/use-primitives/utils/is-valid-css-properties-to-transform.d.ts +3 -0
  89. package/dist/types-ts4.5/rules/use-primitives/utils/is-valid-tag-name.d.ts +1 -0
  90. package/dist/types-ts4.5/rules/use-primitives/utils/update-jsx-element-name.d.ts +1 -1
  91. package/dist/types-ts4.5/rules/utils/get-import-name.d.ts +2 -0
  92. package/package.json +1 -1
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isValidCssPropertiesToTransform = void 0;
7
+ var _eslintCodemodUtils = require("eslint-codemod-utils");
8
+ var _cssToXcss = require("../transformers/css-to-xcss");
9
+ var _convertAstObjectExpressionToJsObject = require("./convert-ast-object-expression-to-js-object");
10
+ var isValidCssPropertiesToTransform = exports.isValidCssPropertiesToTransform = function isValidCssPropertiesToTransform(node) {
11
+ var cssObjectExpression = node.arguments[0];
12
+ // Bail on empty object calls
13
+ if (!cssObjectExpression || !(0, _eslintCodemodUtils.isNodeOfType)(cssObjectExpression, 'ObjectExpression')) {
14
+ return false;
15
+ }
16
+ var cssObject = (0, _convertAstObjectExpressionToJsObject.convertASTObjectExpressionToJSObject)(cssObjectExpression);
17
+ // Bail if there are less or more than 1 styles defined
18
+ if (!cssObject || Object.keys(cssObject).length !== 1) {
19
+ return false;
20
+ }
21
+
22
+ // NOTE: Our approach with this lint rule is to strictly whitelist css properties we can map.
23
+ // It means we have to provide mappings for everything (e.g. `display: block`).
24
+ // However, from a maker's experience, it's much better that the rule doesn't report (if we miss a mapping)
25
+ // than the rule reporting on things that can't be mapped.
26
+ var containsOnlyValidStyles = Object.keys(cssObject).every(function (styleProperty) {
27
+ var styleValue = cssObject[styleProperty];
28
+ return _cssToXcss.supportedStylesMap[styleProperty] &&
29
+ // Is the key something we can map
30
+ _cssToXcss.supportedStylesMap[styleProperty][styleValue] // Is the value something we can map
31
+ ;
32
+ });
33
+
34
+ if (!containsOnlyValidStyles) {
35
+ return false;
36
+ }
37
+ return true;
38
+ };
@@ -3,8 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.isValidTagName = void 0;
7
- var validPrimitiveElements = new Set(['div']);
6
+ exports.validPrimitiveElements = exports.isValidTagName = void 0;
7
+ var validPrimitiveElements = exports.validPrimitiveElements = new Set(['div']);
8
8
  var isValidTagName = exports.isValidTagName = function isValidTagName(node) {
9
9
  if (node.openingElement.name.type !== 'JSXIdentifier') {
10
10
  return false;
@@ -12,5 +12,5 @@ var updateJSXElementName = exports.updateJSXElementName = function updateJSXElem
12
12
  var newClosingElement = closingElement &&
13
13
  // Self closing tags, like `<div />` don't need to have the closing tag updated
14
14
  fixer.replaceText(closingElement.name, (0, _eslintCodemodUtils.jsxIdentifier)(newName).toString());
15
- return [newOpeningElement, newClosingElement];
15
+ return [newOpeningElement, newClosingElement || undefined];
16
16
  };
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getImportName = void 0;
7
+ var _eslintCodemodUtils = require("eslint-codemod-utils");
8
+ var getImportName = exports.getImportName = function getImportName(scope, packageName, componentName) {
9
+ var traversingScope = scope;
10
+ var matchedVariable;
11
+ while (traversingScope && traversingScope.type !== 'global') {
12
+ matchedVariable = traversingScope.variables.find(function (variable) {
13
+ var _variable$defs;
14
+ var def = (_variable$defs = variable.defs) === null || _variable$defs === void 0 ? void 0 : _variable$defs[0];
15
+ if (!def || !def.node || !(0, _eslintCodemodUtils.isNodeOfType)(def.node, 'ImportSpecifier') || !def.parent || !(0, _eslintCodemodUtils.isNodeOfType)(def.parent, 'ImportDeclaration')) {
16
+ return;
17
+ }
18
+ return def.parent.source.value === packageName && def.node.imported.name === componentName;
19
+ });
20
+ if (matchedVariable) {
21
+ break;
22
+ }
23
+ traversingScope = traversingScope.upper;
24
+ }
25
+ if (!matchedVariable) {
26
+ return null;
27
+ } else {
28
+ return matchedVariable.defs[0].node.local.name;
29
+ }
30
+ };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * THIS FILE WAS CREATED VIA CODEGEN DO NOT MODIFY {@see http://go/af-codegen}
3
- * @codegen <<SignedSource::594898d8c5dc8b9a5610d62e7f300a53>>
3
+ * @codegen <<SignedSource::37a857efd0971ab26170d5f1f526279b>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  export default {
@@ -22,6 +22,7 @@ export default {
22
22
  '@atlaskit/design-system/no-unsupported-drag-and-drop-libraries': 'error',
23
23
  '@atlaskit/design-system/prefer-primitives': 'warn',
24
24
  '@atlaskit/design-system/use-drawer-label': 'warn',
25
+ '@atlaskit/design-system/use-heading-level-in-spotlight-card': 'warn',
25
26
  '@atlaskit/design-system/use-href-in-link-item': 'warn',
26
27
  '@atlaskit/design-system/use-primitives': 'warn',
27
28
  '@atlaskit/design-system/use-visually-hidden': 'error'
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * THIS FILE WAS CREATED VIA CODEGEN DO NOT MODIFY {@see http://go/af-codegen}
3
- * @codegen <<SignedSource::240c678d4f10d97618541f35d79e1c71>>
3
+ * @codegen <<SignedSource::b3bce8efb2db1fed84370af608ca0d4c>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  export default {
@@ -17,6 +17,7 @@ export default {
17
17
  '@atlaskit/design-system/no-unsafe-design-token-usage': 'error',
18
18
  '@atlaskit/design-system/no-unsupported-drag-and-drop-libraries': 'error',
19
19
  '@atlaskit/design-system/use-drawer-label': 'warn',
20
+ '@atlaskit/design-system/use-heading-level-in-spotlight-card': 'warn',
20
21
  '@atlaskit/design-system/use-href-in-link-item': 'warn',
21
22
  '@atlaskit/design-system/use-visually-hidden': 'error'
22
23
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * THIS FILE WAS CREATED VIA CODEGEN DO NOT MODIFY {@see http://go/af-codegen}
3
- * @codegen <<SignedSource::8927e5adaec5639c9712dbfb26968de4>>
3
+ * @codegen <<SignedSource::e2cc357c90120dbbd89b2928dba968c0>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  import consistentCssPropUsage from './consistent-css-prop-usage';
@@ -19,6 +19,7 @@ import noUnsafeDesignTokenUsage from './no-unsafe-design-token-usage';
19
19
  import noUnsupportedDragAndDropLibraries from './no-unsupported-drag-and-drop-libraries';
20
20
  import preferPrimitives from './prefer-primitives';
21
21
  import useDrawerLabel from './use-drawer-label';
22
+ import useHeadingLevelInSpotlightCard from './use-heading-level-in-spotlight-card';
22
23
  import useHrefInLinkItem from './use-href-in-link-item';
23
24
  import usePrimitives from './use-primitives';
24
25
  import useVisuallyHidden from './use-visually-hidden';
@@ -39,6 +40,7 @@ export default {
39
40
  'no-unsupported-drag-and-drop-libraries': noUnsupportedDragAndDropLibraries,
40
41
  'prefer-primitives': preferPrimitives,
41
42
  'use-drawer-label': useDrawerLabel,
43
+ 'use-heading-level-in-spotlight-card': useHeadingLevelInSpotlightCard,
42
44
  'use-href-in-link-item': useHrefInLinkItem,
43
45
  'use-primitives': usePrimitives,
44
46
  'use-visually-hidden': useVisuallyHidden
@@ -0,0 +1,49 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+
3
+ import { isNodeOfType } from 'eslint-codemod-utils';
4
+ import { createLintRule } from '../utils/create-rule';
5
+ import { getImportName } from '../utils/get-import-name';
6
+ export const headingLevelRequiredSuggestionText = 'Add a `headingLevel` that is of a contextually relevant level.';
7
+ const rule = createLintRule({
8
+ meta: {
9
+ name: 'use-heading-level-in-spotlight-card',
10
+ type: 'suggestion',
11
+ fixable: 'code',
12
+ docs: {
13
+ description: 'Inform developers of eventual requirement of `headingLevel` prop in `SpotlightCard` component. The heading level should be the appropriate level according to the surrounding context.',
14
+ recommended: true,
15
+ severity: 'warn'
16
+ },
17
+ messages: {
18
+ headingLevelRequired: headingLevelRequiredSuggestionText
19
+ }
20
+ },
21
+ create(context) {
22
+ return {
23
+ JSXElement(node) {
24
+ if (!isNodeOfType(node, 'JSXElement')) {
25
+ return;
26
+ }
27
+ if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) {
28
+ return;
29
+ }
30
+
31
+ // Get the name of the SpotlightCard import
32
+ const spotlightCardImportName = getImportName(context.getScope(), '@atlaskit/onboarding', 'SpotlightCard');
33
+ if (node.openingElement.name.name === spotlightCardImportName) {
34
+ // and if `heading` exists and `headingLevel` prop does not exist
35
+ const spotlightCardProps = node.openingElement.attributes.filter(attr => isNodeOfType(attr, 'JSXAttribute')).filter(attr => attr.name.type === 'JSXIdentifier');
36
+ const heading = spotlightCardProps.find(attr => attr.name.name === 'heading');
37
+ const headingLevel = spotlightCardProps.find(attr => attr.name.name === 'headingLevel');
38
+ if (heading && !headingLevel) {
39
+ context.report({
40
+ node: node,
41
+ messageId: 'headingLevelRequired'
42
+ });
43
+ }
44
+ }
45
+ }
46
+ };
47
+ }
48
+ });
49
+ export default rule;
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { isNodeOfType } from 'eslint-codemod-utils';
4
4
  import { createLintRule } from '../utils/create-rule';
5
- import { getLinkItemImportName, getUniqueButtonItemName, hasImportOfName, hrefHasInvalidValue, insertButtonItemDefaultImport, insertButtonItemImport } from './utils';
5
+ import { getImportName } from '../utils/get-import-name';
6
+ import { getUniqueButtonItemName, hasImportOfName, hrefHasInvalidValue, insertButtonItemDefaultImport, insertButtonItemImport } from './utils';
6
7
  export const hrefRequiredSuggestionText = 'Convert LinkItem to ButtonItem';
7
8
  const rule = createLintRule({
8
9
  meta: {
@@ -46,7 +47,7 @@ const rule = createLintRule({
46
47
  }
47
48
 
48
49
  // Get the name of the LinkItem import
49
- const linkItemImportName = customDefaultLinkItemSpecifier || getLinkItemImportName(context.getScope());
50
+ const linkItemImportName = customDefaultLinkItemSpecifier || getImportName(context.getScope(), '@atlaskit/menu', 'LinkItem');
50
51
  if (node.openingElement.name.name === linkItemImportName) {
51
52
  // and if href prop does not exist
52
53
  const linkProps = node.openingElement.attributes.filter(attr => isNodeOfType(attr, 'JSXAttribute')).filter(attr => attr.name.type === 'JSXIdentifier');
@@ -2,30 +2,6 @@
2
2
 
3
3
  import { isNodeOfType } from 'eslint-codemod-utils';
4
4
  import { findIdentifierInParentScope } from '../utils/find-in-parent';
5
- export const getLinkItemImportName = scope => {
6
- let traversingScope = scope;
7
- let matchedVariable;
8
- while (traversingScope && traversingScope.type !== 'global') {
9
- matchedVariable = traversingScope.variables.find(variable => {
10
- var _variable$defs;
11
- const def = (_variable$defs = variable.defs) === null || _variable$defs === void 0 ? void 0 : _variable$defs[0];
12
- if (!def || !(def !== null && def !== void 0 && def.node) || !isNodeOfType(def.node, 'ImportSpecifier') || !def.parent || !isNodeOfType(def.parent, 'ImportDeclaration')) {
13
- return;
14
- }
15
- return def.parent.source.value === '@atlaskit/menu' && def.node.imported.name === 'LinkItem';
16
- });
17
- if (matchedVariable) {
18
- break;
19
- }
20
- traversingScope = traversingScope.upper;
21
- }
22
- if (!matchedVariable) {
23
- return null;
24
- } else {
25
- var _matchedVariable$defs;
26
- return (_matchedVariable$defs = matchedVariable.defs) === null || _matchedVariable$defs === void 0 ? void 0 : _matchedVariable$defs[0].node.local.name;
27
- }
28
- };
29
5
  const invalidHrefValues = ['', '#', null, undefined];
30
6
  export const hrefHasInvalidValue = (scope, href) => {
31
7
  // If doesn't exist,
@@ -1,8 +1,12 @@
1
1
  import { getIdentifierInParentScope, isNodeOfType } from 'eslint-codemod-utils';
2
+ import assign from 'lodash/assign';
2
3
  import { createLintRule } from '../utils/create-rule';
3
- import { jsxElementToBoxTransformer, supportedStylesMap } from './transformers';
4
- import { containsOnlySupportedAttrs, convertASTObjectExpressionToJSObject, getAttributeValueIdentifier, getFunctionArgumentAtPos, getJSXAttributeByName, getVariableDefinitionValue, getVariableUsagesCount, isFunctionNamed, isValidTagName } from './utils';
4
+ import { jsxElementToBoxTransformer, styledComponentToPrimitive } from './transformers';
5
+ import { containsOnlySupportedAttrs, findValidJsxUsageToTransform, findValidStyledComponentCall, getAttributeValueIdentifier, getJSXAttributeByName, getVariableDefinitionValue, getVariableUsagesCount, isFunctionNamed, isValidCssPropertiesToTransform, isValidTagName } from './utils';
5
6
  const boxDocsUrl = 'https://atlassian.design/components/primitives/box';
7
+ const defaultConfig = {
8
+ preview: false
9
+ };
6
10
  const rule = createLintRule({
7
11
  meta: {
8
12
  name: 'use-primitives',
@@ -15,11 +19,43 @@ const rule = createLintRule({
15
19
  severity: 'warn'
16
20
  },
17
21
  messages: {
18
- preferPrimitivesBox: `This "{{element}}" may be able to be replaced with a "Box". See ${boxDocsUrl} for guidance.`
22
+ preferPrimitivesBox: `This element can be replaced with a "Box" primitive. See ${boxDocsUrl} for additional guidance.`
19
23
  }
20
24
  },
21
25
  create(context) {
26
+ const mergedConfig = assign({}, defaultConfig, context.options[0]);
22
27
  return {
28
+ // transforms styled.<html>(...) usages
29
+ CallExpression(node) {
30
+ if (!mergedConfig.preview) {
31
+ return;
32
+ }
33
+ if (!isNodeOfType(node, 'CallExpression')) {
34
+ return;
35
+ }
36
+ const styledComponentVariableRef = findValidStyledComponentCall(node);
37
+ if (!styledComponentVariableRef || !isNodeOfType(styledComponentVariableRef.id, 'Identifier') || !isValidCssPropertiesToTransform(node)) {
38
+ return;
39
+ }
40
+ const styledComponentJsxRef = findValidJsxUsageToTransform(styledComponentVariableRef.id.name, context.getScope());
41
+ if (!styledComponentJsxRef) {
42
+ return;
43
+ }
44
+
45
+ // if we have both references at this point then we can offer a fix \o/
46
+ context.report({
47
+ node: styledComponentVariableRef,
48
+ messageId: 'preferPrimitivesBox',
49
+ suggest: [{
50
+ desc: `Convert ${styledComponentVariableRef.id.name} to Box`,
51
+ fix: styledComponentToPrimitive({
52
+ stylesRef: styledComponentVariableRef,
53
+ jsxRef: styledComponentJsxRef
54
+ }, context)
55
+ }]
56
+ });
57
+ },
58
+ // transforms <div css={...}> usages
23
59
  JSXOpeningElement(node) {
24
60
  if (!isNodeOfType(node, 'JSXOpeningElement')) {
25
61
  return;
@@ -91,42 +127,10 @@ const shouldSuggestBox = (node, context
91
127
  // Find where `cssVariableName` is defined. We're looking for `const myStyles = css({...})`
92
128
  const cssVariableDefinition = getIdentifierInParentScope(context.getScope(), cssVariableName);
93
129
  const cssVariableValue = getVariableDefinitionValue(cssVariableDefinition);
94
-
95
130
  // Check if `cssVariableValue` is a function called `css()`
96
- if (!isFunctionNamed(cssVariableValue, 'css')) {
97
- return false;
98
- }
99
-
100
- // get the `{}` in `css({})`
101
- // Zero indexed
102
- const cssObjectExpression = getFunctionArgumentAtPos(cssVariableValue, 0);
103
-
104
- // Bail on empty `css()` calls
105
- if (!cssObjectExpression) {
106
- return false;
107
- }
108
- const cssObject = convertASTObjectExpressionToJSObject(cssObjectExpression);
109
-
110
- // Bail if there are less or more than 1 styles defined
111
- if (Object.keys(cssObject).length !== 1) {
112
- return false;
113
- }
114
-
115
- // NOTE: Our approach with this lint rule is to strictly whitelist css properties we can map.
116
- // It means we have to provide mappings for everything (e.g. `display: block`).
117
- // However, from a maker's experience, it's much better that the rule doesn't report (if we miss a mapping)
118
- // than the rule reporting on things that can't be mapped.
119
- const containsOnlyValidStyles = Object.keys(cssObject).every(styleProperty => {
120
- const styleValue = cssObject[styleProperty];
121
- return supportedStylesMap[styleProperty] &&
122
- // Is the key something we can map
123
- supportedStylesMap[styleProperty][styleValue] // Is the value something we can map
124
- ;
125
- });
126
-
127
- if (!containsOnlyValidStyles) {
131
+ if (!cssVariableValue || !isFunctionNamed(cssVariableValue, 'css')) {
128
132
  return false;
129
133
  }
130
- return true;
134
+ return isValidCssPropertiesToTransform(cssVariableValue.node.init);
131
135
  };
132
136
  export default rule;
@@ -28,11 +28,14 @@ export const cssToXcssTransformer = (node, context, fixer) => {
28
28
  const cssObjectExpression = getFunctionArgumentAtPos(cssVariableValue, 0);
29
29
  return [
30
30
  // Update `css` function name to `xcss`.
31
- fixer.replaceText(cssVariableValue.node.init.callee, identifier('xcss').toString()),
32
- // Update css object values to xcss values. e.g. `'8px'` -> `'space.100'`
33
- // Note: `properties` in this context is a group of AST nodes that make up a key/value pair in an object.
34
- // e.g. `padding: '8px'`. For clarity, it's renamed to `entry` inside the `.map()`.
35
- ...cssObjectExpression.properties.map(entry => {
31
+ fixer.replaceText(cssVariableValue.node.init.callee, identifier('xcss').toString()), ...styledObjectToXcssTokens(cssObjectExpression, fixer)];
32
+ };
33
+
34
+ // Update css object values to xcss values. e.g. `'8px'` -> `'space.100'`
35
+ // Note: `properties` in this context is a group of AST nodes that make up a key/value pair in an object.
36
+ // e.g. `padding: '8px'`. For clarity, it's renamed to `entry` inside the `.map()`.
37
+ export const styledObjectToXcssTokens = (styles, fixer) => {
38
+ return styles.properties.map(entry => {
36
39
  if (!isNodeOfType(entry, 'Property')) {
37
40
  return;
38
41
  }
@@ -47,7 +50,7 @@ export const cssToXcssTransformer = (node, context, fixer) => {
47
50
  return;
48
51
  }
49
52
  return fixer.replaceText(entry.value, literal(`'${supportedStylesMap[entry.key.name][value]}'`).toString());
50
- })];
53
+ });
51
54
  };
52
55
  export const spaceTokenMap = {
53
56
  '0px': 'space.0',
@@ -1,2 +1,3 @@
1
1
  export { cssToXcssTransformer, supportedStylesMap, spaceTokenMap } from './css-to-xcss';
2
- export { jsxElementToBoxTransformer } from './jsx-element-to-box';
2
+ export { jsxElementToBoxTransformer } from './jsx-element-to-box';
3
+ export { styledComponentToPrimitive } from './styled-component-to-primitive';
@@ -0,0 +1,59 @@
1
+ import { isNodeOfType } from 'eslint-codemod-utils';
2
+ import { updateJSXElementName, upsertImportDeclaration } from '../utils';
3
+ import { styledObjectToXcssTokens } from './css-to-xcss';
4
+ /**
5
+ * All required validation steps have been taken care of before this
6
+ * transformer is called, so it just goes ahead providing all necessary fixes
7
+ */
8
+ export const styledComponentToPrimitive = ({
9
+ stylesRef,
10
+ jsxRef
11
+ }, context) => {
12
+ return fixer => {
13
+ // generates the new variable name: MyComponent -> myComponentStyles
14
+ const calculatedStylesVariableName = isNodeOfType(stylesRef.id, 'Identifier') && `${stylesRef.id.name.replace(stylesRef.id.name[0], stylesRef.id.name[0].toLowerCase())}Styles`;
15
+ if (!calculatedStylesVariableName) {
16
+ return [];
17
+ }
18
+ const importFixes = upsertImportDeclaration({
19
+ packageName: '@atlaskit/primitives',
20
+ specifiers: ['Box', 'xcss']
21
+ }, context, fixer);
22
+ const stylesFixes = convertStyledComponentToXcss(stylesRef, calculatedStylesVariableName, fixer);
23
+ const jsxFixes = convertJsxCallSite(jsxRef, calculatedStylesVariableName, fixer);
24
+ return [importFixes, ...stylesFixes, ...jsxFixes].filter(fix => Boolean(fix)); // Some of the transformers can return arrays with undefined, so filter them out
25
+ };
26
+ };
27
+
28
+ const convertStyledComponentToXcss = (stylesRef, newStylesVariableName, fixer) => {
29
+ const fixes = [];
30
+
31
+ // renames the variable from MyComponent to myComponentStyles
32
+ fixes.push(fixer.replaceText(stylesRef.id, newStylesVariableName));
33
+
34
+ // renames the function call from styled.<tag> to xcss
35
+ if (stylesRef.init && isNodeOfType(stylesRef.init, 'CallExpression')) {
36
+ fixes.push(fixer.replaceText(stylesRef.init.callee, 'xcss'));
37
+ }
38
+
39
+ // converts CSS values to XCSS-compatible tokens
40
+ if (stylesRef.init && isNodeOfType(stylesRef.init, 'CallExpression')) {
41
+ const objectExpression = stylesRef.init.arguments[0];
42
+ if (isNodeOfType(objectExpression, 'ObjectExpression')) {
43
+ fixes.push(...styledObjectToXcssTokens(objectExpression, fixer));
44
+ }
45
+ }
46
+ return fixes;
47
+ };
48
+ const convertJsxCallSite = (jsxRef, newStylesVariableName, fixer) => {
49
+ const fixes = [];
50
+
51
+ // renames the JSX call site
52
+ if (isNodeOfType(jsxRef.parent, 'JSXElement')) {
53
+ fixes.push(...updateJSXElementName(jsxRef.parent, 'Box', fixer));
54
+ }
55
+
56
+ // adds xcss prop
57
+ fixes.push(fixer.insertTextAfter(jsxRef.name, ` xcss={${newStylesVariableName}}`));
58
+ return fixes;
59
+ };
@@ -7,6 +7,20 @@ import { isNodeOfType } from 'eslint-codemod-utils';
7
7
  */
8
8
  export const convertASTObjectExpressionToJSObject = styles => {
9
9
  const styleObj = {};
10
+
11
+ // if we see any spread props we stop and return false to indicate this is unsupported
12
+ if (!styles.properties.every(prop => isNodeOfType(prop, 'Property'))) {
13
+ return false;
14
+ }
15
+
16
+ // TODO: We need to harden this logic.
17
+ // It currently generates a false positive for:
18
+ // styled.div({
19
+ // marginTop: "0px",
20
+ // marginBottom: token("space.100", "8px"),
21
+ // })
22
+ // as the value for `marginBottom` is not a string, so it is just skipped
23
+ // from the resulting map and this causes the rule to trigger when it shouldn't
10
24
  styles.properties.forEach(prop => {
11
25
  if (!isNodeOfType(prop, 'Property')) {
12
26
  return;
@@ -0,0 +1,32 @@
1
+ import { isNodeOfType } from 'eslint-codemod-utils';
2
+
3
+ /**
4
+ * Given a component name finds its JSX usages and performs some
5
+ * additional validations to ensure transformation can be done correctly
6
+ */
7
+ export const findValidJsxUsageToTransform = (componentName, scope) => {
8
+ const variableDeclaration = scope.variables.find(v => v.name === componentName);
9
+ if (!variableDeclaration) {
10
+ return;
11
+ }
12
+
13
+ // length here should be exactly 2 to indicate only two references:
14
+ // one being the variable declaration itself
15
+ // second being the JSX call site
16
+ // we might consider handling multiple local JSX call sites in the future
17
+ // but "this is good enough for now"™️
18
+ if (variableDeclaration.references.length !== 2) {
19
+ return;
20
+ }
21
+ const jsxUsage = variableDeclaration.references[1].identifier;
22
+ if (!isNodeOfType(jsxUsage, 'JSXIdentifier')) {
23
+ return;
24
+ }
25
+ const jsxOpeningElement = jsxUsage.parent;
26
+ // we could relatively easily support some safe attributes like
27
+ // "id" or "testId" but support will be expanded as we go
28
+ if (!isNodeOfType(jsxOpeningElement, 'JSXOpeningElement') || jsxOpeningElement.attributes.length > 0) {
29
+ return;
30
+ }
31
+ return jsxOpeningElement;
32
+ };
@@ -0,0 +1,47 @@
1
+ import { closestOfType, isNodeOfType } from 'eslint-codemod-utils';
2
+ import { validPrimitiveElements } from './is-valid-tag-name';
3
+
4
+ /**
5
+ * returns a variable reference if preconditions are favourable for
6
+ * the transformation to proceed, undefined otherwise.
7
+ */
8
+
9
+ export const findValidStyledComponentCall = node => {
10
+ // halts unless we are dealing with a styled component
11
+ if (!isStyledCallExpression(node)) {
12
+ return;
13
+ }
14
+ // halts if the component is being exported directly
15
+ if (closestOfType(node, 'ExportNamedDeclaration')) {
16
+ return;
17
+ }
18
+ const styledComponentVariableRef = node.parent;
19
+ // halts if the styled component is not assigned to a variable immediately
20
+ if (!isNodeOfType(styledComponentVariableRef, 'VariableDeclarator')) {
21
+ return;
22
+ }
23
+ return styledComponentVariableRef;
24
+ };
25
+
26
+ /**
27
+ * Some verbose precondition checks but all it does is check
28
+ * a call expression is of form `styled.div` or `styled2.div`
29
+ *
30
+ * In the future it could be enhanced to double check `styled` and `styled2`
31
+ * are Compiled imports but as is should work for the majority of use cases
32
+ */
33
+ const isStyledCallExpression = call => {
34
+ if (!isNodeOfType(call, 'CallExpression')) {
35
+ return false;
36
+ }
37
+ if (!isNodeOfType(call.callee, 'MemberExpression')) {
38
+ return false;
39
+ }
40
+ if (!isNodeOfType(call.callee.object, 'Identifier') || !isNodeOfType(call.callee.property, 'Identifier')) {
41
+ return false;
42
+ }
43
+ if (/^styled2?$/.test(call.callee.object.name) && validPrimitiveElements.has(call.callee.property.name)) {
44
+ return true;
45
+ }
46
+ return false;
47
+ };
@@ -1,11 +1,14 @@
1
1
  export { containsOnlySupportedAttrs } from './contains-only-supported-attrs';
2
2
  export { convertASTObjectExpressionToJSObject } from './convert-ast-object-expression-to-js-object';
3
+ export { findValidJsxUsageToTransform } from './find-valid-jsx-usage-to-transform';
4
+ export { findValidStyledComponentCall } from './find-valid-styled-component-call';
3
5
  export { getAttributeValueIdentifier } from './get-attribute-value-identifier';
4
6
  export { getFunctionArgumentAtPos } from './get-function-argument-at-pos';
5
7
  export { getJSXAttributeByName } from './get-jsx-attribute-by-name';
6
8
  export { getVariableDefinitionValue } from './get-variable-definition-value';
7
9
  export { getVariableUsagesCount } from './get-variable-usage-count';
8
10
  export { isFunctionNamed } from './is-function-named';
11
+ export { isValidCssPropertiesToTransform } from './is-valid-css-properties-to-transform';
9
12
  export { isValidTagName } from './is-valid-tag-name';
10
13
  export { updateJSXAttributeByName } from './update-jsx-attribute-by-name';
11
14
  export { updateJSXElementName } from './update-jsx-element-name';
@@ -0,0 +1,32 @@
1
+ import { isNodeOfType } from 'eslint-codemod-utils';
2
+ import { supportedStylesMap } from '../transformers/css-to-xcss';
3
+ import { convertASTObjectExpressionToJSObject } from './convert-ast-object-expression-to-js-object';
4
+ export const isValidCssPropertiesToTransform = node => {
5
+ const cssObjectExpression = node.arguments[0];
6
+ // Bail on empty object calls
7
+ if (!cssObjectExpression || !isNodeOfType(cssObjectExpression, 'ObjectExpression')) {
8
+ return false;
9
+ }
10
+ const cssObject = convertASTObjectExpressionToJSObject(cssObjectExpression);
11
+ // Bail if there are less or more than 1 styles defined
12
+ if (!cssObject || Object.keys(cssObject).length !== 1) {
13
+ return false;
14
+ }
15
+
16
+ // NOTE: Our approach with this lint rule is to strictly whitelist css properties we can map.
17
+ // It means we have to provide mappings for everything (e.g. `display: block`).
18
+ // However, from a maker's experience, it's much better that the rule doesn't report (if we miss a mapping)
19
+ // than the rule reporting on things that can't be mapped.
20
+ const containsOnlyValidStyles = Object.keys(cssObject).every(styleProperty => {
21
+ const styleValue = cssObject[styleProperty];
22
+ return supportedStylesMap[styleProperty] &&
23
+ // Is the key something we can map
24
+ supportedStylesMap[styleProperty][styleValue] // Is the value something we can map
25
+ ;
26
+ });
27
+
28
+ if (!containsOnlyValidStyles) {
29
+ return false;
30
+ }
31
+ return true;
32
+ };
@@ -1,4 +1,4 @@
1
- const validPrimitiveElements = new Set(['div']);
1
+ export const validPrimitiveElements = new Set(['div']);
2
2
  export const isValidTagName = node => {
3
3
  if (node.openingElement.name.type !== 'JSXIdentifier') {
4
4
  return false;
@@ -8,5 +8,5 @@ export const updateJSXElementName = (node, newName, fixer) => {
8
8
  const newClosingElement = closingElement &&
9
9
  // Self closing tags, like `<div />` don't need to have the closing tag updated
10
10
  fixer.replaceText(closingElement.name, jsxIdentifier(newName).toString());
11
- return [newOpeningElement, newClosingElement];
11
+ return [newOpeningElement, newClosingElement || undefined];
12
12
  };
@@ -0,0 +1,24 @@
1
+ import { isNodeOfType } from 'eslint-codemod-utils';
2
+ export const getImportName = (scope, packageName, componentName) => {
3
+ let traversingScope = scope;
4
+ let matchedVariable;
5
+ while (traversingScope && traversingScope.type !== 'global') {
6
+ matchedVariable = traversingScope.variables.find(variable => {
7
+ var _variable$defs;
8
+ const def = (_variable$defs = variable.defs) === null || _variable$defs === void 0 ? void 0 : _variable$defs[0];
9
+ if (!def || !def.node || !isNodeOfType(def.node, 'ImportSpecifier') || !def.parent || !isNodeOfType(def.parent, 'ImportDeclaration')) {
10
+ return;
11
+ }
12
+ return def.parent.source.value === packageName && def.node.imported.name === componentName;
13
+ });
14
+ if (matchedVariable) {
15
+ break;
16
+ }
17
+ traversingScope = traversingScope.upper;
18
+ }
19
+ if (!matchedVariable) {
20
+ return null;
21
+ } else {
22
+ return matchedVariable.defs[0].node.local.name;
23
+ }
24
+ };