@atlaskit/eslint-plugin-design-system 4.0.0 → 4.2.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.
- package/CHANGELOG.md +31 -0
- package/dist/cjs/index.js +14 -3
- package/dist/cjs/rules/ensure-design-token-usage/index.js +3 -0
- package/dist/cjs/rules/no-deprecated-apis/index.js +91 -0
- package/dist/cjs/rules/no-deprecated-design-token-usage/index.js +76 -0
- package/dist/cjs/rules/no-deprecated-imports/index.js +45 -33
- package/dist/cjs/rules/no-deprecated-imports/paths.js +43 -15
- package/dist/cjs/rules/no-unsafe-design-token-usage/index.js +19 -5
- package/dist/cjs/rules/use-visually-hidden/constants.js +12 -0
- package/dist/cjs/rules/use-visually-hidden/fix-jsx.js +39 -0
- package/dist/cjs/rules/use-visually-hidden/fix-vanilla.js +35 -0
- package/dist/cjs/rules/use-visually-hidden/index.js +194 -0
- package/dist/cjs/rules/use-visually-hidden/utils.js +91 -0
- package/dist/cjs/rules/utils/get-import-node-by-source.js +23 -0
- package/dist/cjs/rules/utils/is-color.js +3 -2
- package/dist/cjs/rules/utils/is-node.js +33 -3
- package/dist/cjs/rules/utils/remove-named-import.js +29 -0
- package/dist/cjs/version.json +1 -1
- package/dist/es2019/index.js +10 -2
- package/dist/es2019/rules/ensure-design-token-usage/index.js +3 -0
- package/dist/es2019/rules/no-deprecated-apis/index.js +72 -0
- package/dist/es2019/rules/no-deprecated-design-token-usage/index.js +58 -0
- package/dist/es2019/rules/no-deprecated-imports/index.js +45 -32
- package/dist/es2019/rules/no-deprecated-imports/paths.js +41 -14
- package/dist/es2019/rules/no-unsafe-design-token-usage/index.js +12 -5
- package/dist/es2019/rules/use-visually-hidden/constants.js +3 -0
- package/dist/es2019/rules/use-visually-hidden/fix-jsx.js +24 -0
- package/dist/es2019/rules/use-visually-hidden/fix-vanilla.js +21 -0
- package/dist/es2019/rules/use-visually-hidden/index.js +169 -0
- package/dist/es2019/rules/use-visually-hidden/utils.js +62 -0
- package/dist/es2019/rules/utils/get-import-node-by-source.js +10 -0
- package/dist/es2019/rules/utils/is-color.js +2 -1
- package/dist/es2019/rules/utils/is-node.js +19 -2
- package/dist/es2019/rules/utils/remove-named-import.js +16 -0
- package/dist/es2019/version.json +1 -1
- package/dist/esm/index.js +10 -2
- package/dist/esm/rules/ensure-design-token-usage/index.js +3 -0
- package/dist/esm/rules/no-deprecated-apis/index.js +81 -0
- package/dist/esm/rules/no-deprecated-design-token-usage/index.js +66 -0
- package/dist/esm/rules/no-deprecated-imports/index.js +45 -32
- package/dist/esm/rules/no-deprecated-imports/paths.js +41 -14
- package/dist/esm/rules/no-unsafe-design-token-usage/index.js +20 -5
- package/dist/esm/rules/use-visually-hidden/constants.js +3 -0
- package/dist/esm/rules/use-visually-hidden/fix-jsx.js +26 -0
- package/dist/esm/rules/use-visually-hidden/fix-vanilla.js +23 -0
- package/dist/esm/rules/use-visually-hidden/index.js +180 -0
- package/dist/esm/rules/use-visually-hidden/utils.js +74 -0
- package/dist/esm/rules/utils/get-import-node-by-source.js +14 -0
- package/dist/esm/rules/utils/is-color.js +2 -1
- package/dist/esm/rules/utils/is-node.js +22 -1
- package/dist/esm/rules/utils/remove-named-import.js +20 -0
- package/dist/esm/version.json +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/rules/no-deprecated-apis/index.d.ts +3 -0
- package/dist/types/rules/no-deprecated-design-token-usage/index.d.ts +3 -0
- package/dist/types/rules/no-deprecated-imports/paths.d.ts +33 -14
- package/dist/types/rules/use-visually-hidden/constants.d.ts +3 -0
- package/dist/types/rules/use-visually-hidden/fix-jsx.d.ts +3 -0
- package/dist/types/rules/use-visually-hidden/fix-vanilla.d.ts +3 -0
- package/dist/types/rules/use-visually-hidden/index.d.ts +3 -0
- package/dist/types/rules/use-visually-hidden/utils.d.ts +35 -0
- package/dist/types/rules/utils/get-import-node-by-source.d.ts +8 -0
- package/dist/types/rules/utils/is-node.d.ts +7 -0
- package/dist/types/rules/utils/remove-named-import.d.ts +8 -0
- package/package.json +6 -2
|
@@ -44,17 +44,24 @@ const rule = {
|
|
|
44
44
|
},
|
|
45
45
|
|
|
46
46
|
create(context) {
|
|
47
|
-
const sourceCode = context.getSourceCode();
|
|
48
47
|
const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
|
|
49
48
|
if (typeof importSource === 'string') {
|
|
50
49
|
memo[importSource] = {
|
|
51
50
|
message: ''
|
|
52
51
|
};
|
|
53
52
|
} else {
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
if ('message' in importSource) {
|
|
54
|
+
memo[importSource.path] = {
|
|
55
|
+
message: importSource.message
|
|
56
|
+
};
|
|
57
|
+
}
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
if ('imports' in importSource) {
|
|
60
|
+
memo[importSource.path] = {
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
imports: importSource.imports
|
|
63
|
+
};
|
|
64
|
+
}
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
return memo;
|
|
@@ -62,26 +69,45 @@ const rule = {
|
|
|
62
69
|
/**
|
|
63
70
|
* Report a restricted path.
|
|
64
71
|
* @param {string} importSource path of the import
|
|
65
|
-
* @param {Map<string,Object[]>} importNames Map of import names that are being imported
|
|
66
72
|
* @param {node} node representing the restricted path reference
|
|
73
|
+
* @param {Map<string,Rule.Node>} importNames Map of import names that are being imported
|
|
67
74
|
* @returns {void}
|
|
68
75
|
* @private
|
|
69
76
|
*/
|
|
70
77
|
|
|
71
|
-
function checkRestrictedPathAndReport(importSource,
|
|
78
|
+
function checkRestrictedPathAndReport(importSource, node, importNames) {
|
|
72
79
|
if (!Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) {
|
|
73
80
|
return;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const config = restrictedPathMessages[importSource]; // The message will only exist if the import is completely banned,
|
|
84
|
+
// eg a deprecated package
|
|
85
|
+
|
|
86
|
+
if ('message' in config) {
|
|
87
|
+
context.report({
|
|
88
|
+
node,
|
|
89
|
+
messageId: config.message ? 'pathWithCustomMessage' : 'path',
|
|
90
|
+
data: {
|
|
91
|
+
importSource,
|
|
92
|
+
customMessage: config.message
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
} // if there are specific named exports that are banned,
|
|
96
|
+
// iterate through and check if they're being imported
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if ('imports' in config) {
|
|
100
|
+
var _config$imports;
|
|
101
|
+
|
|
102
|
+
(_config$imports = config.imports) === null || _config$imports === void 0 ? void 0 : _config$imports.forEach(restrictedImport => {
|
|
103
|
+
if (importNames.has(restrictedImport.importName)) {
|
|
104
|
+
context.report({
|
|
105
|
+
node: importNames.get(restrictedImport.importName),
|
|
106
|
+
message: restrictedImport.message
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
85
111
|
}
|
|
86
112
|
/**
|
|
87
113
|
* Checks a node to see if any problems should be reported.
|
|
@@ -95,18 +121,10 @@ const rule = {
|
|
|
95
121
|
const importSource = node.source.value.trim();
|
|
96
122
|
const importNames = new Map();
|
|
97
123
|
|
|
98
|
-
if (
|
|
99
|
-
const starToken = sourceCode.getFirstToken(node, 1);
|
|
100
|
-
importNames.set('*', [{
|
|
101
|
-
loc: starToken === null || starToken === void 0 ? void 0 : starToken.loc
|
|
102
|
-
}]);
|
|
103
|
-
} else if ('specifiers' in node) {
|
|
124
|
+
if ('specifiers' in node) {
|
|
104
125
|
// @ts-ignore
|
|
105
126
|
for (const specifier of node.specifiers) {
|
|
106
127
|
let name;
|
|
107
|
-
const specifierData = {
|
|
108
|
-
loc: specifier.loc
|
|
109
|
-
};
|
|
110
128
|
|
|
111
129
|
if (specifier.type === 'ImportDefaultSpecifier') {
|
|
112
130
|
name = 'default';
|
|
@@ -119,16 +137,12 @@ const rule = {
|
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
if (name) {
|
|
122
|
-
|
|
123
|
-
importNames.get(name).push(specifierData);
|
|
124
|
-
} else {
|
|
125
|
-
importNames.set(name, [specifierData]);
|
|
126
|
-
}
|
|
140
|
+
importNames.set(name, specifier);
|
|
127
141
|
}
|
|
128
142
|
}
|
|
129
143
|
}
|
|
130
144
|
|
|
131
|
-
checkRestrictedPathAndReport(importSource,
|
|
145
|
+
checkRestrictedPathAndReport(importSource, node, importNames);
|
|
132
146
|
};
|
|
133
147
|
|
|
134
148
|
return {
|
|
@@ -138,9 +152,8 @@ const rule = {
|
|
|
138
152
|
if (node.source) {
|
|
139
153
|
checkNode(node);
|
|
140
154
|
}
|
|
141
|
-
}
|
|
155
|
+
}
|
|
142
156
|
|
|
143
|
-
ExportAllDeclaration: checkNode
|
|
144
157
|
};
|
|
145
158
|
}
|
|
146
159
|
|
|
@@ -1,43 +1,70 @@
|
|
|
1
|
+
export const namedThemeExports = [{
|
|
2
|
+
importName: 'assistive',
|
|
3
|
+
message: 'The assistive mixin is deprecated. Please use `@atlaskit/visually-hidden` instead.'
|
|
4
|
+
}, {
|
|
5
|
+
importName: 'visuallyHidden',
|
|
6
|
+
message: 'The visuallyHidden mixin is deprecated. Please use `@atlaskit/visually-hidden` instead.'
|
|
7
|
+
}, {
|
|
8
|
+
importName: 'focusRing',
|
|
9
|
+
message: 'The focusRing mixin is deprecated. Please use `@atlaskit/visually-hidden` instead.'
|
|
10
|
+
}];
|
|
1
11
|
export const restrictedPaths = [{
|
|
2
|
-
|
|
12
|
+
path: '@atlaskit/navigation-next',
|
|
3
13
|
message: `navigation-next is deprecated. Please use '@atlaskit/atlassian-navigation' instead.`
|
|
4
14
|
}, {
|
|
5
|
-
|
|
15
|
+
path: '@atlaskit/field-base',
|
|
6
16
|
message: `field-base is deprecated. Please use the '@atlaskit/form' package instead.`
|
|
7
17
|
}, {
|
|
8
|
-
|
|
18
|
+
path: '@atlaskit/field-radio-group',
|
|
9
19
|
message: `field-radio-group is deprecated. Please use '@atlaskit/radio' instead, and check the migration guide.`
|
|
10
20
|
}, {
|
|
11
|
-
|
|
21
|
+
path: '@atlaskit/field-range',
|
|
12
22
|
message: `field-range is deprecated. Please use '@atlaskit/range' instead.`
|
|
13
23
|
}, {
|
|
14
|
-
|
|
24
|
+
path: '@atlaskit/field-text',
|
|
15
25
|
message: `field-text is deprecated. Please use '@atlaskit/textfield' instead.`
|
|
16
26
|
}, {
|
|
17
|
-
|
|
27
|
+
path: '@atlaskit/field-text-area',
|
|
18
28
|
message: `field-text-area is deprecated. Please use '@atlaskit/textarea' instead.`
|
|
19
29
|
}, {
|
|
20
|
-
|
|
30
|
+
path: '@atlaskit/navigation',
|
|
21
31
|
message: `navigation is deprecated. Please use '@atlaskit/atlassian-navigation' instead.`
|
|
22
32
|
}, {
|
|
23
|
-
|
|
33
|
+
path: '@atlaskit/global-navigation',
|
|
24
34
|
message: `global-navigation is deprecated. Please use '@atlaskit/atlassian-navigation' for the horizontal nav bar, '@atlaskit/side-navigation' for the side nav, and '@atlaskit/page-layout' to layout your application.`
|
|
25
35
|
}, {
|
|
26
|
-
|
|
36
|
+
path: '@atlaskit/input',
|
|
27
37
|
message: 'input is deprecated. This was an internal component and should not be used directly.'
|
|
28
38
|
}, {
|
|
29
|
-
|
|
39
|
+
path: '@atlaskit/layer',
|
|
30
40
|
message: 'layer is deprecated. This was an internal component and should not be used directly.'
|
|
31
41
|
}, {
|
|
32
|
-
|
|
42
|
+
path: '@atlaskit/single-select',
|
|
33
43
|
message: `single-select is deprecated. Please use '@atlaskit/select' instead.`
|
|
34
44
|
}, {
|
|
35
|
-
|
|
45
|
+
path: '@atlaskit/multi-select',
|
|
36
46
|
message: `multi-select is deprecated. Please use '@atlaskit/select' instead.`
|
|
37
47
|
}, {
|
|
38
|
-
|
|
48
|
+
path: '@atlaskit/droplist',
|
|
39
49
|
message: `droplist is deprecated. For the pop-up behaviour please use '@atlaskit/popup' and for common menu components please use '@atlaskit/menu'.`
|
|
40
50
|
}, {
|
|
41
|
-
|
|
51
|
+
path: '@atlaskit/item',
|
|
42
52
|
message: `item is deprecated. Please use '@atlaskit/menu' instead.`
|
|
53
|
+
}, // TODO uncomment me when we formally deprecate typography
|
|
54
|
+
// {
|
|
55
|
+
// path: '@atlaskit/theme/typography',
|
|
56
|
+
// message: 'The typography mixins are deprecated. Please use `@atlaskit/heading` instead.',
|
|
57
|
+
// },
|
|
58
|
+
{
|
|
59
|
+
path: '@atlaskit/theme/constants',
|
|
60
|
+
imports: namedThemeExports
|
|
61
|
+
}, {
|
|
62
|
+
path: '@atlaskit/theme',
|
|
63
|
+
imports: namedThemeExports // .concat(
|
|
64
|
+
// { importName: 'typography', message: 'The typography mixins are deprecated. Please use `@atlaskit/heading` instead.',}
|
|
65
|
+
// )
|
|
66
|
+
|
|
67
|
+
}, {
|
|
68
|
+
path: '@atlaskit/icon-priority',
|
|
69
|
+
message: `icon-priority is deprecated due to limited usage in Cloud products. It will be deleted after 21 April 2022.`
|
|
43
70
|
}];
|
|
@@ -2,6 +2,9 @@ import renameMapping from '@atlaskit/tokens/rename-mapping';
|
|
|
2
2
|
import tokens from '@atlaskit/tokens/token-names';
|
|
3
3
|
import { isDecendantOfStyleBlock, isDecendantOfStyleJsxAttribute } from '../utils/is-node';
|
|
4
4
|
import { isToken } from '../utils/is-token';
|
|
5
|
+
|
|
6
|
+
const getCleanPathId = path => path.split('.').filter(el => el !== '[default]').join('.');
|
|
7
|
+
|
|
5
8
|
const defaultConfig = {
|
|
6
9
|
shouldEnforceFallbacks: false
|
|
7
10
|
};
|
|
@@ -28,7 +31,7 @@ token('color.background.blanket');
|
|
|
28
31
|
\`\`\`
|
|
29
32
|
`,
|
|
30
33
|
invalidToken: 'The token "{{name}}" does not exist.',
|
|
31
|
-
|
|
34
|
+
tokenRemoved: 'The token "{{name}}" is removed in favour of "{{replacement}}".',
|
|
32
35
|
tokenFallbackEnforced: `Token function requires a fallback, preferably something that best matches the light/default theme in case tokens aren't present.
|
|
33
36
|
|
|
34
37
|
\`\`\`
|
|
@@ -136,14 +139,18 @@ token('color.background.blanket');
|
|
|
136
139
|
return;
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
|
|
142
|
+
const migrationMeta = renameMapping.filter(t => t.state === 'deleted').find(t => t.path === tokenKey);
|
|
143
|
+
|
|
144
|
+
if (typeof tokenKey === 'string' && migrationMeta) {
|
|
145
|
+
const cleanTokenKey = getCleanPathId(migrationMeta.replacement);
|
|
140
146
|
context.report({
|
|
141
|
-
messageId: '
|
|
147
|
+
messageId: 'tokenRemoved',
|
|
142
148
|
node,
|
|
143
149
|
data: {
|
|
144
|
-
name: tokenKey
|
|
150
|
+
name: tokenKey,
|
|
151
|
+
replacement: cleanTokenKey
|
|
145
152
|
},
|
|
146
|
-
fix: fixer => fixer.replaceText(node.arguments[0], `'${
|
|
153
|
+
fix: fixer => fixer.replaceText(node.arguments[0], `'${cleanTokenKey}'`)
|
|
147
154
|
});
|
|
148
155
|
return;
|
|
149
156
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getImportedNodeBySource } from '../utils/get-import-node-by-source';
|
|
2
|
+
import { getClosestNodeOfType } from '../utils/is-node';
|
|
3
|
+
import { IMPORT_NAME, VISUALLY_HIDDEN_IMPORT, VISUALLY_HIDDEN_SOURCE } from './constants';
|
|
4
|
+
import { getFirstImport } from './utils';
|
|
5
|
+
export default ((source, node) => fixer => {
|
|
6
|
+
const fixes = [];
|
|
7
|
+
const importedNode = getFirstImport(source);
|
|
8
|
+
const visuallyHiddenNode = getImportedNodeBySource(source, VISUALLY_HIDDEN_SOURCE);
|
|
9
|
+
|
|
10
|
+
if (!importedNode) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const jsxOpeningElement = getClosestNodeOfType(node, 'JSXOpeningElement');
|
|
15
|
+
|
|
16
|
+
if (visuallyHiddenNode) {
|
|
17
|
+
fixes.push(fixer.replaceText(jsxOpeningElement, visuallyHiddenNode.specifiers[0].local.name));
|
|
18
|
+
} else {
|
|
19
|
+
fixes.push(fixer.insertTextBefore(importedNode, VISUALLY_HIDDEN_IMPORT));
|
|
20
|
+
fixes.push(fixer.replaceText(jsxOpeningElement, `<${IMPORT_NAME} />`));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return fixes;
|
|
24
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getImportedNodeBySource } from '../utils/get-import-node-by-source';
|
|
2
|
+
import { IMPORT_NAME, VISUALLY_HIDDEN_IMPORT, VISUALLY_HIDDEN_SOURCE } from './constants';
|
|
3
|
+
import { getFirstImport } from './utils';
|
|
4
|
+
export default ((source, node) => fixer => {
|
|
5
|
+
const fixes = [];
|
|
6
|
+
const importedNode = getFirstImport(source);
|
|
7
|
+
const visuallyHiddenNode = getImportedNodeBySource(source, VISUALLY_HIDDEN_SOURCE);
|
|
8
|
+
|
|
9
|
+
if (!importedNode) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (visuallyHiddenNode) {
|
|
14
|
+
fixes.push(fixer.replaceText(node, visuallyHiddenNode.specifiers[0].local.name));
|
|
15
|
+
} else {
|
|
16
|
+
fixes.push(fixer.insertTextBefore(importedNode, VISUALLY_HIDDEN_IMPORT));
|
|
17
|
+
fixes.push(fixer.replaceText(node, IMPORT_NAME));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return fixes;
|
|
21
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/* eslint-disable @atlaskit/design-system/use-visually-hidden */
|
|
2
|
+
import { getClosestNodeOfType, isStyledObjectNode, isStyledTemplateNode } from '../utils/is-node';
|
|
3
|
+
import fixJsx from './fix-jsx';
|
|
4
|
+
import fixVanilla from './fix-vanilla';
|
|
5
|
+
import { countMatchingKeyValues, getObjectLikeness, makeTemplateLiteralIntoEntries } from './utils';
|
|
6
|
+
const THEME_IMPORT_NAMES = ['visuallyHidden', 'assistive'];
|
|
7
|
+
const rule = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
fixable: 'code',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Suggest usage of the `@atlaskit/visually-hidden` component instead of either the `@atlaskit/theme` mixins or just something the user has rolled themselves.',
|
|
13
|
+
recommended: true
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
noDeprecatedUsage: 'Using the export `{{local}}` from `{{import}}` as a mixin is discouraged. Please use `@atlaskit/visually-hidden` instead.',
|
|
17
|
+
noDeprecated: 'The export `{{local}}` from `{{import}}` is deprecated. Please use `@atlaskit/visually-hidden` instead.',
|
|
18
|
+
suggestion: 'This CSS closely matches the implementation of a visually hidden element. You should consider using the `@atlaskit/visually-hidden` component instead.'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
create(context) {
|
|
23
|
+
const source = context.getSourceCode();
|
|
24
|
+
return {
|
|
25
|
+
ImportDeclaration(node) {
|
|
26
|
+
const isThemeNode = node.source.value === '@atlaskit/theme' || node.source.value === '@atlaskit/theme/constants';
|
|
27
|
+
|
|
28
|
+
if (!isThemeNode) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const visuallyHiddenOrAssistive = node.specifiers.filter(specifier => specifier.type === 'ImportSpecifier').find(specifier => THEME_IMPORT_NAMES.includes(specifier.imported.name));
|
|
33
|
+
|
|
34
|
+
if (!visuallyHiddenOrAssistive) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
context.getDeclaredVariables(visuallyHiddenOrAssistive).forEach(someNode => {
|
|
39
|
+
someNode.references.map(innerNode => innerNode.identifier).forEach(idNode => {
|
|
40
|
+
// @ts-ignore JSX is not typed correctly in eslint
|
|
41
|
+
if ((idNode === null || idNode === void 0 ? void 0 : idNode.parent.type) === 'JSXExpressionContainer') {
|
|
42
|
+
context.report({
|
|
43
|
+
node: idNode.parent,
|
|
44
|
+
messageId: 'noDeprecatedUsage',
|
|
45
|
+
data: {
|
|
46
|
+
import: `${node.source.value}`,
|
|
47
|
+
local: visuallyHiddenOrAssistive.local.name
|
|
48
|
+
},
|
|
49
|
+
fix: fixJsx(source, idNode)
|
|
50
|
+
}); // this is either a styled usage OR mixin usage in a styled usage
|
|
51
|
+
} else if (idNode.parent.type === 'CallExpression') {
|
|
52
|
+
if (isStyledObjectNode(idNode.parent) || isStyledTemplateNode(idNode.parent)) {
|
|
53
|
+
context.report({
|
|
54
|
+
node: idNode.parent,
|
|
55
|
+
messageId: 'noDeprecatedUsage',
|
|
56
|
+
data: {
|
|
57
|
+
import: `${node.source.value}`,
|
|
58
|
+
local: visuallyHiddenOrAssistive.local.name
|
|
59
|
+
},
|
|
60
|
+
fix: fixVanilla(source, idNode.parent)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (idNode.parent.callee === idNode) {
|
|
65
|
+
context.report({
|
|
66
|
+
node: idNode.parent,
|
|
67
|
+
messageId: 'noDeprecatedUsage',
|
|
68
|
+
data: {
|
|
69
|
+
import: `${node.source.value}`,
|
|
70
|
+
local: visuallyHiddenOrAssistive.local.name
|
|
71
|
+
},
|
|
72
|
+
fix: fixVanilla(source, getClosestNodeOfType(idNode.parent, 'TaggedTemplateExpression'))
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
return context.report({
|
|
79
|
+
node: visuallyHiddenOrAssistive,
|
|
80
|
+
messageId: 'noDeprecated',
|
|
81
|
+
data: {
|
|
82
|
+
import: `${node.source.value}`,
|
|
83
|
+
local: visuallyHiddenOrAssistive.local.name
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
CallExpression(node) {
|
|
89
|
+
var _node$arguments$;
|
|
90
|
+
|
|
91
|
+
if (node.type !== 'CallExpression') {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!(node.callee.type === 'MemberExpression' || node.callee.type === 'Identifier')) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isStyled = isStyledObjectNode(node);
|
|
100
|
+
|
|
101
|
+
if (node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name !== 'styled') {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (node.callee.type === 'Identifier' && node.callee.name !== 'css') {
|
|
106
|
+
return;
|
|
107
|
+
} // This is an object style (probably)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if (node.arguments && ((_node$arguments$ = node.arguments[0]) === null || _node$arguments$ === void 0 ? void 0 : _node$arguments$.type) === 'ObjectExpression') {
|
|
111
|
+
const matchingScore = getObjectLikeness(node.arguments[0]);
|
|
112
|
+
|
|
113
|
+
if (matchingScore > 0.8) {
|
|
114
|
+
return context.report({
|
|
115
|
+
node: node.parent,
|
|
116
|
+
messageId: 'suggestion',
|
|
117
|
+
fix: isStyled ? fixVanilla(source, node) : undefined
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
ObjectExpression(node) {
|
|
126
|
+
if (node.parent.type === 'CallExpression') {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const matchingScore = getObjectLikeness(node);
|
|
131
|
+
|
|
132
|
+
if (matchingScore > 0.8) {
|
|
133
|
+
return context.report({
|
|
134
|
+
node: node,
|
|
135
|
+
messageId: 'suggestion'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
'TaggedTemplateExpression[tag.name="css"],TaggedTemplateExpression[tag.object.name="styled"]': node => {
|
|
141
|
+
if (node.type !== 'TaggedTemplateExpression') {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const templateString = node.quasi.quasis.map(q => q.value.raw).join('');
|
|
146
|
+
const styleEntries = makeTemplateLiteralIntoEntries(templateString);
|
|
147
|
+
|
|
148
|
+
if (!styleEntries) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const count = countMatchingKeyValues(styleEntries.map(([key, value]) => ({
|
|
153
|
+
key,
|
|
154
|
+
value
|
|
155
|
+
})));
|
|
156
|
+
|
|
157
|
+
if (count > 0.8) {
|
|
158
|
+
return context.report({
|
|
159
|
+
node,
|
|
160
|
+
messageId: 'suggestion',
|
|
161
|
+
fix: node.tag.type !== 'Identifier' ? fixVanilla(source, node) : undefined
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
};
|
|
169
|
+
export default rule;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-unresolved
|
|
2
|
+
// eslint-disable-next-line @atlaskit/design-system/use-visually-hidden
|
|
3
|
+
const referenceObject = {
|
|
4
|
+
width: '1px',
|
|
5
|
+
height: '1px',
|
|
6
|
+
padding: '0',
|
|
7
|
+
position: 'absolute',
|
|
8
|
+
border: '0',
|
|
9
|
+
clip: 'rect(1px, 1px, 1px, 1px)',
|
|
10
|
+
overflow: 'hidden',
|
|
11
|
+
whiteSpace: 'nowrap'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns the first import in the esprima AST.
|
|
16
|
+
*/
|
|
17
|
+
export const getFirstImport = source => {
|
|
18
|
+
return source.ast.body.find(node => node.type === 'ImportDeclaration');
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Takes a template literal and returns [key, value] array of the css properties
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const makeTemplateLiteralIntoEntries = templateString => {
|
|
25
|
+
return templateString.replace(/\n/g, '').split(/;|{|}/).filter(el => !el.match(/\@/)).map(el => el.trim().split(':').map(e => e.trim()));
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Given a node, translate the node into css key-value pairs and
|
|
29
|
+
* compare the output to the reference styles required to make a
|
|
30
|
+
* visually hidden element.
|
|
31
|
+
*
|
|
32
|
+
* @returns {number} A fraction between 0-1 depending on the object's likeness.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export const getObjectLikeness = node => {
|
|
36
|
+
const styleEntries = node.properties.map(({
|
|
37
|
+
type,
|
|
38
|
+
key,
|
|
39
|
+
value
|
|
40
|
+
}) => {
|
|
41
|
+
if (type === 'Property' && key.type === 'Identifier') {
|
|
42
|
+
return {
|
|
43
|
+
key: key.name,
|
|
44
|
+
value: value.type === 'Literal' && value.value
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}).filter(node => Boolean(node));
|
|
50
|
+
return countMatchingKeyValues(styleEntries);
|
|
51
|
+
};
|
|
52
|
+
export const countMatchingKeyValues = styleEntries => {
|
|
53
|
+
const matchingStyleEntries = styleEntries.filter(entry => {
|
|
54
|
+
return entry.key in referenceObject;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (styleEntries.length < 5) {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return matchingStyleEntries.reduce((acc, curr) => acc + (referenceObject[curr === null || curr === void 0 ? void 0 : curr.key] === (curr === null || curr === void 0 ? void 0 : curr.value) ? 1.5 : 0.75), 0) / styleEntries.length;
|
|
62
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* eslint-disable import/no-unresolved */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {SourceCode} source The eslint source
|
|
5
|
+
* @param {string} path The path specified to find
|
|
6
|
+
* @returns {ImportDeclaration}
|
|
7
|
+
*/
|
|
8
|
+
export const getImportedNodeBySource = (source, path) => {
|
|
9
|
+
return source.ast.body.filter(node => node.type === 'ImportDeclaration').find(node => node.source.value === path);
|
|
10
|
+
};
|
|
@@ -37,7 +37,8 @@ export const isHardCodedColor = value => {
|
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
if (value.startsWith('#') && (
|
|
40
|
+
if (value.startsWith('#') && ( // short hex, hex, or hex with alpha
|
|
41
|
+
value.length === 4 || value.length === 7 || value.length === 9)) {
|
|
41
42
|
return true;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -32,6 +32,8 @@ export const isDecendantOfStyleJsxAttribute = node => {
|
|
|
32
32
|
|
|
33
33
|
return false;
|
|
34
34
|
};
|
|
35
|
+
export const isStyledTemplateNode = node => (node === null || node === void 0 ? void 0 : node.type) === 'TaggedTemplateExpression' && node.tag.type === 'MemberExpression' && node.tag.object.type === 'Identifier' && node.tag.object.name === 'styled';
|
|
36
|
+
export const isStyledObjectNode = node => (node === null || node === void 0 ? void 0 : node.type) === 'CallExpression' && node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'styled';
|
|
35
37
|
export const isDecendantOfStyleBlock = node => {
|
|
36
38
|
if (node.type === 'VariableDeclarator') {
|
|
37
39
|
if (node.id.type !== 'Identifier') {
|
|
@@ -59,7 +61,7 @@ export const isDecendantOfStyleBlock = node => {
|
|
|
59
61
|
return true;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
if (node
|
|
64
|
+
if (isStyledTemplateNode(node)) {
|
|
63
65
|
return true;
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -73,4 +75,19 @@ export const isDecendantOfStyleBlock = node => {
|
|
|
73
75
|
|
|
74
76
|
return false;
|
|
75
77
|
};
|
|
76
|
-
export const isChildOfType = (node, type) => node.parent.type === type;
|
|
78
|
+
export const isChildOfType = (node, type) => node.parent.type === type;
|
|
79
|
+
/**
|
|
80
|
+
* Given a node, walk up the tree until there is no parent OR a common ancestor of the correct type is found
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
export const getClosestNodeOfType = (node, type) => {
|
|
84
|
+
if (!node) {
|
|
85
|
+
return node;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (node.type === type) {
|
|
89
|
+
return node;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return getClosestNodeOfType(node.parent, type);
|
|
93
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* eslint-disable import/no-unresolved */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {SourceCode} source The eslint source
|
|
5
|
+
* @param {string} path The path specified to find
|
|
6
|
+
* @returns FixerObject
|
|
7
|
+
*/
|
|
8
|
+
export const removeNamedImport = (fixer, importNode, name) => {
|
|
9
|
+
const filteredSpecifers = importNode.specifiers.filter(node => node.type === 'ImportSpecifier' && node.imported.name !== name);
|
|
10
|
+
|
|
11
|
+
if (filteredSpecifers.length) {
|
|
12
|
+
return fixer.remove(importNode.specifiers.find(node => node.type === 'ImportSpecifier' && node.imported.name === name));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return fixer.remove(importNode);
|
|
16
|
+
};
|
package/dist/es2019/version.json
CHANGED
package/dist/esm/index.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import ensureTokenUsage from './rules/ensure-design-token-usage';
|
|
2
|
+
import noDeprecatedAPIs from './rules/no-deprecated-apis';
|
|
3
|
+
import noDeprecatedUsage from './rules/no-deprecated-design-token-usage';
|
|
2
4
|
import noDeprecatedImports from './rules/no-deprecated-imports';
|
|
3
5
|
import noUnsafeUsage from './rules/no-unsafe-design-token-usage';
|
|
6
|
+
import useVisuallyHidden from './rules/use-visually-hidden';
|
|
4
7
|
export var rules = {
|
|
5
8
|
'ensure-design-token-usage': ensureTokenUsage,
|
|
6
9
|
'no-unsafe-design-token-usage': noUnsafeUsage,
|
|
7
|
-
'no-deprecated-
|
|
10
|
+
'no-deprecated-design-token-usage': noDeprecatedUsage,
|
|
11
|
+
'no-deprecated-imports': noDeprecatedImports,
|
|
12
|
+
'no-deprecated-apis': noDeprecatedAPIs,
|
|
13
|
+
'use-visually-hidden': useVisuallyHidden
|
|
8
14
|
};
|
|
9
15
|
export var configs = {
|
|
10
16
|
recommended: {
|
|
11
17
|
plugins: ['@atlaskit/design-system'],
|
|
12
18
|
rules: {
|
|
13
|
-
'@atlaskit/design-system/no-deprecated-imports': 'error'
|
|
19
|
+
'@atlaskit/design-system/no-deprecated-imports': 'error',
|
|
20
|
+
'@atlaskit/design-system/use-visually-hidden': 'error',
|
|
21
|
+
'@atlaskit/design-system/no-deprecated-apis': 'warn'
|
|
14
22
|
}
|
|
15
23
|
}
|
|
16
24
|
};
|