From 5f5b66014112888bada32fe28046b72d362d3252 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 8 Mar 2024 12:59:41 -0300 Subject: [PATCH 1/9] chore(tailwind): Simplify mechanism for processing Tailwind children --- packages/tailwind/src/tailwind.tsx | 62 +++++++++++++----------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index de7a65df7d..619c3d4f58 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -115,8 +115,6 @@ function processElement( return modifiedElement; } -type AnyElement = React.ReactElement>; - type HeadElement = React.ReactElement< HeadProps, string | React.JSXElementConstructor @@ -189,44 +187,38 @@ export const Tailwind: React.FC = ({ children, config }) => { const nonMediaQueryTailwindStylesPerClass = getStylesPerClassMap(nonMediaQueryCSS); - const childrenArray = React.Children.toArray(children); - const validElementsWithIndexes = childrenArray - .map((child, i) => [child, i] as [AnyElement, number]) - .filter(([child]) => React.isValidElement(child)); - - let headElementIndex = -1; - - validElementsWithIndexes.forEach(([element, i]) => { - childrenArray[i] = processElement( - element, - nonMediaQueryTailwindStylesPerClass, - nonEscapedMediaQueryClasses, - ); - - if ( - element.type === "head" || - (typeof element.type === "function" && - "name" in element.type && - element.type.name === "Head") - ) { - headElementIndex = i; - } - }); - - headStyles = headStyles.filter((style) => style.trim().length > 0); + let hasAppliedResponsiveStyles = false as boolean; + + const childrenArray = React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + const element = child; + + if (!hasAppliedResponsiveStyles) { + if ( + element.type === "head" || + (typeof element.type === "function" && + "name" in element.type && + element.type.name === "Head") + ) { + hasAppliedResponsiveStyles = true; + return processHead(processElement(element, nonMediaQueryTailwindStylesPerClass, nonEscapedMediaQueryClasses), headStyles); + } + } - if (headStyles.length > 0) { - if (headElementIndex === -1) { - throw new Error( - "Tailwind: To use responsive styles you must have a element as a direct child of the Tailwind component.", + return processElement( + element, + nonMediaQueryTailwindStylesPerClass, + nonEscapedMediaQueryClasses, ); } + }) ?? []; - const [headElement, headAllElementsIndex] = validElementsWithIndexes[ - headElementIndex - ] as [HeadElement, number]; + headStyles = headStyles.filter((style) => style.trim().length > 0); - childrenArray[headAllElementsIndex] = processHead(headElement, headStyles); + if (headStyles.length > 0 && !hasAppliedResponsiveStyles) { + throw new Error( + "Tailwind: To use responsive styles you must have a element as a direct child of the Tailwind component.", + ); } return <>{childrenArray}; From 3aed88215cb61a9a664df83e8f1b8fad4daa6c2f Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 8 Mar 2024 13:06:27 -0300 Subject: [PATCH 2/9] chore(tailwind): Organize a bit more --- packages/tailwind/src/tailwind.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 619c3d4f58..31d19c51a7 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -187,20 +187,23 @@ export const Tailwind: React.FC = ({ children, config }) => { const nonMediaQueryTailwindStylesPerClass = getStylesPerClassMap(nonMediaQueryCSS); - let hasAppliedResponsiveStyles = false as boolean; + headStyles = headStyles.filter((style) => style.trim().length > 0); + + const hasNonInlineStylesToApply = headStyles.length > 0; + let hasAppliedNonInlineStyles = false as boolean; const childrenArray = React.Children.map(children, (child) => { if (React.isValidElement(child)) { const element = child; - if (!hasAppliedResponsiveStyles) { + if (!hasAppliedNonInlineStyles && hasNonInlineStylesToApply) { if ( element.type === "head" || (typeof element.type === "function" && "name" in element.type && element.type.name === "Head") ) { - hasAppliedResponsiveStyles = true; + hasAppliedNonInlineStyles = true; return processHead(processElement(element, nonMediaQueryTailwindStylesPerClass, nonEscapedMediaQueryClasses), headStyles); } } @@ -213,9 +216,7 @@ export const Tailwind: React.FC = ({ children, config }) => { } }) ?? []; - headStyles = headStyles.filter((style) => style.trim().length > 0); - - if (headStyles.length > 0 && !hasAppliedResponsiveStyles) { + if (hasNonInlineStylesToApply && !hasAppliedNonInlineStyles) { throw new Error( "Tailwind: To use responsive styles you must have a element as a direct child of the Tailwind component.", ); From 150f58781386b91d1c0a354001d1e4d2912adc12 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 8 Mar 2024 13:11:27 -0300 Subject: [PATCH 3/9] chore(tailwind): Organize imports and rename getStylesPerClasSMap --- packages/tailwind/src/tailwind.tsx | 86 +++++++++++-------- ...ts => get-map-of-styles-per-class-name.ts} | 4 +- packages/tailwind/src/utils/index.ts | 5 +- 3 files changed, 57 insertions(+), 38 deletions(-) rename packages/tailwind/src/utils/{get-css-class-properties-map.ts => get-map-of-styles-per-class-name.ts} (75%) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 31d19c51a7..9944c29838 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -3,14 +3,16 @@ import * as React from "react"; import type { Config as TailwindOriginalConfig } from "tailwindcss"; import type { HeadProps } from "@react-email/head"; -import { cssToJsxStyle } from "./utils/css-to-jsx-style"; -import { getCssForMarkup } from "./utils/get-css-for-markup"; -import { minifyCss } from "./utils/minify-css"; -import { getStylesPerClassMap } from "./utils/get-css-class-properties-map"; -import { escapeClassName } from "./utils/escape-class-name"; -import { sanitizeClassName } from "./utils/sanitize-class-name"; -import { useRgbNonSpacedSyntax } from "./utils/use-rgb-non-spaced-syntax"; -import { quickSafeRenderToString } from "./utils/quick-safe-render-to-string"; +import { + getMapOfStylesPerClassName, + useRgbNonSpacedSyntax, + quickSafeRenderToString, + minifyCss, + getCssForMarkup, + cssToJsxStyle, + escapeClassName, + sanitizeClassName, +} from "./utils"; export type TailwindConfig = Omit; @@ -122,10 +124,12 @@ type HeadElement = React.ReactElement< function processHead( headElement: HeadElement, - responsiveStyles: string[], + nonInlineStylesToApply: string[], ): React.ReactElement { /* only minify here since it is the only place that is going to be in the DOM */ - const styleElement = ; + const styleElement = ( + + ); return React.cloneElement( headElement, @@ -136,7 +140,7 @@ function processHead( } export const Tailwind: React.FC = ({ children, config }) => { - let headStyles: string[] = []; + let nonInlineStylesToApply: string[] = []; const markupWithTailwindClasses = quickSafeRenderToString(<>{children}); const markupCSS = useRgbNonSpacedSyntax( @@ -181,40 +185,50 @@ export const Tailwind: React.FC = ({ children, config }) => { ); } - headStyles.push(finalMediaQuery); + nonInlineStylesToApply.push(finalMediaQuery); } const nonMediaQueryTailwindStylesPerClass = - getStylesPerClassMap(nonMediaQueryCSS); + getMapOfStylesPerClassName(nonMediaQueryCSS); - headStyles = headStyles.filter((style) => style.trim().length > 0); + nonInlineStylesToApply = nonInlineStylesToApply.filter( + (style) => style.trim().length > 0, + ); - const hasNonInlineStylesToApply = headStyles.length > 0; + const hasNonInlineStylesToApply = nonInlineStylesToApply.length > 0; let hasAppliedNonInlineStyles = false as boolean; - const childrenArray = React.Children.map(children, (child) => { - if (React.isValidElement(child)) { - const element = child; - - if (!hasAppliedNonInlineStyles && hasNonInlineStylesToApply) { - if ( - element.type === "head" || - (typeof element.type === "function" && - "name" in element.type && - element.type.name === "Head") - ) { - hasAppliedNonInlineStyles = true; - return processHead(processElement(element, nonMediaQueryTailwindStylesPerClass, nonEscapedMediaQueryClasses), headStyles); + const childrenArray = + React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + const element = child; + + if (!hasAppliedNonInlineStyles && hasNonInlineStylesToApply) { + if ( + element.type === "head" || + (typeof element.type === "function" && + "name" in element.type && + element.type.name === "Head") + ) { + hasAppliedNonInlineStyles = true; + return processHead( + processElement( + element, + nonMediaQueryTailwindStylesPerClass, + nonEscapedMediaQueryClasses, + ), + nonInlineStylesToApply, + ); + } } - } - return processElement( - element, - nonMediaQueryTailwindStylesPerClass, - nonEscapedMediaQueryClasses, - ); - } - }) ?? []; + return processElement( + element, + nonMediaQueryTailwindStylesPerClass, + nonEscapedMediaQueryClasses, + ); + } + }) ?? []; if (hasNonInlineStylesToApply && !hasAppliedNonInlineStyles) { throw new Error( diff --git a/packages/tailwind/src/utils/get-css-class-properties-map.ts b/packages/tailwind/src/utils/get-map-of-styles-per-class-name.ts similarity index 75% rename from packages/tailwind/src/utils/get-css-class-properties-map.ts rename to packages/tailwind/src/utils/get-map-of-styles-per-class-name.ts index 7100a18e91..db84795779 100644 --- a/packages/tailwind/src/utils/get-css-class-properties-map.ts +++ b/packages/tailwind/src/utils/get-map-of-styles-per-class-name.ts @@ -1,4 +1,6 @@ -export const getStylesPerClassMap = (css: string): Record => { +export const getMapOfStylesPerClassName = ( + css: string, +): Record => { const map = {} as Record; for (const [_match, className, contents] of css.matchAll( /\s*\.([\S]+)\s*{([^}]*)}/gm, diff --git a/packages/tailwind/src/utils/index.ts b/packages/tailwind/src/utils/index.ts index 06ae9dab29..3a75295e71 100644 --- a/packages/tailwind/src/utils/index.ts +++ b/packages/tailwind/src/utils/index.ts @@ -1,5 +1,8 @@ export * from "./get-css-for-markup"; export * from "./escape-class-name"; -export * from "./get-css-class-properties-map"; +export * from "./sanitize-class-name"; +export * from "./get-map-of-styles-per-class-name"; +export * from "./use-rgb-non-spaced-syntax"; +export * from "./quick-safe-render-to-string"; export * from "./css-to-jsx-style"; export * from "./minify-css"; From 777f897f829c1df1c72986c2ba82b2dc0803b1bc Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 8 Mar 2024 13:16:09 -0300 Subject: [PATCH 4/9] chore(tailwind): Improve a comment on the processElement --- packages/tailwind/src/tailwind.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 9944c29838..33d6437a32 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -44,7 +44,10 @@ function processElement( const styles = [] as string[]; classNames.forEach((className) => { - /* escape all unallowed characters in css class selectors */ + /* + We need to first escape the original className used here because, since Tailwind escapes + class names on the CSS, the map of styles per class name will have escaped class names as keys. + */ const tailwindEscapedClassName = escapeClassName(className); // no need to filter in for media query classes since it is going to keep these classes // as custom since they are not going to be in the markup map of styles From 90644f3abea6d93b746a84991fb5618170d89496 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 8 Mar 2024 15:29:14 -0300 Subject: [PATCH 5/9] feat(tailwind): More organized appraoch on element processing and style inlining --- .../tailwind/src/hooks/use-style-inlining.ts | 32 ++ .../src/hooks/use-tailwind-styles.spec.tsx | 39 +++ .../src/hooks/use-tailwind-styles.tsx | 58 ++++ packages/tailwind/src/tailwind.tsx | 278 ++++++------------ .../css-to-jsx-style.spec.ts | 0 .../{ => compatibility}/css-to-jsx-style.ts | 0 .../escape-class-name.spec.ts | 0 .../{ => compatibility}/escape-class-name.ts | 3 +- .../make-all-rule-properties-important.ts | 10 + .../sanitize-class-name.spec.ts | 0 .../sanitize-class-name.ts | 0 .../compatibility/sanitize-rule-selector.ts | 8 + .../src/utils/compatibility/unescape-class.ts | 3 + .../use-rgb-non-spaced-syntax.spec.ts | 0 .../use-rgb-non-spaced-syntax.ts | 0 .../separate-media-queries-from-css.ts | 17 ++ .../src/utils/{ => css}/minify-css.ts | 0 packages/tailwind/src/utils/css/rules-for.ts | 19 ++ .../utils/get-map-of-styles-per-class-name.ts | 14 - packages/tailwind/src/utils/index.ts | 8 - .../get-css-for-markup.spec.tsx | 2 +- .../{ => tailwindcss}/get-css-for-markup.ts | 2 +- 22 files changed, 277 insertions(+), 216 deletions(-) create mode 100644 packages/tailwind/src/hooks/use-style-inlining.ts create mode 100644 packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx create mode 100644 packages/tailwind/src/hooks/use-tailwind-styles.tsx rename packages/tailwind/src/utils/{ => compatibility}/css-to-jsx-style.spec.ts (100%) rename packages/tailwind/src/utils/{ => compatibility}/css-to-jsx-style.ts (100%) rename packages/tailwind/src/utils/{ => compatibility}/escape-class-name.spec.ts (100%) rename packages/tailwind/src/utils/{ => compatibility}/escape-class-name.ts (95%) create mode 100644 packages/tailwind/src/utils/compatibility/make-all-rule-properties-important.ts rename packages/tailwind/src/utils/{ => compatibility}/sanitize-class-name.spec.ts (100%) rename packages/tailwind/src/utils/{ => compatibility}/sanitize-class-name.ts (100%) create mode 100644 packages/tailwind/src/utils/compatibility/sanitize-rule-selector.ts create mode 100644 packages/tailwind/src/utils/compatibility/unescape-class.ts rename packages/tailwind/src/utils/{ => compatibility}/use-rgb-non-spaced-syntax.spec.ts (100%) rename packages/tailwind/src/utils/{ => compatibility}/use-rgb-non-spaced-syntax.ts (100%) create mode 100644 packages/tailwind/src/utils/css/media-queries/separate-media-queries-from-css.ts rename packages/tailwind/src/utils/{ => css}/minify-css.ts (100%) create mode 100644 packages/tailwind/src/utils/css/rules-for.ts delete mode 100644 packages/tailwind/src/utils/get-map-of-styles-per-class-name.ts delete mode 100644 packages/tailwind/src/utils/index.ts rename packages/tailwind/src/utils/{ => tailwindcss}/get-css-for-markup.spec.tsx (95%) rename packages/tailwind/src/utils/{ => tailwindcss}/get-css-for-markup.ts (95%) diff --git a/packages/tailwind/src/hooks/use-style-inlining.ts b/packages/tailwind/src/hooks/use-style-inlining.ts new file mode 100644 index 0000000000..291c783b1c --- /dev/null +++ b/packages/tailwind/src/hooks/use-style-inlining.ts @@ -0,0 +1,32 @@ +/** + * Creates a style inlining function that converts an element's className into inlined React styles - based on the + * {@link stylePerClass} map - for all classes also. + * + * Also returns residual classes that could not be found on the map. + */ +export function useStyleInlining( + stylePerClass: Record +) { + return (className: string) => { + const classes = className.split(' '); + + const residualClasses = []; + let styles: React.CSSProperties = {}; + + for (const singleClass of classes) { + if (singleClass in stylePerClass) { + styles = { + ...styles, + ...stylePerClass[singleClass] + }; + } else { + residualClasses.push(singleClass); + } + } + + return { + styles, + residualClassName: residualClasses.join(' '), + } + } +} diff --git a/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx b/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx new file mode 100644 index 0000000000..15f9e7aa35 --- /dev/null +++ b/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx @@ -0,0 +1,39 @@ +import { useTailwindStyles } from "./use-tailwind-styles"; + +describe("useTailwindStyles()", () => { + test("with basic media queries and nested elements", () => { + const node = ( +
+ Well, hello friends! +
+ ); + + expect(useTailwindStyles(node, {})).toEqual({ + stylePerClassMap: { + "w-full": { width: "100%" }, + "bg-red-500": { backgroundColor: "rgb(239,68,68)" }, + "text-red-100": { color: "rgb(254,226,226)" }, + }, + sanitizedMediaQueries: [".md_w-250px {width: 250px!important}\n"], + nonInlinableClasses: ["md:w-[250px]"], + }); + }); + + test.only("with more media queries", () => { + const node = ( +
+ ); + + expect(useTailwindStyles(node, {})).toEqual({ + stylePerClassMap: { + "bg-red-200": { backgroundColor: "rgb(254,202,202)" }, + }, + sanitizedMediaQueries: [ + "@media (min-width: 640px) {.sm_bg-red-300 {background-color: rgb(252,165,165)!important}}", + "@media (min-width: 768px) {.md_bg-red-400 {background-color: rgb(248,113,113)!important}}", + "@media (min-width: 1024px) {.lg_bg-red-500 {background-color: rgb(239,68,68)!important}}", + ], + nonInlinableClasses: ["sm:bg-red-300", "md:bg-red-400", "lg:bg-red-500"], + }); + }); +}); diff --git a/packages/tailwind/src/hooks/use-tailwind-styles.tsx b/packages/tailwind/src/hooks/use-tailwind-styles.tsx new file mode 100644 index 0000000000..54415809e0 --- /dev/null +++ b/packages/tailwind/src/hooks/use-tailwind-styles.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import type { TailwindConfig } from "../tailwind"; +import { separateMediaQueriesFromCSS } from "../utils/css/media-queries/separate-media-queries-from-css"; +import { rulesFor } from "../utils/css/rules-for"; +import { quickSafeRenderToString } from "../utils/quick-safe-render-to-string"; +import { getCssForMarkup } from "../utils/tailwindcss/get-css-for-markup"; +import { useRgbNonSpacedSyntax } from "../utils/compatibility/use-rgb-non-spaced-syntax"; +import { cssToJsxStyle } from "../utils/compatibility/css-to-jsx-style"; +import { unescapeClass } from "../utils/compatibility/unescape-class"; +import { sanitizeRuleSelector } from "../utils/compatibility/sanitize-rule-selector"; +import { makeAllRulePropertiesImportant } from "../utils/compatibility/make-all-rule-properties-important"; + +/** + * Gets all the necessary information from the node and the Tailwind config to be able + * to apply all the Tailwind styles. + */ +export function useTailwindStyles( + node: React.ReactNode, + config: TailwindConfig, +) { + const markup = quickSafeRenderToString(<>{node}); + const css = useRgbNonSpacedSyntax(getCssForMarkup(markup, config)); + + const [cssWithoutMediaQueries, mediaQueries] = + separateMediaQueriesFromCSS(css); + + const stylePerClassMap: Record = {}; + for (const rule of rulesFor(cssWithoutMediaQueries)) { + const unescapedClass = unescapeClass(rule.selector); + stylePerClassMap[unescapedClass] = cssToJsxStyle(rule.content); + } + + const nonInlinableClasses: string[] = []; + + const sanitizedMediaQueries = mediaQueries.map((mediaQuery) => { + let sanitizedMediaQuery = mediaQuery; + for (const rule of rulesFor(mediaQuery)) { + nonInlinableClasses.push(unescapeClass(rule.selector)); + + sanitizedMediaQuery = sanitizedMediaQuery.replace( + rule.value, + rule.value + .replace(rule.selector, sanitizeRuleSelector(rule.selector)) + .replace(rule.content, makeAllRulePropertiesImportant(rule.content)) + .trim(), + ); + } + return sanitizedMediaQuery + .replace(/(\r\n|\r|\n)+/gm, "") + .replace(/\s+/gm, " "); + }); + + return { + stylePerClassMap, + sanitizedMediaQueries, + nonInlinableClasses, + }; +} diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 33d6437a32..ca4adb5f8c 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -1,18 +1,9 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ import * as React from "react"; import type { Config as TailwindOriginalConfig } from "tailwindcss"; -import type { HeadProps } from "@react-email/head"; -import { - getMapOfStylesPerClassName, - useRgbNonSpacedSyntax, - quickSafeRenderToString, - minifyCss, - getCssForMarkup, - cssToJsxStyle, - escapeClassName, - sanitizeClassName, -} from "./utils"; +import { useTailwindStyles } from "./hooks/use-tailwind-styles"; +import { useStyleInlining } from "./hooks/use-style-inlining"; +import { sanitizeClassName } from "./utils/compatibility/sanitize-class-name"; +import { minifyCss } from "./utils/css/minify-css"; export type TailwindConfig = Omit; @@ -21,189 +12,94 @@ export interface TailwindProps { config?: TailwindConfig; } -function processElement( - element: React.ReactElement, - nonMediaQueryTailwindStylesPerClass: Record, - nonEscapedMediaQueryClasses: string[], -): React.ReactElement { - let modifiedElement = element; - - let resultingClassName = modifiedElement.props.className as - | string - | undefined; - let resultingStyle = modifiedElement.props.style as - | React.CSSProperties - | undefined; - let resultingChildren: React.ReactNode[] = []; - - if (modifiedElement.props.className) { - const fullClassName = modifiedElement.props.className as string; - const classNames = fullClassName.split(" "); - const classNamesToKeep = [] as string[]; - - const styles = [] as string[]; - - classNames.forEach((className) => { - /* - We need to first escape the original className used here because, since Tailwind escapes - class names on the CSS, the map of styles per class name will have escaped class names as keys. - */ - const tailwindEscapedClassName = escapeClassName(className); - // no need to filter in for media query classes since it is going to keep these classes - // as custom since they are not going to be in the markup map of styles - if ( - typeof nonMediaQueryTailwindStylesPerClass[tailwindEscapedClassName] === - "undefined" - ) { - if (nonEscapedMediaQueryClasses.includes(className)) { - classNamesToKeep.push(sanitizeClassName(className)); - } else { - classNamesToKeep.push(className); - } - } else { - styles.push( - `${nonMediaQueryTailwindStylesPerClass[tailwindEscapedClassName]};`, - ); - } - }); +interface EmailElementProps { + children?: React.ReactNode; + className?: string; + style?: React.CSSProperties; +} - resultingStyle = { - ...(modifiedElement.props.style as Record), - ...cssToJsxStyle(styles.join(" ")), - }; - resultingClassName = - classNamesToKeep.length > 0 ? classNamesToKeep.join(" ") : undefined; - } +export const Tailwind: React.FC = ({ children, config }) => { + const { stylePerClassMap, nonInlinableClasses, sanitizedMediaQueries } = + useTailwindStyles(children, config ?? {}); - if (modifiedElement.props.children) { - resultingChildren = React.Children.toArray( - modifiedElement.props.children, - ).map((child) => { - if (React.isValidElement(child)) { - return processElement( - child, - nonMediaQueryTailwindStylesPerClass, - nonEscapedMediaQueryClasses, - ); - } - return child; - }); - } + const inline = useStyleInlining(stylePerClassMap); - modifiedElement = React.cloneElement( - modifiedElement, - { - ...modifiedElement.props, - className: resultingClassName, - // passing in style here as undefined may mess up - // the rendering process of child components - ...(typeof resultingStyle === "undefined" - ? {} - : { style: resultingStyle }), - }, - ...resultingChildren, + const nonInlineStylesToApply = sanitizedMediaQueries.filter( + (style) => style.trim().length > 0, ); - // if this is a component, then we render it and recurse it through processElement - if (typeof modifiedElement.type === "function") { - const component = modifiedElement.type as React.FC; - const renderedComponent = component(modifiedElement.props); - if (React.isValidElement(renderedComponent)) { - modifiedElement = processElement( - renderedComponent, - nonMediaQueryTailwindStylesPerClass, - nonEscapedMediaQueryClasses, + function processElement( + element: React.ReactElement, + ): React.ReactElement { + const propsToOverwrite = {} as Partial; + + if (element.props.children) { + propsToOverwrite.children = React.Children.map( + element.props.children, + (child) => { + if (React.isValidElement(child)) { + return processElement(child); + } + + return child; + }, ); } - } - - return modifiedElement; -} - -type HeadElement = React.ReactElement< - HeadProps, - string | React.JSXElementConstructor ->; - -function processHead( - headElement: HeadElement, - nonInlineStylesToApply: string[], -): React.ReactElement { - /* only minify here since it is the only place that is going to be in the DOM */ - const styleElement = ( - - ); - - return React.cloneElement( - headElement, - headElement.props, - ...React.Children.toArray(headElement.props.children), - styleElement, - ); -} -export const Tailwind: React.FC = ({ children, config }) => { - let nonInlineStylesToApply: string[] = []; - - const markupWithTailwindClasses = quickSafeRenderToString(<>{children}); - const markupCSS = useRgbNonSpacedSyntax( - getCssForMarkup(markupWithTailwindClasses, config), - ); + if (element.props.className) { + const { styles, residualClassName } = inline(element.props.className); + propsToOverwrite.style = { + ...element.props.style, + ...styles, + }; + if (residualClassName.trim().length > 0) { + propsToOverwrite.className = residualClassName; + /* + We sanitize only the class names of Tailwind classes that we are not going to inline + to avoid unpredictable behavior on the user's code. If we did sanitize all class names + a user-defined class could end up also being sanitized which would lead to unexpected + behavior and bugs that are hard to track. + */ + for (const singleClass of nonInlinableClasses) { + propsToOverwrite.className = propsToOverwrite.className.replace( + singleClass, + sanitizeClassName(singleClass), + ); + } + } else { + propsToOverwrite.className = undefined; + } + } - let nonMediaQueryCSS = markupCSS; - const nonEscapedMediaQueryClasses = [] as string[]; - - for (const [mediaQuery, content] of markupCSS.matchAll( - /@media\s*\(.*\)\s*{(\s*\..*\s*{[\s\S]*}\s*)}/gm, - )) { - nonMediaQueryCSS = nonMediaQueryCSS.replace(mediaQuery, ""); - - let finalMediaQuery = mediaQuery; - - for (const [ - fullRule, - escapedRuleClassName, - ruleContent, - ] of content.matchAll(/\s*\.([\S]+)\s*{([^}]*)}/gm)) { - const ruleClassName = escapedRuleClassName.replaceAll(/\\[0-9]|\\/g, ""); - nonEscapedMediaQueryClasses.push(ruleClassName); - - finalMediaQuery = finalMediaQuery.replace( - fullRule, - fullRule - .replace(escapedRuleClassName, sanitizeClassName(ruleClassName)) - .replace( - ruleContent, - ruleContent - .split(";") - .map((propertyDeclaration) => - propertyDeclaration.endsWith("!important") - ? propertyDeclaration.trim() - : `${propertyDeclaration.trim()}!important`, - ) - .join(";"), - ) - .replace(/[\r\n|\r|\n]+/g, "") // remove line breaks - .replace(/\s+/g, " "), - ); + const newProps = { + ...element.props, + ...propsToOverwrite, + }; + const newChildren = propsToOverwrite.children + ? propsToOverwrite.children + : element.props.children; + + if (typeof element.type === "function") { + const component = element.type as React.FC; + const renderedComponent = component({ + ...element.props, + ...propsToOverwrite, + }); + + if (React.isValidElement(renderedComponent)) { + return processElement(renderedComponent); + } } - nonInlineStylesToApply.push(finalMediaQuery); + return React.cloneElement(element, newProps, newChildren); } - const nonMediaQueryTailwindStylesPerClass = - getMapOfStylesPerClassName(nonMediaQueryCSS); - - nonInlineStylesToApply = nonInlineStylesToApply.filter( - (style) => style.trim().length > 0, - ); - const hasNonInlineStylesToApply = nonInlineStylesToApply.length > 0; - let hasAppliedNonInlineStyles = false as boolean; + const childrenArray = React.Children.map(children, (child) => { - if (React.isValidElement(child)) { + if (React.isValidElement(child)) { const element = child; if (!hasAppliedNonInlineStyles && hasNonInlineStylesToApply) { @@ -214,22 +110,22 @@ export const Tailwind: React.FC = ({ children, config }) => { element.type.name === "Head") ) { hasAppliedNonInlineStyles = true; - return processHead( - processElement( - element, - nonMediaQueryTailwindStylesPerClass, - nonEscapedMediaQueryClasses, - ), - nonInlineStylesToApply, + + /* only minify here since it is the only place that is going to be in the DOM */ + const styleElement = ( + + ); + + return React.cloneElement( + element, + element.props, + element.props.children, + styleElement, ); } } - return processElement( - element, - nonMediaQueryTailwindStylesPerClass, - nonEscapedMediaQueryClasses, - ); + return processElement(element); } }) ?? []; diff --git a/packages/tailwind/src/utils/css-to-jsx-style.spec.ts b/packages/tailwind/src/utils/compatibility/css-to-jsx-style.spec.ts similarity index 100% rename from packages/tailwind/src/utils/css-to-jsx-style.spec.ts rename to packages/tailwind/src/utils/compatibility/css-to-jsx-style.spec.ts diff --git a/packages/tailwind/src/utils/css-to-jsx-style.ts b/packages/tailwind/src/utils/compatibility/css-to-jsx-style.ts similarity index 100% rename from packages/tailwind/src/utils/css-to-jsx-style.ts rename to packages/tailwind/src/utils/compatibility/css-to-jsx-style.ts diff --git a/packages/tailwind/src/utils/escape-class-name.spec.ts b/packages/tailwind/src/utils/compatibility/escape-class-name.spec.ts similarity index 100% rename from packages/tailwind/src/utils/escape-class-name.spec.ts rename to packages/tailwind/src/utils/compatibility/escape-class-name.spec.ts diff --git a/packages/tailwind/src/utils/escape-class-name.ts b/packages/tailwind/src/utils/compatibility/escape-class-name.ts similarity index 95% rename from packages/tailwind/src/utils/escape-class-name.ts rename to packages/tailwind/src/utils/compatibility/escape-class-name.ts index aee38e09b4..379219728e 100644 --- a/packages/tailwind/src/utils/escape-class-name.ts +++ b/packages/tailwind/src/utils/compatibility/escape-class-name.ts @@ -3,7 +3,8 @@ * CSS selectors by using the regex "[^a-zA-Z0-9\-_]". * * Also does a bit more trickery to avoid escaping already - * escaped characters.8 */ + * escaped characters. + */ export const escapeClassName = (className: string) => { return className.replace( /* we need this look ahead capturing group to avoid using negative look behinds */ diff --git a/packages/tailwind/src/utils/compatibility/make-all-rule-properties-important.ts b/packages/tailwind/src/utils/compatibility/make-all-rule-properties-important.ts new file mode 100644 index 0000000000..1ac0a49e3e --- /dev/null +++ b/packages/tailwind/src/utils/compatibility/make-all-rule-properties-important.ts @@ -0,0 +1,10 @@ +export function makeAllRulePropertiesImportant(ruleContent: string) { + return ruleContent + .split(";") + .map((declaration) => + declaration.endsWith("!important") + ? declaration.trim() + : `${declaration.trim()}!important`, + ) + .join(";"); +} diff --git a/packages/tailwind/src/utils/sanitize-class-name.spec.ts b/packages/tailwind/src/utils/compatibility/sanitize-class-name.spec.ts similarity index 100% rename from packages/tailwind/src/utils/sanitize-class-name.spec.ts rename to packages/tailwind/src/utils/compatibility/sanitize-class-name.spec.ts diff --git a/packages/tailwind/src/utils/sanitize-class-name.ts b/packages/tailwind/src/utils/compatibility/sanitize-class-name.ts similarity index 100% rename from packages/tailwind/src/utils/sanitize-class-name.ts rename to packages/tailwind/src/utils/compatibility/sanitize-class-name.ts diff --git a/packages/tailwind/src/utils/compatibility/sanitize-rule-selector.ts b/packages/tailwind/src/utils/compatibility/sanitize-rule-selector.ts new file mode 100644 index 0000000000..470ffb4837 --- /dev/null +++ b/packages/tailwind/src/utils/compatibility/sanitize-rule-selector.ts @@ -0,0 +1,8 @@ +import { sanitizeClassName } from "./sanitize-class-name"; +import { unescapeClass } from "./unescape-class"; + +export function sanitizeRuleSelector(classSelector: string) { + const unescapedClass = unescapeClass(classSelector); + + return sanitizeClassName(unescapedClass); +} diff --git a/packages/tailwind/src/utils/compatibility/unescape-class.ts b/packages/tailwind/src/utils/compatibility/unescape-class.ts new file mode 100644 index 0000000000..5126dca417 --- /dev/null +++ b/packages/tailwind/src/utils/compatibility/unescape-class.ts @@ -0,0 +1,3 @@ +export function unescapeClass(singleClass: string) { + return singleClass.replaceAll(/\\[0-9]|\\/g, ""); +} diff --git a/packages/tailwind/src/utils/use-rgb-non-spaced-syntax.spec.ts b/packages/tailwind/src/utils/compatibility/use-rgb-non-spaced-syntax.spec.ts similarity index 100% rename from packages/tailwind/src/utils/use-rgb-non-spaced-syntax.spec.ts rename to packages/tailwind/src/utils/compatibility/use-rgb-non-spaced-syntax.spec.ts diff --git a/packages/tailwind/src/utils/use-rgb-non-spaced-syntax.ts b/packages/tailwind/src/utils/compatibility/use-rgb-non-spaced-syntax.ts similarity index 100% rename from packages/tailwind/src/utils/use-rgb-non-spaced-syntax.ts rename to packages/tailwind/src/utils/compatibility/use-rgb-non-spaced-syntax.ts diff --git a/packages/tailwind/src/utils/css/media-queries/separate-media-queries-from-css.ts b/packages/tailwind/src/utils/css/media-queries/separate-media-queries-from-css.ts new file mode 100644 index 0000000000..1cf73c6c95 --- /dev/null +++ b/packages/tailwind/src/utils/css/media-queries/separate-media-queries-from-css.ts @@ -0,0 +1,17 @@ +export function separateMediaQueriesFromCSS( + css: string +): [cssWithoutMediaQueries: string, mediaQueries: string[]] { + let cssWithoutMediaQueries = css; + const mediaQueries: string[] = []; + + for (const match of css.matchAll( + /@media\s*\(.*\)\s*{\s*\..*\s*{[\s\S]*?}\s*}/gm, + )) { + const [mediaQuery] = match; + cssWithoutMediaQueries = cssWithoutMediaQueries.replace(mediaQuery, ""); + + mediaQueries.push(mediaQuery); + } + + return [cssWithoutMediaQueries, mediaQueries]; +} diff --git a/packages/tailwind/src/utils/minify-css.ts b/packages/tailwind/src/utils/css/minify-css.ts similarity index 100% rename from packages/tailwind/src/utils/minify-css.ts rename to packages/tailwind/src/utils/css/minify-css.ts diff --git a/packages/tailwind/src/utils/css/rules-for.ts b/packages/tailwind/src/utils/css/rules-for.ts new file mode 100644 index 0000000000..4d4845d9a3 --- /dev/null +++ b/packages/tailwind/src/utils/css/rules-for.ts @@ -0,0 +1,19 @@ +export interface Rule { + value: string, + selector: string, + content: string +}; + +export function* rulesFor(cssWithRules: string) { + for (const [ + fullRule, + selector, + content, + ] of cssWithRules.matchAll(/\s*\.([\S]+)\s*{([^}]*)}/gm)) { + yield { + value: fullRule, + selector, + content + } satisfies Rule; + } +} diff --git a/packages/tailwind/src/utils/get-map-of-styles-per-class-name.ts b/packages/tailwind/src/utils/get-map-of-styles-per-class-name.ts deleted file mode 100644 index db84795779..0000000000 --- a/packages/tailwind/src/utils/get-map-of-styles-per-class-name.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const getMapOfStylesPerClassName = ( - css: string, -): Record => { - const map = {} as Record; - for (const [_match, className, contents] of css.matchAll( - /\s*\.([\S]+)\s*{([^}]*)}/gm, - )) { - map[className.trim()] = contents - .replace(/^\n+/, "") - .replace(/\n+$/, "") - .trim(); - } - return map; -}; diff --git a/packages/tailwind/src/utils/index.ts b/packages/tailwind/src/utils/index.ts deleted file mode 100644 index 3a75295e71..0000000000 --- a/packages/tailwind/src/utils/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from "./get-css-for-markup"; -export * from "./escape-class-name"; -export * from "./sanitize-class-name"; -export * from "./get-map-of-styles-per-class-name"; -export * from "./use-rgb-non-spaced-syntax"; -export * from "./quick-safe-render-to-string"; -export * from "./css-to-jsx-style"; -export * from "./minify-css"; diff --git a/packages/tailwind/src/utils/get-css-for-markup.spec.tsx b/packages/tailwind/src/utils/tailwindcss/get-css-for-markup.spec.tsx similarity index 95% rename from packages/tailwind/src/utils/get-css-for-markup.spec.tsx rename to packages/tailwind/src/utils/tailwindcss/get-css-for-markup.spec.tsx index e336c1c3e7..99400d5fcd 100644 --- a/packages/tailwind/src/utils/get-css-for-markup.spec.tsx +++ b/packages/tailwind/src/utils/tailwindcss/get-css-for-markup.spec.tsx @@ -1,6 +1,6 @@ import { renderToStaticMarkup } from "react-dom/server"; +import { minifyCss } from "../css/minify-css"; import { getCssForMarkup } from "./get-css-for-markup"; -import { minifyCss } from "./minify-css"; // these tests will fail if the minifyCSS is broken as well // the reason we use it here is because it just makes it simpler to compare diff --git a/packages/tailwind/src/utils/get-css-for-markup.ts b/packages/tailwind/src/utils/tailwindcss/get-css-for-markup.ts similarity index 95% rename from packages/tailwind/src/utils/get-css-for-markup.ts rename to packages/tailwind/src/utils/tailwindcss/get-css-for-markup.ts index 6730150cba..3f013a7002 100644 --- a/packages/tailwind/src/utils/get-css-for-markup.ts +++ b/packages/tailwind/src/utils/tailwindcss/get-css-for-markup.ts @@ -2,7 +2,7 @@ import tailwindcss from "tailwindcss"; import type { CorePluginsConfig } from "tailwindcss/types/config"; import postcssCssVariables from "postcss-css-variables"; import postcss from "postcss"; -import type { TailwindConfig } from "../tailwind"; +import type { TailwindConfig } from "../../tailwind"; declare global { // eslint-disable-next-line no-var From d49cd7f06831e25454ff812932b69557b556dbda Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 11 Mar 2024 07:51:01 -0300 Subject: [PATCH 6/9] chore(tailwind): Format --- .../tailwind/src/hooks/use-style-inlining.ts | 22 +++++++++---------- .../separate-media-queries-from-css.ts | 2 +- packages/tailwind/src/utils/css/rules-for.ts | 18 +++++++-------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/tailwind/src/hooks/use-style-inlining.ts b/packages/tailwind/src/hooks/use-style-inlining.ts index 291c783b1c..0de5214d46 100644 --- a/packages/tailwind/src/hooks/use-style-inlining.ts +++ b/packages/tailwind/src/hooks/use-style-inlining.ts @@ -1,14 +1,14 @@ /** - * Creates a style inlining function that converts an element's className into inlined React styles - based on the - * {@link stylePerClass} map - for all classes also. - * - * Also returns residual classes that could not be found on the map. - */ + * Creates a style inlining function that converts an element's className into inlined React styles - based on the + * {@link stylePerClass} map - for all classes also. + * + * Also returns residual classes that could not be found on the map. + */ export function useStyleInlining( - stylePerClass: Record + stylePerClass: Record, ) { return (className: string) => { - const classes = className.split(' '); + const classes = className.split(" "); const residualClasses = []; let styles: React.CSSProperties = {}; @@ -17,7 +17,7 @@ export function useStyleInlining( if (singleClass in stylePerClass) { styles = { ...styles, - ...stylePerClass[singleClass] + ...stylePerClass[singleClass], }; } else { residualClasses.push(singleClass); @@ -26,7 +26,7 @@ export function useStyleInlining( return { styles, - residualClassName: residualClasses.join(' '), - } - } + residualClassName: residualClasses.join(" "), + }; + }; } diff --git a/packages/tailwind/src/utils/css/media-queries/separate-media-queries-from-css.ts b/packages/tailwind/src/utils/css/media-queries/separate-media-queries-from-css.ts index 1cf73c6c95..60a7aa8220 100644 --- a/packages/tailwind/src/utils/css/media-queries/separate-media-queries-from-css.ts +++ b/packages/tailwind/src/utils/css/media-queries/separate-media-queries-from-css.ts @@ -1,5 +1,5 @@ export function separateMediaQueriesFromCSS( - css: string + css: string, ): [cssWithoutMediaQueries: string, mediaQueries: string[]] { let cssWithoutMediaQueries = css; const mediaQueries: string[] = []; diff --git a/packages/tailwind/src/utils/css/rules-for.ts b/packages/tailwind/src/utils/css/rules-for.ts index 4d4845d9a3..9011a96bc8 100644 --- a/packages/tailwind/src/utils/css/rules-for.ts +++ b/packages/tailwind/src/utils/css/rules-for.ts @@ -1,19 +1,17 @@ export interface Rule { - value: string, - selector: string, - content: string -}; + value: string; + selector: string; + content: string; +} export function* rulesFor(cssWithRules: string) { - for (const [ - fullRule, - selector, - content, - ] of cssWithRules.matchAll(/\s*\.([\S]+)\s*{([^}]*)}/gm)) { + for (const [fullRule, selector, content] of cssWithRules.matchAll( + /\s*\.([\S]+)\s*{([^}]*)}/gm, + )) { yield { value: fullRule, selector, - content + content, } satisfies Rule; } } From 06ed58e1cca2a22998dc2f3b69cd8bfda80f6dde Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 11 Mar 2024 07:58:16 -0300 Subject: [PATCH 7/9] chore(tailwind): Remove sneaky .only --- packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx b/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx index 15f9e7aa35..e1580f5535 100644 --- a/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx +++ b/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx @@ -19,7 +19,7 @@ describe("useTailwindStyles()", () => { }); }); - test.only("with more media queries", () => { + test("with more media queries", () => { const node = (
); From 62bdbdb543e3bc6c0a2632fff1c7ec4aefe7ce5f Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 11 Mar 2024 07:59:32 -0300 Subject: [PATCH 8/9] fix(tailwind): Wrong expected sanitized media queries from the useTailwindStyles hook --- packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx b/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx index e1580f5535..c28ab18649 100644 --- a/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx +++ b/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx @@ -14,7 +14,7 @@ describe("useTailwindStyles()", () => { "bg-red-500": { backgroundColor: "rgb(239,68,68)" }, "text-red-100": { color: "rgb(254,226,226)" }, }, - sanitizedMediaQueries: [".md_w-250px {width: 250px!important}\n"], + sanitizedMediaQueries: ["@media (min-width: 768px) {.md_w-250px {width: 250px!important}}"], nonInlinableClasses: ["md:w-[250px]"], }); }); From 78bcc36c9dccf5fba3b60dc33ecf1071a6d8856b Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 11 Mar 2024 08:03:06 -0300 Subject: [PATCH 9/9] chore(tailwind): Format --- packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx b/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx index c28ab18649..9ed8f51646 100644 --- a/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx +++ b/packages/tailwind/src/hooks/use-tailwind-styles.spec.tsx @@ -14,7 +14,9 @@ describe("useTailwindStyles()", () => { "bg-red-500": { backgroundColor: "rgb(239,68,68)" }, "text-red-100": { color: "rgb(254,226,226)" }, }, - sanitizedMediaQueries: ["@media (min-width: 768px) {.md_w-250px {width: 250px!important}}"], + sanitizedMediaQueries: [ + "@media (min-width: 768px) {.md_w-250px {width: 250px!important}}", + ], nonInlinableClasses: ["md:w-[250px]"], }); });