@atlaskit/eslint-plugin-design-system 13.9.0 → 13.11.0

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 (80) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +59 -57
  3. package/dist/cjs/presets/all-flat.codegen.js +3 -1
  4. package/dist/cjs/presets/all.codegen.js +3 -1
  5. package/dist/cjs/presets/recommended-flat.codegen.js +3 -1
  6. package/dist/cjs/presets/recommended.codegen.js +3 -1
  7. package/dist/cjs/rules/index.codegen.js +5 -1
  8. package/dist/cjs/rules/no-html-code/index.js +40 -0
  9. package/dist/cjs/rules/no-html-code/node-types/index.js +19 -0
  10. package/dist/cjs/rules/no-html-code/node-types/jsx-element/index.js +114 -0
  11. package/dist/cjs/rules/no-html-code/node-types/styled-component/index.js +42 -0
  12. package/dist/cjs/rules/no-html-code/node-types/supported.js +66 -0
  13. package/dist/cjs/rules/no-html-heading/index.js +40 -0
  14. package/dist/cjs/rules/no-html-heading/node-types/index.js +19 -0
  15. package/dist/cjs/rules/no-html-heading/node-types/jsx-element/index.js +136 -0
  16. package/dist/cjs/rules/no-html-heading/node-types/styled-component/index.js +42 -0
  17. package/dist/cjs/rules/no-html-heading/node-types/supported.js +82 -0
  18. package/dist/es2019/presets/all-flat.codegen.js +3 -1
  19. package/dist/es2019/presets/all.codegen.js +3 -1
  20. package/dist/es2019/presets/recommended-flat.codegen.js +3 -1
  21. package/dist/es2019/presets/recommended.codegen.js +3 -1
  22. package/dist/es2019/rules/index.codegen.js +5 -1
  23. package/dist/es2019/rules/no-html-code/index.js +34 -0
  24. package/dist/es2019/rules/no-html-code/node-types/index.js +2 -0
  25. package/dist/es2019/rules/no-html-code/node-types/jsx-element/index.js +81 -0
  26. package/dist/es2019/rules/no-html-code/node-types/styled-component/index.js +37 -0
  27. package/dist/es2019/rules/no-html-code/node-types/supported.js +56 -0
  28. package/dist/es2019/rules/no-html-heading/index.js +34 -0
  29. package/dist/es2019/rules/no-html-heading/node-types/index.js +2 -0
  30. package/dist/es2019/rules/no-html-heading/node-types/jsx-element/index.js +99 -0
  31. package/dist/es2019/rules/no-html-heading/node-types/styled-component/index.js +37 -0
  32. package/dist/es2019/rules/no-html-heading/node-types/supported.js +72 -0
  33. package/dist/esm/presets/all-flat.codegen.js +3 -1
  34. package/dist/esm/presets/all.codegen.js +3 -1
  35. package/dist/esm/presets/recommended-flat.codegen.js +3 -1
  36. package/dist/esm/presets/recommended.codegen.js +3 -1
  37. package/dist/esm/rules/index.codegen.js +5 -1
  38. package/dist/esm/rules/no-html-code/index.js +34 -0
  39. package/dist/esm/rules/no-html-code/node-types/index.js +2 -0
  40. package/dist/esm/rules/no-html-code/node-types/jsx-element/index.js +105 -0
  41. package/dist/esm/rules/no-html-code/node-types/styled-component/index.js +36 -0
  42. package/dist/esm/rules/no-html-code/node-types/supported.js +57 -0
  43. package/dist/esm/rules/no-html-heading/index.js +34 -0
  44. package/dist/esm/rules/no-html-heading/node-types/index.js +2 -0
  45. package/dist/esm/rules/no-html-heading/node-types/jsx-element/index.js +127 -0
  46. package/dist/esm/rules/no-html-heading/node-types/styled-component/index.js +36 -0
  47. package/dist/esm/rules/no-html-heading/node-types/supported.js +73 -0
  48. package/dist/types/index.codegen.d.ts +18 -0
  49. package/dist/types/presets/all-flat.codegen.d.ts +2 -0
  50. package/dist/types/presets/all.codegen.d.ts +2 -0
  51. package/dist/types/presets/recommended-flat.codegen.d.ts +2 -0
  52. package/dist/types/presets/recommended.codegen.d.ts +2 -0
  53. package/dist/types/rules/index.codegen.d.ts +2 -0
  54. package/dist/types/rules/no-html-code/index.d.ts +3 -0
  55. package/dist/types/rules/no-html-code/node-types/index.d.ts +2 -0
  56. package/dist/types/rules/no-html-code/node-types/jsx-element/index.d.ts +8 -0
  57. package/dist/types/rules/no-html-code/node-types/styled-component/index.d.ts +8 -0
  58. package/dist/types/rules/no-html-code/node-types/supported.d.ts +7 -0
  59. package/dist/types/rules/no-html-heading/index.d.ts +3 -0
  60. package/dist/types/rules/no-html-heading/node-types/index.d.ts +2 -0
  61. package/dist/types/rules/no-html-heading/node-types/jsx-element/index.d.ts +8 -0
  62. package/dist/types/rules/no-html-heading/node-types/styled-component/index.d.ts +8 -0
  63. package/dist/types/rules/no-html-heading/node-types/supported.d.ts +7 -0
  64. package/dist/types-ts4.5/index.codegen.d.ts +18 -0
  65. package/dist/types-ts4.5/presets/all-flat.codegen.d.ts +2 -0
  66. package/dist/types-ts4.5/presets/all.codegen.d.ts +2 -0
  67. package/dist/types-ts4.5/presets/recommended-flat.codegen.d.ts +2 -0
  68. package/dist/types-ts4.5/presets/recommended.codegen.d.ts +2 -0
  69. package/dist/types-ts4.5/rules/index.codegen.d.ts +2 -0
  70. package/dist/types-ts4.5/rules/no-html-code/index.d.ts +3 -0
  71. package/dist/types-ts4.5/rules/no-html-code/node-types/index.d.ts +2 -0
  72. package/dist/types-ts4.5/rules/no-html-code/node-types/jsx-element/index.d.ts +8 -0
  73. package/dist/types-ts4.5/rules/no-html-code/node-types/styled-component/index.d.ts +8 -0
  74. package/dist/types-ts4.5/rules/no-html-code/node-types/supported.d.ts +7 -0
  75. package/dist/types-ts4.5/rules/no-html-heading/index.d.ts +3 -0
  76. package/dist/types-ts4.5/rules/no-html-heading/node-types/index.d.ts +2 -0
  77. package/dist/types-ts4.5/rules/no-html-heading/node-types/jsx-element/index.d.ts +8 -0
  78. package/dist/types-ts4.5/rules/no-html-heading/node-types/styled-component/index.d.ts +8 -0
  79. package/dist/types-ts4.5/rules/no-html-heading/node-types/supported.d.ts +7 -0
  80. package/package.json +2 -2
