From 9041d0f6c2d8a6c84b3d252e0330921f34956aec Mon Sep 17 00:00:00 2001 From: Corban Riley Date: Wed, 6 Sep 2023 12:38:06 -0400 Subject: [PATCH] Refactoring getTailwindClassName into own file --- src/css/tailwind.ts | 456 +++++++++++++++++++++++++++ src/css/utils.ts | 3 + transforms/tailwind-transform.ts | 519 +++---------------------------- 3 files changed, 495 insertions(+), 483 deletions(-) create mode 100644 src/css/tailwind.ts diff --git a/src/css/tailwind.ts b/src/css/tailwind.ts new file mode 100644 index 000000000..fd7fe16f7 --- /dev/null +++ b/src/css/tailwind.ts @@ -0,0 +1,456 @@ +import { Atoms } from './atoms' +import { isTruthy, kebabize } from './utils' + +type TextVariant = 'ellipsis' | 'capitalize' | 'lowercase' | 'uppercase' + +export type TailwindMapName = keyof Required | TextVariant + +export type TailwindMapValue = string | ((value: string) => string | null) + +const borderRadius = + ( + side?: 't' | 'b' | 'l' | 'r' | 'tl' | 'tr' | 'bl' | 'br' + ): TailwindMapValue => + (value: string) => { + const radii = { + none: 'none', + xs: '', // 4px + sm: 'lg', // 8px + md: 'xl', // 12px + lg: '2xl', // 16px + circle: 'full', // 9999px + } + + const radiiValue = radii[value as keyof typeof radii] + + return `rounded${side ? `-${side}` : ''}${ + radiiValue ? `-${radiiValue}` : '' + }` + } + +const borderWidth = + (side?: 't' | 'b' | 'l' | 'r'): TailwindMapValue => + (value: string) => { + const widths = { + none: '0', + thin: '', // '0.075rem', no suffix for 1px border in tailwind + thick: '2', // '0.125rem', + } + + const widthValue = widths[value as keyof typeof widths] + + return `border${side ? `-${side}` : ''}${ + widthValue ? `-${widthValue}` : '' + }` + } + +const borderColor = + (side?: 't' | 'b' | 'l' | 'r'): TailwindMapValue => + (value: string) => { + const colors = { + normal: 'normal', + focus: 'focus', + } + + const colorValue = colors[value as keyof typeof colors] + + return `border${side ? `-${side}` : ''}${ + colorValue ? `-${colorValue}` : '' + }` + } + +const tailwindMap: { + [key in TailwindMapName]: TailwindMapValue +} = { + position: '', // Empty string means just pass the value prefixless + margin: 'm', + marginTop: 'mt', + marginBottom: 'mb', + marginLeft: 'ml', + marginRight: 'mr', + marginX: 'mx', + marginY: 'my', + + padding: 'p', + paddingTop: 'pt', + paddingBottom: 'pb', + paddingLeft: 'pl', + paddingRight: 'pr', + paddingX: 'px', + paddingY: 'py', + + width: 'w', + minWidth: 'min-w', + maxWidth: 'max-w', + + height: 'h', + minHeight: 'min-h', + maxHeight: 'max-h', + + gap: 'gap', + + whiteSpace: 'whitespace', + + wordWrap: 'break', + wordBreak: 'break', + + top: 'top', + right: 'right', + bottom: 'bottom', + left: 'left', + + inset: 'inset', + + overflow: 'overflow', + overflowX: 'overflow-x', + overflowY: 'overflow-y', + + zIndex: 'z', + + userSelect: 'select', + + cursor: 'cursor', + + fontFamily: 'font', + + textTransform: '', // No prefix - pass through value as utility class + + opacity: 'opacity', + + pointerEvents: 'pointer-events', + + focusRing: 'shadow', // focusRing is only used for disabling a box shadow with none + boxShadow: 'shadow', + outline: 'outline', // Not used + + // Text variants: Boolean short hands for applying specific text styles + ellipsis: 'truncate', + capitalize: 'capitalize', + uppercase: 'uppercase', + lowercase: 'lowercase', + + borderRadius: borderRadius(), + borderTopRadius: borderRadius('t'), + borderBottomRadius: borderRadius('b'), + borderLeftRadius: borderRadius('l'), + borderRightRadius: borderRadius('r'), + borderTopLeftRadius: borderRadius('tl'), + borderTopRightRadius: borderRadius('tr'), + borderBottomLeftRadius: borderRadius('bl'), + borderBottomRightRadius: borderRadius('br'), + + borderWidth: borderWidth(), + borderTopWidth: borderWidth('t'), + borderBottomWidth: borderWidth('b'), + borderLeftWidth: borderWidth('l'), + borderRightWidth: borderWidth('r'), + + borderColor: borderColor(), + borderTopColor: borderColor('t'), + borderBottomColor: borderColor('b'), + borderLeftColor: borderColor('l'), + borderRightColor: borderColor('r'), + + // tailwind only supports a single border style at a time so we can't specify a side here - just map all to border + borderStyle: 'border', + borderTopStyle: 'border', + borderBottomStyle: 'border', + borderLeftStyle: 'border', + borderRightStyle: 'border', + + aspectRatio: value => { + switch (value) { + case 'auto': + return 'aspect-auto' + case '1/1': + return 'aspect-square' + case '16/9': + return 'aspect-video' + case '4/3': + return 'aspect-[4 / 3]' + case '3/1': + return 'aspect-[3 / 1]' + } + + return null + }, + + alignSelf: value => { + switch (value) { + case 'auto': + return 'self-auto' + case 'flex-start': + return 'self-start' + case 'flex-end': + return 'self-end' + case 'center': + return 'self-center' + + case 'stretch': + return 'self-stretch' + case 'baseline': + return 'self-baseline' + } + + return null + }, + + textOverflow: value => { + switch (value) { + case 'ellipsis': + return 'ellipsis' + case 'clip': + return 'overflow-clip' + } + + return null + }, + + fontSize: value => { + switch (value) { + case 'inherit': + return 'text-[inherit]' + case 'xsmall': + return 'text-xs' + case 'small': + return 'text-sm' + case 'normal': + return 'text-base' + case 'medium': + return 'text-large' + case 'large': + return 'text-xl' + case 'xlarge': + return 'text-2xl' + } + + return null + }, + + fontWeight: value => { + switch (value) { + case 'inherit': + return 'font-[inherit]' + case 'normal': + return 'font-normal' + case 'medium': + return 'font-medium' + case 'semibold': + return 'font-semibold' + case 'bold': + return 'font-bold' + } + + return null + }, + + letterSpacing: value => { + switch (value) { + case 'inherit': + return 'tracking-[inherit]' + case 'none': + return 'tracking-normal' + case 'normal': + return 'tracking-wide' + case 'wide': + return 'tracking-wider' + } + + return null + }, + + lineHeight: value => { + switch (value) { + case 'inherit': + return 'leading-[inherit]' + case '4': + return 'leading-4' + case '5': + return 'leading-5' + case '6': + return 'leading-6' + case '7': + return 'leading-7' + case '9': + return 'leading-9' + } + + return null + }, + + color: value => `text-${kebabize(value)}`, + background: value => `bg-${kebabize(value.replace('background', ''))}`, + + placeItems: value => { + switch (value) { + case 'center': + return 'place-items-center' + case 'start': + return 'place-items-start' + case 'end': + return 'place-items-end' + case 'stretch': + return 'place-items-stretch' + } + + return null + }, + + alignItems: value => { + switch (value) { + case 'flex-start': + return 'items-start' + case 'flex-end': + return 'items-end' + case 'center': + return 'items-center' + case 'baseline': + return 'items-baseline' + case 'stretch': + return 'items-stretch' + } + + return null + }, + + justifyContent: value => { + switch (value) { + case 'flex-start': + return 'justify-start' + case 'flex-end': + return 'justify-end' + case 'center': + return 'justify-center' + case 'space-between': + return 'justify-between' + case 'space-around': + return 'justify-around' + case 'space-evenly': + return 'justify-evenly' + } + + return null + }, + + justifySelf: value => { + switch (value) { + case 'auto': + return 'justify-self-auto' + case 'flex-start': + return 'justify-self-start' + case 'flex-end': + return 'justify-self-end' + case 'center': + return 'justify-self-center' + case 'stretch': + return 'justify-self-stretch' + } + + return null + }, + + backdropFilter: value => { + switch (value) { + case 'none': + return 'backdrop-filter-none' + case 'blur': + return 'backdrop-filter' + } + + return null + }, + + flexDirection: value => { + switch (value) { + case 'column': + return 'flex-col' + case 'column-reverse': + return 'flex-col-reverse' + case 'row': + return 'flex-row' + case 'row-reverse': + return 'flex-row-reverse' + } + + return null + }, + + flexShrink: value => { + switch (value) { + case '0': + return 'flex-shrink-0' + case '1': + return 'flex-shrink' + } + + return null + }, + + flexGrow: value => { + switch (value) { + case '0': + return 'flex-grow-0' + case '1': + return 'flex-grow' + } + + return null + }, + + flexWrap: value => `flex-${value}`, + + visibility: value => { + switch (value) { + case 'hidden': + return 'invisible' + case 'visible': + return 'visible' + } + + return null + }, + + textAlign: value => `text-${value}`, + + display: value => { + switch (value) { + case 'none': + return 'hidden' + case 'block': + return 'block' + case 'inline': + return 'inline' + case 'inline-block': + return 'inline-block' + case 'flex': + return 'flex' + case 'inline-flex': + return 'inline-flex' + case 'grid': + return 'grid' + case 'contents': + return 'contents' + } + + return null + }, +} + +export const TAILWIND_MAP_NAMES = Object.keys(tailwindMap) // as any as TailwindMapName[] + +export const getTailwindClassName = ( + key: string, + value?: string +): string | null => { + const mapValue = tailwindMap[key as TailwindMapName] + + if (!mapValue) { + return null + } + + if (typeof mapValue === 'function') { + return mapValue(value ? value : '') + } else { + return [mapValue, value].filter(isTruthy).join('-') + } +} diff --git a/src/css/utils.ts b/src/css/utils.ts index a61218714..a34137c2a 100644 --- a/src/css/utils.ts +++ b/src/css/utils.ts @@ -24,3 +24,6 @@ export const responsiveStyle = (rules: { export const selectorize = (classNames: string) => '.' + classNames.split(' ').join('.') + +export const isTruthy = (value: T | undefined | null): value is T => + Boolean(value) diff --git a/transforms/tailwind-transform.ts b/transforms/tailwind-transform.ts index 1ff35e3ef..c32d2b361 100644 --- a/transforms/tailwind-transform.ts +++ b/transforms/tailwind-transform.ts @@ -6,455 +6,25 @@ import * as prettierPluginEstree from 'prettier/plugins/estree' import * as prettierPluginTypescript from 'prettier/plugins/typescript' import { format } from 'prettier/standalone' -import { Atoms } from '../src/css/atoms' +import { + getTailwindClassName, + TailwindMapName, + TAILWIND_MAP_NAMES, +} from '../src/css/tailwind' +import { isTruthy } from '../src/css/utils' const COMPONENTS = ['Box', 'Text'] // const breakpoints = ['sm', 'md', 'lg', 'xl'] // const selectors = ['base', 'active', 'focus', 'disabled', 'hover', 'checked'] -//type LiteralValue = string | number | boolean | null | RegExp - -type MapFunc = string | ((value: string) => string | null) - -const borderRadius = - (side?: 't' | 'b' | 'l' | 'r' | 'tl' | 'tr' | 'bl' | 'br'): MapFunc => - (value: string) => { - const radii = { - none: 'none', - xs: '', // 4px - sm: 'lg', // 8px - md: 'xl', // 12px - lg: '2xl', // 16px - circle: 'full', // 9999px - } - - const radiiValue = radii[value] - - return `rounded${side ? `-${side}` : ''}${ - radiiValue ? `-${radiiValue}` : '' - }` - } - -const borderWidth = - (side?: 't' | 'b' | 'l' | 'r'): MapFunc => - (value: string) => { - const widths = { - none: '0', - thin: '', // '0.075rem', no suffix for 1px border in tailwind - thick: '2', // '0.125rem', - } - - const widthValue = widths[value] - - return `border${side ? `-${side}` : ''}${ - widthValue ? `-${widthValue}` : '' - }` - } - -const borderColor = - (side?: 't' | 'b' | 'l' | 'r'): MapFunc => - (value: string) => { - const colors = { - normal: 'normal', - focus: 'focus', - } - - const colorValue = colors[value] - - return `border${side ? `-${side}` : ''}${ - colorValue ? `-${colorValue}` : '' - }` - } - -type TextVariantKeys = 'ellipsis' | 'capitalize' | 'lowercase' | 'uppercase' - -const MAPPING: { [key in keyof Required | TextVariantKeys]: MapFunc } = { - position: '', // Empty string means just pass the value prefixless - margin: 'm', - marginTop: 'mt', - marginBottom: 'mb', - marginLeft: 'ml', - marginRight: 'mr', - marginX: 'mx', - marginY: 'my', - - padding: 'p', - paddingTop: 'pt', - paddingBottom: 'pb', - paddingLeft: 'pl', - paddingRight: 'pr', - paddingX: 'px', - paddingY: 'py', - - width: 'w', - minWidth: 'min-w', - maxWidth: 'max-w', - - height: 'h', - minHeight: 'min-h', - maxHeight: 'max-h', - - gap: 'gap', - - whiteSpace: 'whitespace', - - wordWrap: 'break', - wordBreak: 'break', - - top: 'top', - right: 'right', - bottom: 'bottom', - left: 'left', - - inset: 'inset', - - overflow: 'overflow', - overflowX: 'overflow-x', - overflowY: 'overflow-y', - - zIndex: 'z', - - userSelect: 'select', - - cursor: 'cursor', - - fontFamily: 'font', - - textTransform: '', // No prefix - pass through value as utility class - - opacity: 'opacity', - - pointerEvents: 'pointer-events', - - focusRing: 'shadow', // focusRing is only used for disabling a box shadow with none - boxShadow: 'shadow', - outline: 'outline', // Not used - - // Text variants: Boolean short hands for applying specific text styles - ellipsis: 'truncate', - capitalize: 'capitalize', - uppercase: 'uppercase', - lowercase: 'lowercase', - - borderRadius: borderRadius(), - borderTopRadius: borderRadius('t'), - borderBottomRadius: borderRadius('b'), - borderLeftRadius: borderRadius('l'), - borderRightRadius: borderRadius('r'), - borderTopLeftRadius: borderRadius('tl'), - borderTopRightRadius: borderRadius('tr'), - borderBottomLeftRadius: borderRadius('bl'), - borderBottomRightRadius: borderRadius('br'), - - borderWidth: borderWidth(), - borderTopWidth: borderWidth('t'), - borderBottomWidth: borderWidth('b'), - borderLeftWidth: borderWidth('l'), - borderRightWidth: borderWidth('r'), - - borderColor: borderColor(), - borderTopColor: borderColor('t'), - borderBottomColor: borderColor('b'), - borderLeftColor: borderColor('l'), - borderRightColor: borderColor('r'), - - // tailwind only supports a single border style at a time so we can't specify a side here - just map all to border - borderStyle: 'border', - borderTopStyle: 'border', - borderBottomStyle: 'border', - borderLeftStyle: 'border', - borderRightStyle: 'border', - - aspectRatio: value => { - switch (value) { - case 'auto': - return 'aspect-auto' - case '1/1': - return 'aspect-square' - case '16/9': - return 'aspect-video' - case '4/3': - return 'aspect-[4 / 3]' - case '3/1': - return 'aspect-[3 / 1]' - } - - return null - }, - - alignSelf: value => { - switch (value) { - case 'auto': - return 'self-auto' - case 'flex-start': - return 'self-start' - case 'flex-end': - return 'self-end' - case 'center': - return 'self-center' - - case 'stretch': - return 'self-stretch' - case 'baseline': - return 'self-baseline' - } - - return null - }, - - textOverflow: value => { - switch (value) { - case 'ellipsis': - return 'ellipsis' - case 'clip': - return 'overflow-clip' - } - - return null - }, - - fontSize: value => { - switch (value) { - case 'inherit': - return 'text-[inherit]' - case 'xsmall': - return 'text-xs' - case 'small': - return 'text-sm' - case 'normal': - return 'text-base' - case 'medium': - return 'text-large' - case 'large': - return 'text-xl' - case 'xlarge': - return 'text-2xl' - } - - return null - }, - - fontWeight: value => { - switch (value) { - case 'inherit': - return 'font-[inherit]' - case 'normal': - return 'font-normal' - case 'medium': - return 'font-medium' - case 'semibold': - return 'font-semibold' - case 'bold': - return 'font-bold' - } - - return null - }, - - letterSpacing: value => { - switch (value) { - case 'inherit': - return 'tracking-[inherit]' - case 'none': - return 'tracking-normal' - case 'normal': - return 'tracking-wide' - case 'wide': - return 'tracking-wider' - } - - return null - }, - - lineHeight: value => { - switch (value) { - case 'inherit': - return 'leading-[inherit]' - case '4': - return 'leading-4' - case '5': - return 'leading-5' - case '6': - return 'leading-6' - case '7': - return 'leading-7' - case '9': - return 'leading-9' - } - - return null - }, - - color: value => `text-${kebabize(value)}`, - background: value => `bg-${kebabize(value.replace('background', ''))}`, - - placeItems: value => { - switch (value) { - case 'center': - return 'place-items-center' - case 'start': - return 'place-items-start' - case 'end': - return 'place-items-end' - case 'stretch': - return 'place-items-stretch' - } - - return null - }, - - alignItems: value => { - switch (value) { - case 'flex-start': - return 'items-start' - case 'flex-end': - return 'items-end' - case 'center': - return 'items-center' - case 'baseline': - return 'items-baseline' - case 'stretch': - return 'items-stretch' - } - - return null - }, - - justifyContent: value => { - switch (value) { - case 'flex-start': - return 'justify-start' - case 'flex-end': - return 'justify-end' - case 'center': - return 'justify-center' - case 'space-between': - return 'justify-between' - case 'space-around': - return 'justify-around' - case 'space-evenly': - return 'justify-evenly' - } - - return null - }, - - justifySelf: value => { - switch (value) { - case 'auto': - return 'justify-self-auto' - case 'flex-start': - return 'justify-self-start' - case 'flex-end': - return 'justify-self-end' - case 'center': - return 'justify-self-center' - case 'stretch': - return 'justify-self-stretch' - } - - return null - }, - - backdropFilter: value => { - switch (value) { - case 'none': - return 'backdrop-filter-none' - case 'blur': - return 'backdrop-filter' - } - - return null - }, - - flexDirection: value => { - switch (value) { - case 'column': - return 'flex-col' - case 'column-reverse': - return 'flex-col-reverse' - case 'row': - return 'flex-row' - case 'row-reverse': - return 'flex-row-reverse' - } - - return null - }, - - flexShrink: value => { - switch (value) { - case '0': - return 'flex-shrink-0' - case '1': - return 'flex-shrink' - } - - return null - }, - - flexGrow: value => { - switch (value) { - case '0': - return 'flex-grow-0' - case '1': - return 'flex-grow' - } - - return null - }, - - flexWrap: value => `flex-${value}`, - - visibility: value => { - switch (value) { - case 'hidden': - return 'invisible' - case 'visible': - return 'visible' - } - - return null - }, - - textAlign: value => `text-${value}`, - - display: value => { - switch (value) { - case 'none': - return 'hidden' - case 'block': - return 'block' - case 'inline': - return 'inline' - case 'inline-block': - return 'inline-block' - case 'flex': - return 'flex' - case 'inline-flex': - return 'inline-flex' - case 'grid': - return 'grid' - case 'contents': - return 'contents' - } - - return null - }, -} - -type AttributeName = keyof typeof MAPPING - -const ATTRIBUTES = Object.keys(MAPPING) as any as AttributeName[] - const transform = async (file: FileInfo, api: API) => { const j = api.jscodeshift const root = j(file.source) let isMutated = false - const mutatedAttributes: AttributeName[] = [] + const mutatedAttributes: TailwindMapName[] = [] root .findJSXElements() @@ -470,7 +40,7 @@ const transform = async (file: FileInfo, api: API) => { attribute => attribute.type === 'JSXAttribute' && attribute.name.type === 'JSXIdentifier' && - ATTRIBUTES.includes(attribute.name.name as AttributeName) + TAILWIND_MAP_NAMES.includes(attribute.name.name) ) ) }) @@ -482,42 +52,41 @@ const transform = async (file: FileInfo, api: API) => { } // Replace Box components with div if there is no "as" prop - if ( - node.openingElement.name.type === 'JSXIdentifier' && - node.openingElement.name.name === 'Box' - ) { - // Check if node has an "as" prop - const asProp = node.openingElement.attributes.find( - attribute => - attribute.type === 'JSXAttribute' && - attribute.name.type === 'JSXIdentifier' && - attribute.name.name === 'as' - ) as JSXAttribute - - if (!asProp) { - node.openingElement.name.name = 'div' - - if (!node.openingElement.selfClosing) { - if (node.closingElement?.name.type === 'JSXIdentifier') { - node.closingElement.name.name = 'div' - } - } - } - } + // if ( + // node.openingElement.name.type === 'JSXIdentifier' && + // node.openingElement.name.name === 'Box' + // ) { + // // Check if node has an "as" prop + // const asProp = node.openingElement.attributes.find( + // attribute => + // attribute.type === 'JSXAttribute' && + // attribute.name.type === 'JSXIdentifier' && + // attribute.name.name === 'as' + // ) as JSXAttribute + + // if (!asProp) { + // node.openingElement.name.name = 'div' + + // if (!node.openingElement.selfClosing) { + // if (node.closingElement?.name.type === 'JSXIdentifier') { + // node.closingElement.name.name = 'div' + // } + // } + // } + // } // Find all the attributes that we want to transform const attrs = node.openingElement.attributes.filter( attribute => attribute.type === 'JSXAttribute' && attribute.name.type === 'JSXIdentifier' && - ATTRIBUTES.includes(attribute.name.name as AttributeName) + TAILWIND_MAP_NAMES.includes(attribute.name.name) ) as JSXAttribute[] - // Map atomic props to tailwind classNames + // Map atomic props to tailwind classNames const tailwindClassNames: string[] = attrs .map(attr => { - const name = attr.name.name as AttributeName - const map: MapFunc = MAPPING[name] + const name = attr.name.name as TailwindMapName // const parseValue = (node: JSXAttribute['value']) => { // if (node) { @@ -576,11 +145,11 @@ const transform = async (file: FileInfo, api: API) => { if (attr.value === null) { // Handle boolean props - result = getClassName(map) + result = getTailwindClassName(name) } else if (attr.value) { switch (attr.value.type) { case 'StringLiteral': - result = getClassName(map, attr.value.value) + result = getTailwindClassName(name, attr.value.value) break } } @@ -653,7 +222,7 @@ const transform = async (file: FileInfo, api: API) => { attr => !( attr.type === 'JSXAttribute' && - mutatedAttributes.includes(attr.name.name as AttributeName) + mutatedAttributes.includes(attr.name.name as TailwindMapName) ) ) @@ -693,19 +262,3 @@ export default transform const joinClassNames = (classNames: string[]) => classNames.filter(isTruthy).join(' ') - -const kebabize = (str: string) => - str.replace( - /[A-Z]+(?![a-z])|[A-Z]/g, - ($, ofs) => (ofs ? '-' : '') + $.toLowerCase() - ) - -const isTruthy = (value: T | undefined | null): value is T => Boolean(value) - -const getClassName = (mapFn: MapFunc, value?: string) => { - if (typeof mapFn === 'function') { - return mapFn(value ? value : '') - } else { - return [mapFn, value].filter(isTruthy).join('-') - } -}