|
1 | 1 | import { Rule } from "eslint";
|
2 | 2 | import {
|
| 3 | + IdentifierWithParent, |
3 | 4 | getDefaultDeclarationString,
|
4 | 5 | getDefaultImportsFromPackage,
|
5 | 6 | getFromPackage,
|
6 | 7 | } from "../../helpers";
|
7 |
| -import { ImportDeclaration, ImportSpecifier, Literal } from "estree-jsx"; |
8 |
| -import { oldTokens } from "./tokenLists/oldTokens"; |
9 |
| -import { oldCssVarNamesV5 } from "./tokenLists/oldCssVarNamesV5"; |
| 8 | +import { |
| 9 | + Identifier, |
| 10 | + ImportDeclaration, |
| 11 | + ImportSpecifier, |
| 12 | + Literal, |
| 13 | +} from "estree-jsx"; |
| 14 | +import { |
| 15 | + oldTokens, |
| 16 | + oldCssVarNamesV5, |
| 17 | + globalNonColorTokensMap, |
| 18 | + oldGlobalNonColorTokens, |
| 19 | + oldGlobalNonColorCssVarNames, |
| 20 | + globalNonColorCssVarNamesMap, |
| 21 | +} from "../../../tokenLists"; |
10 | 22 |
|
11 | 23 | module.exports = {
|
12 |
| - meta: {}, |
| 24 | + meta: { fixable: "code" }, |
13 | 25 | create: function (context: Rule.RuleContext) {
|
14 | 26 | const tokensPackage = "@patternfly/react-tokens";
|
15 | 27 |
|
16 | 28 | const { imports: tokenSpecifiers } = getFromPackage(context, tokensPackage);
|
17 | 29 |
|
18 |
| - const defaultTokensWithDeclaration = getDefaultImportsFromPackage( |
| 30 | + const defaultTokenImports = getDefaultImportsFromPackage( |
19 | 31 | context,
|
20 | 32 | tokensPackage
|
21 | 33 | )
|
22 | 34 | .map((specifier) => ({
|
| 35 | + specifier, |
23 | 36 | path: getDefaultDeclarationString(specifier),
|
24 | 37 | declaration: specifier.parent,
|
25 | 38 | }))
|
26 | 39 | .filter(({ path }) => path !== undefined)
|
27 |
| - .map(({ path, declaration }) => ({ |
| 40 | + .map(({ specifier, path, declaration }) => ({ |
| 41 | + specifier, |
28 | 42 | token: (path as string).split("/").pop() as string,
|
29 | 43 | declaration,
|
30 | 44 | }));
|
31 | 45 |
|
32 |
| - const getMessage = (tokenName: string) => |
| 46 | + const getWarnMessage = (tokenName: string) => |
33 | 47 | `${tokenName} is an old CSS token. About half of our tokens have been replaced with newer ones. To find a suitable replacement token, check our new documentation https://staging-v6.patternfly.org/tokens/all-patternfly-tokens.`;
|
34 | 48 |
|
| 49 | + const getFixMessage = (oldToken: string, newToken: string) => |
| 50 | + `${oldToken} is an old CSS token and has been replaced with ${newToken}. If you want to use a different token, check our new documentation https://staging-v6.patternfly.org/tokens/all-patternfly-tokens.`; |
| 51 | + |
| 52 | + const shouldReplaceToken = (token: string) => |
| 53 | + oldGlobalNonColorTokens.includes(token) && |
| 54 | + globalNonColorTokensMap[token as keyof typeof globalNonColorTokensMap] !== |
| 55 | + "SKIP"; |
| 56 | + |
| 57 | + const replaceToken = ( |
| 58 | + node: ImportDeclaration | ImportSpecifier | Identifier, |
| 59 | + oldToken: string |
| 60 | + ) => { |
| 61 | + const newToken = |
| 62 | + globalNonColorTokensMap[ |
| 63 | + oldToken as keyof typeof globalNonColorTokensMap |
| 64 | + ]; |
| 65 | + |
| 66 | + context.report({ |
| 67 | + node, |
| 68 | + message: getFixMessage(oldToken, newToken), |
| 69 | + fix(fixer) { |
| 70 | + if (node.type === "ImportDeclaration") { |
| 71 | + const newDeclaration = node.source.value |
| 72 | + ?.toString() |
| 73 | + .replace(oldToken, newToken) as string; |
| 74 | + |
| 75 | + return [ |
| 76 | + fixer.replaceText(node.specifiers[0], newToken), |
| 77 | + fixer.replaceText(node.source, `"${newDeclaration}"`), |
| 78 | + ]; |
| 79 | + } |
| 80 | + |
| 81 | + if (node.type === "ImportSpecifier") { |
| 82 | + return fixer.replaceText(node.imported, newToken); |
| 83 | + } |
| 84 | + |
| 85 | + return fixer.replaceText(node, newToken); |
| 86 | + }, |
| 87 | + }); |
| 88 | + }; |
| 89 | + |
| 90 | + const replaceTokenOrWarn = ( |
| 91 | + node: ImportSpecifier | ImportDeclaration, |
| 92 | + token: string |
| 93 | + ) => { |
| 94 | + if (shouldReplaceToken(token)) { |
| 95 | + replaceToken(node, token); |
| 96 | + } else if (oldTokens.includes(token)) { |
| 97 | + context.report({ |
| 98 | + node, |
| 99 | + message: getWarnMessage(token), |
| 100 | + }); |
| 101 | + } |
| 102 | + }; |
| 103 | + |
35 | 104 | return {
|
36 | 105 | ImportSpecifier(node: ImportSpecifier) {
|
37 | 106 | if (tokenSpecifiers.includes(node)) {
|
38 |
| - const tokenName = node.imported.name; |
39 |
| - if (oldTokens.includes(tokenName)) { |
40 |
| - context.report({ |
41 |
| - node, |
42 |
| - message: getMessage(tokenName), |
43 |
| - }); |
44 |
| - } |
| 107 | + const token = node.imported.name; |
| 108 | + replaceTokenOrWarn(node, token); |
45 | 109 | }
|
46 | 110 | },
|
47 | 111 | ImportDeclaration(node: ImportDeclaration) {
|
48 |
| - const tokenWithDeclaration = defaultTokensWithDeclaration.find( |
| 112 | + const tokenWithDeclaration = defaultTokenImports.find( |
49 | 113 | ({ declaration }) => node.source.value === declaration?.source.value
|
50 | 114 | );
|
51 | 115 |
|
| 116 | + if (!tokenWithDeclaration) { |
| 117 | + return; |
| 118 | + } |
| 119 | + |
| 120 | + replaceTokenOrWarn(node, tokenWithDeclaration.token); |
| 121 | + }, |
| 122 | + Identifier(node: Identifier) { |
| 123 | + const parentType = (node as IdentifierWithParent).parent?.type; |
| 124 | + // handle ImportSpecifier and ImportDeclaration separately |
52 | 125 | if (
|
53 |
| - tokenWithDeclaration && |
54 |
| - oldTokens.includes(tokenWithDeclaration.token) |
| 126 | + parentType === "ImportSpecifier" || |
| 127 | + parentType === "ImportDefaultSpecifier" |
55 | 128 | ) {
|
56 |
| - context.report({ |
57 |
| - node, |
58 |
| - message: getMessage(tokenWithDeclaration.token), |
59 |
| - }); |
| 129 | + return; |
| 130 | + } |
| 131 | + |
| 132 | + const tokenInfo = defaultTokenImports.find( |
| 133 | + ({ specifier }) => node.name === specifier.local.name |
| 134 | + ); |
| 135 | + |
| 136 | + if (tokenInfo && shouldReplaceToken(tokenInfo.token)) { |
| 137 | + replaceToken(node, tokenInfo.token); |
| 138 | + } |
| 139 | + |
| 140 | + const unaliasedTokenSpecifier = tokenSpecifiers.find( |
| 141 | + (specifier) => |
| 142 | + specifier.local.name === specifier.imported.name && |
| 143 | + node.name === specifier.local.name |
| 144 | + ); |
| 145 | + |
| 146 | + if (unaliasedTokenSpecifier && shouldReplaceToken(node.name)) { |
| 147 | + replaceToken(node, node.name); |
60 | 148 | }
|
61 | 149 | },
|
62 | 150 | Literal(node: Literal) {
|
63 |
| - if ( |
64 |
| - typeof node.value === "string" && |
65 |
| - [...oldCssVarNames, ...oldCssVars].includes(node.value) |
66 |
| - ) { |
| 151 | + if (typeof node.value !== "string") { |
| 152 | + return; |
| 153 | + } |
| 154 | + |
| 155 | + let varName = node.value; |
| 156 | + const varRegex = /var\(([^)]+)\)/; |
| 157 | + const match = node.value.match(varRegex); |
| 158 | + |
| 159 | + if (match) { |
| 160 | + varName = match[1]; |
| 161 | + } |
| 162 | + |
| 163 | + const shouldReplaceVar = |
| 164 | + oldGlobalNonColorCssVarNames.includes(varName) && |
| 165 | + globalNonColorCssVarNamesMap[ |
| 166 | + varName as keyof typeof globalNonColorCssVarNamesMap |
| 167 | + ] !== "SKIP"; |
| 168 | + |
| 169 | + if (shouldReplaceVar) { |
| 170 | + const newVarName = |
| 171 | + globalNonColorCssVarNamesMap[ |
| 172 | + varName as keyof typeof globalNonColorCssVarNamesMap |
| 173 | + ]; |
| 174 | + |
| 175 | + if (newVarName !== "SKIP") { |
| 176 | + context.report({ |
| 177 | + node, |
| 178 | + message: getFixMessage(varName, newVarName), |
| 179 | + fix(fixer) { |
| 180 | + return fixer.replaceText( |
| 181 | + node, |
| 182 | + node.value?.toString().startsWith("var") |
| 183 | + ? `"var(${newVarName})"` |
| 184 | + : `"${newVarName}"` |
| 185 | + ); |
| 186 | + }, |
| 187 | + }); |
| 188 | + } |
| 189 | + } else if (oldCssVarNames.includes(varName)) { |
67 | 190 | context.report({
|
68 | 191 | node,
|
69 |
| - message: getMessage(node.value), |
| 192 | + message: getWarnMessage(node.value), |
70 | 193 | });
|
71 | 194 | }
|
72 | 195 | },
|
73 | 196 | };
|
74 | 197 | },
|
75 | 198 | };
|
76 | 199 |
|
77 |
| -// consumers may run class-name-updater before codemods, so we have to check also old tokens with v6 prefix |
| 200 | +// consumers may have run the old class-name-updater before codemods, so we should check also old tokens with v6 prefix |
78 | 201 | const oldCssVarNamesV6 = oldCssVarNamesV5.map((cssVarName) =>
|
79 | 202 | cssVarName.replace("v5", "v6")
|
80 | 203 | );
|
81 | 204 | const oldCssVarNames = [...oldCssVarNamesV5, ...oldCssVarNamesV6];
|
82 |
| -const oldCssVars = oldCssVarNames.map((cssVarName) => `var(${cssVarName})`); |
|
0 commit comments