@@ -0,0 +1,56 @@
1
+ import { isNodeOfType } from 'eslint-codemod-utils';
2
+ import * as ast from '../../../ast-nodes';
3
+ const supportedElements = [{
4
+ name: 'code'
5
+ }];
6
+
7
+ /**
8
+ * Determines if the given JSX element is a supported element to lint with this rule.
9
+ */
10
+ export function isSupportedForLint(jsxNode, elementName) {
11
+ if (!isNodeOfType(jsxNode, 'JSXElement')) {
12
+ return false;
13
+ }
14
+
15
+ // Allow passing in the element name because the jsxNode doesn't
16
+ // represent the element name with styled components
17
+ const elName = elementName || ast.JSXElement.getName(jsxNode);
18
+ if (!elName) {
19
+ return false;
20
+ }
21
+
22
+ // Only check native HTML elements, not components
23
+ if (elName[0] !== elName[0].toLowerCase()) {
24
+ return false;
25
+ }
26
+ let supportedElement = supportedElements.find(({
27
+ name
28
+ }) => name === elName);
29
+ if (!supportedElement) {
30
+ supportedElement = supportedElements.find(({
31
+ name
32
+ }) => name === '*');
33
+ }
34
+ if (!supportedElement) {
35
+ return false;
36
+ }
37
+
38
+ // Check if the element has any attributes that are not supported
39
+ const attributes = ast.JSXElement.getAttributes(jsxNode);
40
+ if (supportedElement.attributes && !supportedElement.attributes.every(({
41
+ name,
42
+ values
43
+ }) => {
44
+ return attributes.some(attribute => {
45
+ if (attribute.type === 'JSXSpreadAttribute') {
46
+ return false;
47
+ }
48
+ const isMatchingName = attribute.name.name === name;
49
+ const isMatchingValues = values && attribute.value && attribute.value.type === 'Literal' && typeof attribute.value.value === 'string' && (values === null || values === void 0 ? void 0 : values.includes(attribute.value.value));
50
+ return isMatchingName && isMatchingValues;
51
+ });
52
+ })) {
53
+ return false;
54
+ }
55
+ return true;
56
+ }
@@ -0,0 +1,34 @@
1
+ import { createLintRule } from '../utils/create-rule';
2
+ import { JSXElement, StyledComponent } from './node-types';
3
+ const rule = createLintRule({
4
+ meta: {
5
+ name: 'no-html-heading',
6
+ type: 'suggestion',
7
+ hasSuggestions: true,
8
+ docs: {
9
+ description: 'Discourage direct usage of HTML heading elements in favor of Atlassian Design System heading components.',
10
+ recommended: true,
11
+ severity: 'warn'
12
+ },
13
+ messages: {
14
+ noHtmlHeading: `This <{{ name }}> should be replaced with a heading component from the Atlassian Design System. ADS headings include ensure accessible implementations and provide access to ADS styling features like design tokens.`
15
+ }
16
+ },
17
+ create(context) {
18
+ return {
19
+ // transforms styled.<heading>(...) usages
20
+ CallExpression(node) {
21
+ StyledComponent.lint(node, {
22
+ context
23
+ });
24
+ },
25
+ // transforms <heading css={...}> usages
26
+ JSXElement(node) {
27
+ JSXElement.lint(node, {
28
+ context
29
+ });
30
+ }
31
+ };
32
+ }
33
+ });
34
+ export default rule;
@@ -0,0 +1,2 @@
1
+ export { StyledComponent } from './styled-component';
2
+ export { JSXElement } from './jsx-element';
@@ -0,0 +1,99 @@
1
+ import { isNodeOfType } from 'eslint-codemod-utils';
2
+ import { getSourceCode } from '@atlaskit/eslint-utils/context-compat';
3
+ import * as ast from '../../../../ast-nodes';
4
+ import { isSupportedForLint } from '../supported';
5
+ function isImportDeclaration(node) {
6
+ return node.type === 'ImportDeclaration';
7
+ }
8
+ export const JSXElement = {
9
+ lint(node, {
10
+ context
11
+ }) {
12
+ if (!isSupportedForLint(node)) {
13
+ return;
14
+ }
15
+ const nodeName = ast.JSXElement.getName(node);
16
+ const sourceCode = getSourceCode(context);
17
+ const importDeclarations = sourceCode.ast.body.filter(isImportDeclaration);
18
+ let existingHeadingName = null;
19
+ const usedNames = new Set();
20
+
21
+ // Check for existing imports
22
+ for (const declaration of importDeclarations) {
23
+ for (const specifier of declaration.specifiers) {
24
+ usedNames.add(specifier.local.name);
25
+ }
26
+ if (declaration.source.value === '@atlaskit/heading') {
27
+ const defaultSpecifier = declaration.specifiers.find(specifier => specifier.type === 'ImportDefaultSpecifier');
28
+ if (defaultSpecifier) {
29
+ existingHeadingName = defaultSpecifier.local.name;
30
+ }
31
+ }
32
+ }
33
+ const generateUniqueName = baseName => {
34
+ let index = 1;
35
+ let newName = baseName;
36
+ while (usedNames.has(newName)) {
37
+ newName = `${baseName}${index}`;
38
+ index++;
39
+ }
40
+ return newName;
41
+ };
42
+ const headingName = existingHeadingName || generateUniqueName('Heading');
43
+ context.report({
44
+ node: node.openingElement,
45
+ messageId: 'noHtmlHeading',
46
+ data: {
47
+ name: nodeName
48
+ },
49
+ suggest: [{
50
+ desc: 'Replace with Heading component from @atlaskit/heading',
51
+ fix(fixer) {
52
+ var _node$closingElement;
53
+ const openingTagRange = node.openingElement.range;
54
+ const closingTagRange = (_node$closingElement = node.closingElement) === null || _node$closingElement === void 0 ? void 0 : _node$closingElement.range;
55
+ const elementName = isNodeOfType(node.openingElement.name, 'JSXIdentifier') ? node.openingElement.name.name : '';
56
+ const attributesText = node.openingElement.attributes
57
+ // Don't bring in the "role" or the "aria-level" because it's not needed
58
+ .filter(attr => !isNodeOfType(attr, 'JSXAttribute') || typeof attr.name.name === 'string' && !['role', 'aria-level'].includes(attr.name.name)).map(attr => sourceCode.getText(attr)).join(' ');
59
+
60
+ // Get the heading level
61
+ let headingLevel = '';
62
+ const ariaLevel = node.openingElement.attributes.find(attr => isNodeOfType(attr, 'JSXAttribute') && attr.name.name === 'aria-level');
63
+ if (ariaLevel && isNodeOfType(ariaLevel, 'JSXAttribute')) {
64
+ var _ariaLevel$value, _ariaLevel$value2;
65
+ // If it's a string
66
+ if (((_ariaLevel$value = ariaLevel.value) === null || _ariaLevel$value === void 0 ? void 0 : _ariaLevel$value.type) === 'Literal' && ariaLevel.value.value) {
67
+ headingLevel = `h${ariaLevel.value.value}`;
68
+ // If it's a number or some other literal in an expression container
69
+ } else if (((_ariaLevel$value2 = ariaLevel.value) === null || _ariaLevel$value2 === void 0 ? void 0 : _ariaLevel$value2.type) === 'JSXExpressionContainer' && ariaLevel.value.expression.type === 'Literal' && ariaLevel.value.expression.value) {
70
+ headingLevel = `h${ariaLevel.value.expression.value}`;
71
+ }
72
+ } else if (elementName.match(/h[1-6]/)) {
73
+ headingLevel = elementName;
74
+ }
75
+ const fixers = [];
76
+
77
+ // Replace <a> with <Heading> and retain attributes
78
+ if (openingTagRange) {
79
+ if (node.openingElement.selfClosing) {
80
+ fixers.push(fixer.replaceTextRange([openingTagRange[0] + 1, openingTagRange[1] - 1], `${headingName}${headingLevel ? ` as="${headingLevel}"` : ''}${attributesText ? ` ${attributesText}` : ''} /`));
81
+ } else {
82
+ fixers.push(fixer.replaceTextRange([openingTagRange[0] + 1, openingTagRange[1] - 1], `${headingName}${headingLevel ? ` as="${headingLevel}"` : ''}${attributesText ? ` ${attributesText}` : ''}`));
83
+ }
84
+ }
85
+ if (closingTagRange && !node.openingElement.selfClosing) {
86
+ fixers.push(fixer.replaceTextRange([closingTagRange[0] + 2, closingTagRange[1] - 1], headingName));
87
+ }
88
+
89
+ // Add import if not present
90
+ if (!existingHeadingName) {
91
+ const importStatement = `import ${headingName} from '@atlaskit/heading';\n`;
92
+ fixers.push(fixer.insertTextBefore(sourceCode.ast, importStatement));
93
+ }
94
+ return fixers;
95
+ }
96
+ }]
97
+ });
98
+ }
99
+ };
@@ -0,0 +1,37 @@
1
+ /* eslint-disable @repo/internal/react/require-jsdoc */
2
+
3
+ import { isNodeOfType } from 'eslint-codemod-utils';
4
+ import { getScope } from '@atlaskit/eslint-utils/context-compat';
5
+ import { getJsxElementByName } from '../../../utils/get-jsx-element-by-name';
6
+ import { getStyledComponentCall } from '../../../utils/get-styled-component-call';
7
+ import { isSupportedForLint } from '../supported';
8
+ export const StyledComponent = {
9
+ lint(node, {
10
+ context
11
+ }) {
12
+ var _getJsxElementByName;
13
+ if (!isNodeOfType(node, 'CallExpression') || !isNodeOfType(node.callee, 'MemberExpression') || !isNodeOfType(node.callee.object, 'Identifier') || !isNodeOfType(node.callee.property, 'Identifier')) {
14
+ return;
15
+ }
16
+ const styles = getStyledComponentCall(node);
17
+ const elementName = node.callee.property.name;
18
+ if (!styles || !isNodeOfType(styles.id, 'Identifier')) {
19
+ return;
20
+ }
21
+ const jsxElement = (_getJsxElementByName = getJsxElementByName(styles.id.name, getScope(context, node))) === null || _getJsxElementByName === void 0 ? void 0 : _getJsxElementByName.parent;
22
+ if (!jsxElement) {
23
+ // If there's no JSX element, we can't determine if it's being used as an heading or not
24
+ return;
25
+ }
26
+ if (jsxElement && !isSupportedForLint(jsxElement, elementName)) {
27
+ return;
28
+ }
29
+ context.report({
30
+ node: styles,
31
+ messageId: 'noHtmlHeading',
32
+ data: {
33
+ name: node.callee.property.name
34
+ }
35
+ });
36
+ }
37
+ };
@@ -0,0 +1,72 @@
1
+ import { isNodeOfType } from 'eslint-codemod-utils';
2
+ import * as ast from '../../../ast-nodes';
3
+ const supportedElements = [{
4
+ name: 'h1'
5
+ }, {
6
+ name: 'h2'
7
+ }, {
8
+ name: 'h3'
9
+ }, {
10
+ name: 'h4'
11
+ }, {
12
+ name: 'h5'
13
+ }, {
14
+ name: 'h6'
15
+ }, {
16
+ name: '*',
17
+ attributes: [{
18
+ name: 'role',
19
+ values: ['heading']
20
+ }]
21
+ }];
22
+
23
+ /**
24
+ * Determines if the given JSX element is a supported element to lint with this rule.
25
+ */
26
+ export function isSupportedForLint(jsxNode, elementName) {
27
+ if (!isNodeOfType(jsxNode, 'JSXElement')) {
28
+ return false;
29
+ }
30
+
31
+ // Allow passing in the element name because the jsxNode doesn't
32
+ // represent the element name with styled components
33
+ const elName = elementName || ast.JSXElement.getName(jsxNode);
34
+ if (!elName) {
35
+ return false;
36
+ }
37
+
38
+ // Only check native HTML elements, not components
39
+ if (elName[0] !== elName[0].toLowerCase()) {
40
+ return false;
41
+ }
42
+ let supportedElement = supportedElements.find(({
43
+ name
44
+ }) => name === elName);
45
+ if (!supportedElement) {
46
+ supportedElement = supportedElements.find(({
47
+ name
48
+ }) => name === '*');
49
+ }
50
+ if (!supportedElement) {
51
+ return false;
52
+ }
53
+
54
+ // Check if the element has any attributes that are not supported
55
+ const attributes = ast.JSXElement.getAttributes(jsxNode);
56
+ if (supportedElement.attributes && !supportedElement.attributes.every(({
57
+ name,
58
+ values
59
+ }) => {
60
+ return attributes.some(attribute => {
61
+ if (attribute.type === 'JSXSpreadAttribute') {
62
+ return false;
63
+ }
64
+ const isMatchingName = attribute.name.name === name;
65
+ const isMatchingValues = values && attribute.value && attribute.value.type === 'Literal' && typeof attribute.value.value === 'string' && (values === null || values === void 0 ? void 0 : values.includes(attribute.value.value));
66
+ return isMatchingName && isMatchingValues;
67
+ });
68
+ })) {
69
+ return false;
70
+ }
71
+ return true;
72
+ }
@@ -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::f202c585a3745f4765e0515889577a22>>
3
+ * @codegen <<SignedSource::1d6565916d1f3cd5c89e9037bed73bae>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
 
