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 "`;
+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 >
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)