From 6085a08212a3c525c4944e3bc10af66ba65d1d26 Mon Sep 17 00:00:00 2001 From: Ben Rogerson Date: Sun, 10 Jan 2021 10:21:39 +1030 Subject: [PATCH 1/2] Add className matching --- src/config/twinConfig.js | 5 +++ src/getStyles.js | 36 ++++++++++++++++++--- src/macro.js | 3 ++ src/macro/className.js | 68 ++++++++++++++++++++++++++++++++++++++++ src/macro/tw.js | 10 +++--- 5 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/macro/className.js diff --git a/src/config/twinConfig.js b/src/config/twinConfig.js index cb071327..162ad294 100644 --- a/src/config/twinConfig.js +++ b/src/config/twinConfig.js @@ -10,6 +10,7 @@ const configDefaultsTwin = ({ isStyledComponents, isGoober, isDev }) => ({ hasSuggestions: true, // Switch suggestions on/off when you use a tailwind class that's not found sassyPseudo: false, // Sets selectors like hover to &:hover debug: false, // Show the output of the classes twin converts + includeClassNames: false, // Look in the className props for tailwind classes to convert ...(isStyledComponents && configDefaultsStyledComponents), ...(isGoober && configDefaultsGoober), }) @@ -48,6 +49,10 @@ const configTwinValidators = { value => value === undefined, `The “debugProp” option was renamed to “dataTwProp”, please rename it in your twin config`, ], + includeClassNames: [ + isBoolean, + 'The config includeClassNames can only be true or false', + ], } export { configDefaultsTwin, configTwinValidators } diff --git a/src/getStyles.js b/src/getStyles.js index 1e3e5dcc..97545641 100644 --- a/src/getStyles.js +++ b/src/getStyles.js @@ -21,8 +21,10 @@ import { handleDynamic, } from './handlers' -export default (classes, t, state) => { - throwIf([null, 'null', undefined].includes(classes), () => +export default (classes, t, state, silentMismatches = false) => { + const hasEmptyClasses = [null, 'null', undefined].includes(classes) + if (silentMismatches && hasEmptyClasses) return + throwIf(hasEmptyClasses, () => logGeneralError( 'Only plain strings can be used with "tw".\nRead more at https://twinredirect.page.link/template-literals' ) @@ -39,14 +41,22 @@ export default (classes, t, state) => { const theme = getTheme(state.config.theme) + const classesMatched = [] + const classesMismatched = [] + // Merge styles into a single css object const styles = classesOrdered.reduce((results, classNameRaw) => { - doPrechecks([precheckGroup], { classNameRaw }) + !silentMismatches && doPrechecks([precheckGroup], { classNameRaw }) const pieces = getPieces({ classNameRaw, state }) const { className, hasVariants } = pieces const { configTwin } = state + if (silentMismatches && !className) { + classesMismatched.push(classNameRaw) + return results + } + throwIf(!className, () => hasVariants ? logNotFoundVariant({ classNameRaw }) : logNotFoundClass ) @@ -61,6 +71,11 @@ export default (classes, t, state) => { } = getProperties(className, state) // Kick off suggestions when no class matches + if (silentMismatches && !hasMatches && !hasUserPlugins) { + classesMismatched.push(classNameRaw) + return results + } + throwIf(!hasMatches && !hasUserPlugins, () => errorSuggestions({ pieces, state }) ) @@ -93,6 +108,11 @@ export default (classes, t, state) => { } // Check again there are no userPlugin matches + if (silentMismatches && !hasMatches && !style) { + classesMismatched.push(classNameRaw) + return results + } + throwIf(!hasMatches && !style, () => errorSuggestions({ pieces, state })) style = @@ -103,10 +123,16 @@ export default (classes, t, state) => { pieces.hasVariants ? addVariants({ results, style, pieces }) : style ) - state.configTwin.debug && debug(classNameRaw, result) + state.configTwin.debug && debug(classNameRaw, style) + classesMatched.push(classNameRaw) return result }, {}) - return astify(isEmpty(styles) ? {} : styles, t) + return { + // TODO: Avoid astifying here, move it outside function + styles: astify(isEmpty(styles) ? {} : styles, t), + mismatched: classesMismatched.join(' '), + matched: classesMatched.join(' '), + } } diff --git a/src/macro.js b/src/macro.js index 8d17fa9c..555f3a93 100644 --- a/src/macro.js +++ b/src/macro.js @@ -24,6 +24,7 @@ import { import { handleThemeFunction } from './macro/theme' import { handleGlobalStylesFunction } from './macro/globalStyles' import { handleTwProperty, handleTwFunction } from './macro/tw' +import { handleClassNameProperty } from './macro/className' import getUserPluginData from './utils/getUserPluginData' import { debugPlugins } from './logging' @@ -98,6 +99,8 @@ const twinMacro = ({ babel: { types: t }, references, state, config }) => { setCssIdentifier({ state, path, cssImport }) }, JSXAttribute(path) { + if (path.node.name.name === 'css') state.hasCssProp = true + handleClassNameProperty({ path, t, state }) handleTwProperty({ path, t, state }) }, }) diff --git a/src/macro/className.js b/src/macro/className.js new file mode 100644 index 00000000..44ca3f7e --- /dev/null +++ b/src/macro/className.js @@ -0,0 +1,68 @@ +import { throwIf } from './../utils' +import { logGeneralError } from './../logging' +/* eslint-disable-next-line unicorn/prevent-abbreviations */ +import { addDataTwPropToPath, addDataTwPropToExistingPath } from './debug' +import getStyles from './../getStyles' + +const handleClassNameProperty = ({ path, t, state }) => { + if (!state.configTwin.includeClassNames) return + if (path.node.name.name !== 'className') return + + const nodeValue = path.node.value + + // Ignore className if it cannot be resolved + if (nodeValue.expression) return + + const rawClasses = nodeValue.value || '' + if (!rawClasses) return + + const { styles, mismatched, matched } = getStyles(rawClasses, t, state, true) + + // When classes can't be matched we add them back into the className (it exists as a few properties) + path.node.value.value = mismatched + path.node.value.extra.rawValue = mismatched + path.node.value.extra.raw = `"${mismatched}"` + + const jsxPath = path.findParent(p => p.isJSXOpeningElement()) + const attributes = jsxPath.get('attributes') + const cssAttributes = attributes.filter( + p => p.node.name && p.node.name.name === 'css' + ) + + if (cssAttributes.length === 0) { + const attribute = t.jsxAttribute( + t.jsxIdentifier('css'), + t.jsxExpressionContainer(styles) + ) + mismatched ? path.insertAfter(attribute) : path.replaceWith(attribute) + addDataTwPropToPath({ t, attributes, rawClasses: matched, path, state }) + + return + } + + const expr = cssAttributes[0].get('value').get('expression') + + if (expr.isArrayExpression()) { + expr.unshiftContainer('elements', styles) + } else { + const cssProperty = expr.node + throwIf(!cssProperty, () => + logGeneralError( + `An empty css prop (css="") isn’t supported alongside the className prop` + ) + ) + expr.replaceWith(t.arrayExpression([styles, cssProperty])) + } + + if (!mismatched) path.remove() + + addDataTwPropToExistingPath({ + t, + attributes, + rawClasses: matched, + path: jsxPath, + state, + }) +} + +export { handleClassNameProperty } diff --git a/src/macro/tw.js b/src/macro/tw.js index 07405be6..f1a6e7fb 100644 --- a/src/macro/tw.js +++ b/src/macro/tw.js @@ -6,9 +6,7 @@ import { addDataTwPropToPath, addDataTwPropToExistingPath } from './debug' import getStyles from './../getStyles' const handleTwProperty = ({ path, t, state }) => { - if (path.node.name.name === 'css') state.hasCssProp = true - - if (path.node.name.name !== 'tw') return + if (!path.node || path.node.name.name !== 'tw') return state.hasTwProp = true const nodeValue = path.node.value @@ -27,7 +25,8 @@ const handleTwProperty = ({ path, t, state }) => { ) const rawClasses = expressionValue || nodeValue.value || '' - const styles = getStyles(rawClasses, t, state) + + const { styles } = getStyles(rawClasses, t, state) const jsxPath = path.findParent(p => p.isJSXOpeningElement()) const attributes = jsxPath.get('attributes') @@ -130,7 +129,8 @@ const handleTwFunction = ({ references, state, t }) => { }) } - replaceWithLocation(parsed.path, getStyles(rawClasses, t, state)) + const { styles } = getStyles(rawClasses, t, state) + replaceWithLocation(parsed.path, styles) }) } From d7aa794457077fde9741a86b77b0870665332886 Mon Sep 17 00:00:00 2001 From: Ben Rogerson Date: Wed, 20 Jan 2021 11:01:10 +1030 Subject: [PATCH 2/2] Improve className ordering + add tests --- .eslintrc.js | 2 + __fixtures__/!general.js | 2 - __fixtures__/!imports.js | 2 - __fixtures__/!namelessImport.js | 4 - __fixtures__/!plugins.js | 1 - __fixtures__/!properties.js | 2 - __fixtures__/!variantGrouping.js | 1 - __fixtures__/!variants.js | 1 - __fixtures__/.eslintrc.js | 8 + __fixtures__/includeClassNames/config.json | 3 + .../includeClassNames/includeClassNames.js | 144 ++++++ __snapshots__/plugin.test.js.snap | 434 ++++++++++++++++-- plugin.test.js | 9 +- src/{getStyles.js => getStyleData.js} | 3 +- src/macro.js | 1 - src/macro/className.js | 55 ++- src/macro/debug.js | 2 - src/macro/tw.js | 7 +- 18 files changed, 618 insertions(+), 63 deletions(-) create mode 100644 __fixtures__/.eslintrc.js create mode 100644 __fixtures__/includeClassNames/config.json create mode 100644 __fixtures__/includeClassNames/includeClassNames.js rename src/{getStyles.js => getStyleData.js} (98%) diff --git a/.eslintrc.js b/.eslintrc.js index e9b779e3..81c95bd4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -67,6 +67,7 @@ module.exports = { 'unicorn/import-style': 0, 'unicorn/prefer-optional-catch-binding': 0, 'unicorn/no-null': 0, + 'unicorn/prevent-abbreviations': 0, }, overrides: [ { @@ -94,4 +95,5 @@ module.exports = { }, }, ], + ignorePatterns: ['.eslintrc.js'], } diff --git a/__fixtures__/!general.js b/__fixtures__/!general.js index 2f086edb..17e9ba1b 100644 --- a/__fixtures__/!general.js +++ b/__fixtures__/!general.js @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable react/react-in-jsx-scope */ import tw from './macro' /** diff --git a/__fixtures__/!imports.js b/__fixtures__/!imports.js index 0aa33c6e..053ff2d9 100644 --- a/__fixtures__/!imports.js +++ b/__fixtures__/!imports.js @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable react/react-in-jsx-scope */ import tw, { theme, styled, css, GlobalStyles } from './macro' const twPropertyTest =
diff --git a/__fixtures__/!namelessImport.js b/__fixtures__/!namelessImport.js index f8d714fa..6802a0e1 100644 --- a/__fixtures__/!namelessImport.js +++ b/__fixtures__/!namelessImport.js @@ -1,7 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable import/no-unassigned-import */ -/* eslint-disable react/react-in-jsx-scope */ -/* eslint-disable react/jsx-curly-brace-presence */ import './macro' const twPropertyString =
diff --git a/__fixtures__/!plugins.js b/__fixtures__/!plugins.js index 8b89a173..53c2dc10 100644 --- a/__fixtures__/!plugins.js +++ b/__fixtures__/!plugins.js @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import tw from './macro' // Tailwind plugin tests diff --git a/__fixtures__/!properties.js b/__fixtures__/!properties.js index 108f5fdd..b759ab73 100644 --- a/__fixtures__/!properties.js +++ b/__fixtures__/!properties.js @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable react/react-in-jsx-scope */ import tw from './macro' const Component1 = () =>
diff --git a/__fixtures__/!variantGrouping.js b/__fixtures__/!variantGrouping.js index 1ec01743..2a6c6b5a 100644 --- a/__fixtures__/!variantGrouping.js +++ b/__fixtures__/!variantGrouping.js @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import tw from './macro' const basic = tw`group-hover:(flex m-10)` diff --git a/__fixtures__/!variants.js b/__fixtures__/!variants.js index 874b5f7f..7458029e 100644 --- a/__fixtures__/!variants.js +++ b/__fixtures__/!variants.js @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import tw from './macro' // Before/after pseudo elements diff --git a/__fixtures__/.eslintrc.js b/__fixtures__/.eslintrc.js new file mode 100644 index 00000000..c93526f3 --- /dev/null +++ b/__fixtures__/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'react/react-in-jsx-scope': 'off', + 'import/no-unassigned-import': 'off', + 'react/jsx-curly-brace-presence': 'off', + }, +} diff --git a/__fixtures__/includeClassNames/config.json b/__fixtures__/includeClassNames/config.json new file mode 100644 index 00000000..bf8219be --- /dev/null +++ b/__fixtures__/includeClassNames/config.json @@ -0,0 +1,3 @@ +{ + "includeClassNames": true +} diff --git a/__fixtures__/includeClassNames/includeClassNames.js b/__fixtures__/includeClassNames/includeClassNames.js new file mode 100644 index 00000000..7caf110e --- /dev/null +++ b/__fixtures__/includeClassNames/includeClassNames.js @@ -0,0 +1,144 @@ +import tw from './macro' + +const SkipEmptyClassName =
+const OnlyUppercaseConverted =
+const AllConverted =
+const SkippedCurlies =
+const SkippedConditionals =
+const SkippedGroup =
+ +// css + className +const CssPropFirst = ( +
+) +const CssPropLast = ( +
+) + +// tw + className +const TwPropFirst =
+const TwPropLast =
+ +// tw + css + className +const TwThenCssThenClassName = ( +
+) +const TwThenClassNameThenCss_KNOWN_ORDER_ISSUE = ( +
+) +const ClassNameThenTwThenCss_KNOWN_ORDER_ISSUE = ( +
+) +const ClassNameThenCssThenTw = ( +
+) +const CssThenClassNameThenTw_KNOWN_ORDER_ISSUE = ( +
+) +const CssThenTwThenClassName = ( +
+) + +// styled + everything +const Button = tw.div`` + +const StyledTwThenCssThenClassName = ( +