@@ -25,6 +25,8 @@ export default {
25
25
  '@atlaskit/design-system/no-html-anchor': 'warn',
26
26
  '@atlaskit/design-system/no-html-button': 'warn',
27
27
  '@atlaskit/design-system/no-html-checkbox': 'warn',
28
+ '@atlaskit/design-system/no-html-code': 'warn',
29
+ '@atlaskit/design-system/no-html-heading': 'warn',
28
30
  '@atlaskit/design-system/no-html-image': 'warn',
29
31
  '@atlaskit/design-system/no-html-range': 'warn',
30
32
  '@atlaskit/design-system/no-html-select': 'warn',
@@ -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::6b4a8032ac1f59781b9675b9cc053a55>>
3
+ * @codegen <<SignedSource::e184ed47313aaf593cfc89e54f6b46a1>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
 
@@ -24,6 +24,8 @@ export default {
24
24
  '@atlaskit/design-system/no-html-anchor': 'warn',
25
25
  '@atlaskit/design-system/no-html-button': 'warn',
26
26
  '@atlaskit/design-system/no-html-checkbox': 'warn',
27
+ '@atlaskit/design-system/no-html-code': 'warn',
28
+ '@atlaskit/design-system/no-html-heading': 'warn',
27
29
  '@atlaskit/design-system/no-html-image': 'warn',
28
30
  '@atlaskit/design-system/no-html-range': 'warn',
29
31
  '@atlaskit/design-system/no-html-select': 'warn',
@@ -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::88336f6c2f8c5d33f84730619006b49e>>
3
+ * @codegen <<SignedSource::9fc355aa8513a229264d34aef4020b4c>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
 
@@ -20,6 +20,8 @@ export default {
20
20
  '@atlaskit/design-system/no-html-anchor': 'warn',
21
21
  '@atlaskit/design-system/no-html-button': 'warn',
22
22
  '@atlaskit/design-system/no-html-checkbox': 'warn',
23
+ '@atlaskit/design-system/no-html-code': 'warn',
24
+ '@atlaskit/design-system/no-html-heading': 'warn',
23
25
  '@atlaskit/design-system/no-html-image': 'warn',
24
26
  '@atlaskit/design-system/no-html-range': 'warn',
25
27
  '@atlaskit/design-system/no-html-select': 'warn',
@@ -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::829b781e15e90c3a142273e2b153459f>>
3
+ * @codegen <<SignedSource::e19338713541f03377c09ec215ef56d4>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
 
@@ -19,6 +19,8 @@ export default {
19
19
  '@atlaskit/design-system/no-html-anchor': 'warn',
20
20
  '@atlaskit/design-system/no-html-button': 'warn',
21
21
  '@atlaskit/design-system/no-html-checkbox': 'warn',
22
+ '@atlaskit/design-system/no-html-code': 'warn',
23
+ '@atlaskit/design-system/no-html-heading': 'warn',
22
24
  '@atlaskit/design-system/no-html-image': 'warn',
23
25
  '@atlaskit/design-system/no-html-range': 'warn',
24
26
  '@atlaskit/design-system/no-html-select': 'warn',
@@ -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::60d053061448c69290fba282ee307366>>
3
+ * @codegen <<SignedSource::0b49339ed517ad09644d8ae58d540900>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  import consistentCssPropUsage from './consistent-css-prop-usage';
@@ -23,6 +23,8 @@ import noExportedKeyframes from './no-exported-keyframes';
23
23
  import noHtmlAnchor from './no-html-anchor';
24
24
  import noHtmlButton from './no-html-button';
25
25
  import noHtmlCheckbox from './no-html-checkbox';
26
+ import noHtmlCode from './no-html-code';
27
+ import noHtmlHeading from './no-html-heading';
26
28
  import noHtmlImage from './no-html-image';
27
29
  import noHtmlRange from './no-html-range';
28
30
  import noHtmlSelect from './no-html-select';
@@ -79,6 +81,8 @@ export var rules = {
79
81
  'no-html-anchor': noHtmlAnchor,
80
82
  'no-html-button': noHtmlButton,
81
83
  'no-html-checkbox': noHtmlCheckbox,
84
+ 'no-html-code': noHtmlCode,
85
+ 'no-html-heading': noHtmlHeading,
82
86
  'no-html-image': noHtmlImage,
83
87
  'no-html-range': noHtmlRange,
84
88
  'no-html-select': noHtmlSelect,
@@ -0,0 +1,34 @@
1
+ import { createLintRule } from '../utils/create-rule';
2
+ import { JSXElement as _JSXElement, StyledComponent } from './node-types';
3
+ var rule = createLintRule({
4
+ meta: {
5
+ name: 'no-html-code',
6
+ type: 'suggestion',
7
+ hasSuggestions: true,
8
+ docs: {
9
+ description: 'Discourage direct usage of HTML code elements in favor of the Atlassian Design System code component.',
10
+ recommended: true,
11
+ severity: 'warn'
12
+ },
13
+ messages: {
14
+ noHtmlCode: "This <{{ name }}> should be replaced with the code component from the Atlassian Design System. The ADS code component ensures accessible implementations and consistent ADS styling."
15
+ }
16
+ },
17
+ create: function create(context) {
18
+ return {
19
+ // transforms styled.<anchor>(...) usages
20
+ CallExpression: function CallExpression(node) {
21
+ StyledComponent.lint(node, {
22
+ context: context
23
+ });
24
+ },
25
+ // transforms <anchor css={...}> usages
26
+ JSXElement: function JSXElement(node) {
27
+ _JSXElement.lint(node, {
28
+ context: context
29
+ });
30
+ }
31
+ };
32
+ }
33
+ });
34
+ export default rule;
@@ -0,0 +1,2 @@
1
+ export { StyledComponent } from './styled-component';
2
+ export { JSXElement } from './jsx-element';
@@ -0,0 +1,105 @@
1
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
2
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
3
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
4
+ import { isNodeOfType } from 'eslint-codemod-utils';
5
+ import { getSourceCode } from '@atlaskit/eslint-utils/context-compat';
6
+ import * as ast from '../../../../ast-nodes';
7
+ import { isSupportedForLint } from '../supported';
8
+ function isImportDeclaration(node) {
9
+ return node.type === 'ImportDeclaration';
10
+ }
11
+ export var JSXElement = {
12
+ lint: function lint(node, _ref) {
13
+ var context = _ref.context;
14
+ if (!isSupportedForLint(node)) {
15
+ return;
16
+ }
17
+ var nodeName = ast.JSXElement.getName(node);
18
+ var sourceCode = getSourceCode(context);
19
+ var importDeclarations = sourceCode.ast.body.filter(isImportDeclaration);
20
+ var existingCodeName = null;
21
+ var usedNames = new Set();
22
+
23
+ // Check for existing imports
24
+ var _iterator = _createForOfIteratorHelper(importDeclarations),
25
+ _step;
26
+ try {
27
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
28
+ var declaration = _step.value;
29
+ var _iterator2 = _createForOfIteratorHelper(declaration.specifiers),
30
+ _step2;
31
+ try {
32
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
33
+ var specifier = _step2.value;
34
+ usedNames.add(specifier.local.name);
35
+ }
36
+ } catch (err) {
37
+ _iterator2.e(err);
38
+ } finally {
39
+ _iterator2.f();
40
+ }
41
+ if (declaration.source.value === '@atlaskit/code') {
42
+ declaration.specifiers.filter(function (specifier) {
43
+ return isNodeOfType(specifier, 'ImportSpecifier') && isNodeOfType(specifier.parent, 'ImportDeclaration');
44
+ }).forEach(function (specifier) {
45
+ if (isNodeOfType(specifier, 'ImportSpecifier') && specifier.imported.name === 'Code') {
46
+ existingCodeName = specifier.local.name;
47
+ }
48
+ });
49
+ }
50
+ }
51
+ } catch (err) {
52
+ _iterator.e(err);
53
+ } finally {
54
+ _iterator.f();
55
+ }
56
+ var generateUniqueName = function generateUniqueName(baseName) {
57
+ var index = 1;
58
+ var newName = baseName;
59
+ while (usedNames.has(newName)) {
60
+ newName = "".concat(baseName).concat(index);
61
+ index++;
62
+ }
63
+ return newName;
64
+ };
65
+ var codeName = existingCodeName || generateUniqueName('Code');
66
+ context.report({
67
+ node: node.openingElement,
68
+ messageId: 'noHtmlCode',
69
+ data: {
70
+ name: nodeName
71
+ },
72
+ suggest: [{
73
+ desc: 'Replace with Code component from @atlaskit/code',
74
+ fix: function fix(fixer) {
75
+ var _node$closingElement;
76
+ var openingTagRange = node.openingElement.range;
77
+ var closingTagRange = (_node$closingElement = node.closingElement) === null || _node$closingElement === void 0 ? void 0 : _node$closingElement.range;
78
+ var attributesText = node.openingElement.attributes.map(function (attr) {
79
+ return sourceCode.getText(attr);
80
+ }).join(' ');
81
+ var fixers = [];
82
+
83
+ // Replace <code> with <Code> and retain attributes
84
+ if (openingTagRange) {
85
+ if (node.openingElement.selfClosing) {
86
+ fixers.push(fixer.replaceTextRange([openingTagRange[0] + 1, openingTagRange[1] - 1], "".concat(codeName).concat(attributesText ? " ".concat(attributesText) : '', " /")));
87
+ } else {
88
+ fixers.push(fixer.replaceTextRange([openingTagRange[0] + 1, openingTagRange[1] - 1], "".concat(codeName).concat(attributesText ? " ".concat(attributesText) : '')));
89
+ }
90
+ }
91
+ if (closingTagRange && !node.openingElement.selfClosing) {
92
+ fixers.push(fixer.replaceTextRange([closingTagRange[0] + 2, closingTagRange[1] - 1], codeName));
93
+ }
94
+
95
+ // Add import if not present
96
+ if (!existingCodeName) {
97
+ var importStatement = "import { ".concat(codeName, " } from '@atlaskit/code';\n");
98
+ fixers.push(fixer.insertTextBefore(sourceCode.ast, importStatement));
99
+ }
100
+ return fixers;
101
+ }
102
+ }]
103
+ });
104
+ }
105
+ };
@@ -0,0 +1,36 @@
1
+ /* eslint-disable @repo/internal/react/require-jsdoc */
2
+
3
+ import { isNodeOfType } from 'eslint-codemod-utils';
4
+ import { getScope } from '@atlaskit/eslint-utils/context-compat';
5
+ import { getJsxElementByName } from '../../../utils/get-jsx-element-by-name';
6
+ import { getStyledComponentCall } from '../../../utils/get-styled-component-call';
7
+ import { isSupportedForLint } from '../supported';
8
+ export var StyledComponent = {
9
+ lint: function lint(node, _ref) {
10
+ var _getJsxElementByName;
11
+ var context = _ref.context;
12
+ if (!isNodeOfType(node, 'CallExpression') || !isNodeOfType(node.callee, 'MemberExpression') || !isNodeOfType(node.callee.object, 'Identifier') || !isNodeOfType(node.callee.property, 'Identifier')) {
13
+ return;
14
+ }
15
+ var styles = getStyledComponentCall(node);
16
+ var elementName = node.callee.property.name;
17
+ if (!styles || !isNodeOfType(styles.id, 'Identifier')) {
18
+ return;
19
+ }
20
+ var jsxElement = (_getJsxElementByName = getJsxElementByName(styles.id.name, getScope(context, node))) === null || _getJsxElementByName === void 0 ? void 0 : _getJsxElementByName.parent;
21
+ if (!jsxElement) {
22
+ // If there's no JSX element, we can't determine if it's being used as an code or not
23
+ return;
24
+ }
25
+ if (jsxElement && !isSupportedForLint(jsxElement, elementName)) {
26
+ return;
27
+ }
28
+ context.report({
29
+ node: styles,
30
+ messageId: 'noHtmlCode',
31
+ data: {
32
+ name: node.callee.property.name
33
+ }
34
+ });
35
+ }
36
+ };
@@ -0,0 +1,57 @@
1
+ import { isNodeOfType } from 'eslint-codemod-utils';
2
+ import * as ast from '../../../ast-nodes';
3
+ var supportedElements = [{
4
+ name: 'code'
5
+ }];
6
+
7
+ /**
8
+ * Determines if the given JSX element is a supported element to lint with this rule.
9
+ */
10
+ export function isSupportedForLint(jsxNode, elementName) {
11
+ if (!isNodeOfType(jsxNode, 'JSXElement')) {
12
+ return false;
13
+ }
14
+
15
+ // Allow passing in the element name because the jsxNode doesn't
16
+ // represent the element name with styled components
17
+ var elName = elementName || ast.JSXElement.getName(jsxNode);
18
+ if (!elName) {
19
+ return false;
20
+ }
21
+
22
+ // Only check native HTML elements, not components
23
+ if (elName[0] !== elName[0].toLowerCase()) {
24
+ return false;
25
+ }
26
+ var supportedElement = supportedElements.find(function (_ref) {
27
+ var name = _ref.name;
28
+ return name === elName;
29
+ });
30
+ if (!supportedElement) {
31
+ supportedElement = supportedElements.find(function (_ref2) {
32
+ var name = _ref2.name;
33
+ return name === '*';
34
+ });
35
+ }
36
+ if (!supportedElement) {
37
+ return false;
38
+ }
39
+
40
+ // Check if the element has any attributes that are not supported
41
+ var attributes = ast.JSXElement.getAttributes(jsxNode);
42
+ if (supportedElement.attributes && !supportedElement.attributes.every(function (_ref3) {
43
+ var name = _ref3.name,
44
+ values = _ref3.values;
45
+ return attributes.some(function (attribute) {
46
+ if (attribute.type === 'JSXSpreadAttribute') {
47
+ return false;
48
+ }
49
+ var isMatchingName = attribute.name.name === name;
50
+ var isMatchingValues = values && attribute.value && attribute.value.type === 'Literal' && typeof attribute.value.value === 'string' && (values === null || values === void 0 ? void 0 : values.includes(attribute.value.value));
51
+ return isMatchingName && isMatchingValues;
52
+ });
53
+ })) {
54
+ return false;
55
+ }
56
+ return true;
57
+ }