From e36c87211d693f5a09d5a8f62e7c53e35c537731 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 23 Jun 2025 10:00:54 -0300 Subject: [PATCH 1/4] add test to ensure the query works --- packages/tailwind/src/tailwind.spec.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/tailwind/src/tailwind.spec.tsx b/packages/tailwind/src/tailwind.spec.tsx index aafa0abb93..7c9fa24727 100644 --- a/packages/tailwind/src/tailwind.spec.tsx +++ b/packages/tailwind/src/tailwind.spec.tsx @@ -380,6 +380,18 @@ describe('Responsive styles', () => { ).toMatchSnapshot(); }); + // https://github.com/resend/react-email/issues/2297 + it('should work with max-* breakpoints', async () => { + const actualOutput = await render( + + +
Test
+
, + ); + + expect(actualOutput).toMatchSnapshot(); + }); + it('should not have duplicate media queries', async () => { const Body = (props: { className: string; children: React.ReactNode }) => { return {props.children}; From dbe1bb509b22654a4cc01d651cab95ea271324a9 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 23 Jun 2025 10:01:25 -0300 Subject: [PATCH 2/4] fix minifyCss removing spaces from media query parameters --- packages/tailwind/package.json | 1 + packages/tailwind/src/tailwind.tsx | 4 +- .../tailwind/src/utils/css/minify-css.spec.ts | 47 ++++++++++++ packages/tailwind/src/utils/css/minify-css.ts | 72 ++++++++++++++----- packages/tailwind/vite.config.ts | 1 + pnpm-lock.yaml | 3 + 6 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 packages/tailwind/src/utils/css/minify-css.spec.ts diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index fab11dce3b..6ce1be219c 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -58,6 +58,7 @@ "@vitejs/plugin-react": "4.4.1", "postcss": "8.5.3", "postcss-selector-parser": "7.1.0", + "postcss-value-parser": "4.2.0", "react-dom": "^19", "shelljs": "0.9.2", "tailwindcss": "3.4.10", diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 25ec946f02..ca347d2c00 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -79,9 +79,7 @@ export const Tailwind: React.FC = ({ children, config }) => { /* only minify here since it is the only place that is going to be in the DOM */ const styleElement = ( - + ); return React.cloneElement( diff --git a/packages/tailwind/src/utils/css/minify-css.spec.ts b/packages/tailwind/src/utils/css/minify-css.spec.ts new file mode 100644 index 0000000000..521f46bfd5 --- /dev/null +++ b/packages/tailwind/src/utils/css/minify-css.spec.ts @@ -0,0 +1,47 @@ +import { parse } from 'postcss'; +import { minifyCss } from './minify-css'; + +describe('minifyCss', () => { + it('should remove comments', () => { + const input = parse('body { color: red; /* This is a comment */ }'); + const expected = 'body{color:red}'; + expect(minifyCss(input)).toBe(expected); + }); + + it('should remove extra spaces after semicolons and colons', () => { + const input = parse('body { color: red; font-size: 16px; }'); + const expected = 'body{color:red;font-size:16px}'; + expect(minifyCss(input)).toBe(expected); + }); + + it('should remove extra spaces before and after brackets', () => { + const input = parse('body { color: red; } .class { margin: 10px; }'); + const expected = 'body{color:red}.class{margin:10px}'; + expect(minifyCss(input)).toBe(expected); + }); + + it('should handle multiple rules in a single string', () => { + const input = parse('body { color: red; } .class { margin: 10px; }'); + const expected = 'body{color:red}.class{margin:10px}'; + expect(minifyCss(input)).toBe(expected); + }); + + // https://github.com/resend/react-email/issues/2297 + it('should handle at rules with multiple parameters', () => { + const input = parse(`@media not all and (min-width:600px) { + .max-desktop_px-40 { + padding-left: 40px !important; + padding-right: 40px !important + } +}`); + const expected = + '@media not all and (min-width:600px){.max-desktop_px-40{padding-left:40px!important;padding-right:40px!important}}'; + expect(minifyCss(input)).toBe(expected); + }); + + it('should handle empty strings', () => { + const input = parse(''); + const expected = ''; + expect(minifyCss(input)).toBe(expected); + }); +}); diff --git a/packages/tailwind/src/utils/css/minify-css.ts b/packages/tailwind/src/utils/css/minify-css.ts index 2d7b7832be..13001b102a 100644 --- a/packages/tailwind/src/utils/css/minify-css.ts +++ b/packages/tailwind/src/utils/css/minify-css.ts @@ -1,21 +1,57 @@ -export const minifyCss = (css: string): string => { - // Thanks tw-to-css! - // from https://github.com/vinicoder/tw-to-css/blob/main/src/util/format-css.ts - return ( - css - // Remove comments - .replace(/\/\*[\s\S]*?\*\//gm, '') +import type { Root } from 'postcss'; +import selectorParser from 'postcss-selector-parser'; +import valueParser from 'postcss-value-parser'; - // Remove extra spaces after semicolons and colons - .replace(/;\s+/gm, ';') - .replace(/:\s+/gm, ':') +function minifyValue(value: string) { + const parsed = valueParser(value.trim()); + parsed.walk((node) => { + if ('before' in node) node.before = ''; + if ('after' in node) node.after = ''; + if (node.type === 'space') node.value = ' '; + }); + return parsed.toString(); +} - // Remove extra spaces before and after brackets - .replace(/\)\s*{/gm, '){') // Remove spaces before opening curly brace after closing parenthesis - .replace(/\s+\(/gm, '(') // Remove spaces before opening parenthesis - .replace(/{\s+/gm, '{') // Remove spaces after opening curly brace - .replace(/}\s+/gm, '}') // Remove spaces before closing curly brace - .replace(/\s*{/gm, '{') // Remove spaces after opening curly brace - .replace(/;?\s*}/gm, '}') - ); // Remove extra spaces and semicolons before closing curly braces +export const minifyCss = (root: Root): string => { + const toMinify = root.clone(); + toMinify.walk((node) => { + if (node.type === 'comment') { + if (node.text[0] === '!') { + node.raws.before = ''; + node.raws.after = ''; + } else { + node.remove(); + } + } else if (node.type === 'atrule') { + node.raws = { + before: '', + after: '', + afterName: ' ', + }; + node.params = minifyValue(node.params); + } else if (node.type === 'decl') { + node.raws = { + before: '', + between: ':', + important: node.important + ? (node.raws.important?.replaceAll(' ', '') ?? '!important') + : undefined, + }; + node.value = minifyValue(node.value); + } else if (node.type === 'rule') { + node.raws = { before: '', between: '', after: '', semicolon: false }; + node.selector = selectorParser((selectorRoot) => { + selectorRoot.walk((selector) => { + selector.spaces = { before: '', after: '' }; + + if ('raws' in selector && selector.raws?.spaces) { + selector.raws.spaces = {}; + } + }); + }) + .processSync(node.selector) + .toString(); + } + }); + return toMinify.toString(); }; diff --git a/packages/tailwind/vite.config.ts b/packages/tailwind/vite.config.ts index c7fbbd5260..1ec93b241a 100644 --- a/packages/tailwind/vite.config.ts +++ b/packages/tailwind/vite.config.ts @@ -21,6 +21,7 @@ export default defineConfig({ // - tailwindcss // - postcss // - postcss-selector-parser + // - postcss-value-parser external: ['react', /^react\/.*/, 'react-dom', /react-dom\/.*/], }, lib: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc582ccb15..cc55c49448 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -916,6 +916,9 @@ importers: postcss-selector-parser: specifier: 7.1.0 version: 7.1.0 + postcss-value-parser: + specifier: 4.2.0 + version: 4.2.0 react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) From 6baa1441feb1c7356006b6bb2624ebaf88059429 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 23 Jun 2025 10:01:37 -0300 Subject: [PATCH 3/4] update snapshots --- .../src/__snapshots__/tailwind.spec.tsx.snap | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap index 76d7a2b1da..785b34cde5 100644 --- a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap +++ b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`Custom plugins config > should be able to use custom plugins 1`] = `"
"`; -exports[`Custom plugins config > should be able to use custom plugins with responsive styles 1`] = `"
"`; +exports[`Custom plugins config > should be able to use custom plugins with responsive styles 1`] = `"
"`; exports[`Custom theme config > should be able to use custom border radius 1`] = `"
"`; @@ -14,11 +14,11 @@ exports[`Custom theme config > should be able to use custom spacing 1`] = `" should be able to use custom text alignment 1`] = `"
"`; -exports[`Responsive styles > should add css to and keep responsive class names 1`] = `"
"`; +exports[`Responsive styles > should add css to and keep responsive class names 1`] = `"
"`; -exports[`Responsive styles > should not have duplicate media queries 1`] = `"
"`; +exports[`Responsive styles > should not have duplicate media queries 1`] = `"
"`; -exports[`Responsive styles > should persist existing elements 1`] = `"
"`; +exports[`Responsive styles > should persist existing elements 1`] = `"
"`; exports[`Responsive styles > should throw an error when used without a 1`] = ` [Error: You are trying to use the following Tailwind classes that cannot be inlined: sm:bg-red-500. @@ -44,11 +44,13 @@ If you do already have a element at some depth, please file a bug https://github.com/resend/react-email/issues/new?assignees=&labels=Type%3A+Bug&projects=&template=1.bug_report.yml.] `; -exports[`Responsive styles > should work with arbitrarily deep (in the React tree) elements 1`] = `"
"`; +exports[`Responsive styles > should work with arbitrarily deep (in the React tree) elements 1`] = `"
"`; -exports[`Responsive styles > should work with arbitrarily deep (in the React tree) elements 2`] = `"
"`; +exports[`Responsive styles > should work with arbitrarily deep (in the React tree) elements 2`] = `"
"`; -exports[`Responsive styles > should work with relatively complex media query utilities 1`] = `"

I am some text

"`; +exports[`Responsive styles > should work with max-* breakpoints 1`] = `"
Test
"`; + +exports[`Responsive styles > should work with relatively complex media query utilities 1`] = `"

I am some text

"`; exports[`Tailwind component >