Skip to content

Commit

Permalink
fix(tailwind): User-defined CSS variables being replaced with undefin…
Browse files Browse the repository at this point in the history
…ed (#1587)
  • Loading branch information
gabrielmfern committed Oct 14, 2024
1 parent d2281cf commit 7daf214
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 72 deletions.
5 changes: 5 additions & 0 deletions .changeset/heavy-rings-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-email/tailwind": patch
---

Fixes CSS variables being replaced with `undefined`
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"@types/react-dom": "npm:[email protected]"
},
"patchedDependencies": {
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]"
}
Expand Down
2 changes: 0 additions & 2 deletions packages/tailwind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@
"@react-email/html": "workspace:*",
"@react-email/render": "workspace:*",
"@responsive-email/react-email": "0.0.3",
"@types/postcss-css-variables": "0.18.3",
"eslint-config-custom": "workspace:*",
"eslint-plugin-regex": "1.10.0",
"memfs": "4.6.0",
"postcss": "8.4.40",
"postcss-css-variables": "0.19.0",
"process": "^0.11.10",
"react-dom": "19.0.0-rc-187dd6a7-20240806",
"tailwindcss": "3.4.10",
Expand Down
82 changes: 82 additions & 0 deletions packages/tailwind/src/utils/css/css-variables-resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import postcss from "postcss";
import { cssVariablesResolver } from "./css-variables-resolver";

describe("cssVariablesResolver", () => {
const processor = postcss([cssVariablesResolver()]);

it("should work with simple css variables on a :root", () => {
const css = `:root {
--width: 100px;
}
.box {
width: var(--width);
}`;

expect(processor.process(css).css).toBe(`.box {
width: 100px;
}`);
});

it("should keep variable usages if it cant find their declaration", () => {
const result = processor.process(`.box {
width: var(--width);
}`);

expect(result.css).toBe(`.box {
width: var(--width);
}`);
});

it("should work with variables set in the same rule", () => {
const result = processor.process(`.box {
--width: 200px;
width: var(--width);
}
@media (min-width: 1280px) {
.xl\\:bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity))
}
}
`);
expect(result.css).toBe(`.box {
width: 200px;
}
@media (min-width: 1280px) {
.xl\\:bg-green-500 {
background-color: rgb(34 197 94 / 1)
}
}
`);
});

it("should work with different values between media queries", () => {
const css = `:root {
--width: 100px;
}
@media (max-width: 1000px) {
:root {
--width: 200px;
}
}
.box {
width: var(--width);
}`;

const result = processor.process(css);
expect(result.css).toBe(`@media (max-width: 1000px) {
.box {
width: 200px;
}
}
.box {
width: 100px;
}`);
});
});
121 changes: 121 additions & 0 deletions packages/tailwind/src/utils/css/css-variables-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {
type Plugin,
type Container,
type Document,
type Node,
type Declaration,
Rule,
rule as createRule,
decl as createDeclaration,
AtRule,
} from "postcss";

export const cssVariablesResolver = () => {
const removeIfEmptyRecursively = (node: Container | Document) => {
if (node.first === undefined) {
const parent = node.parent;
if (parent) {
node.remove();
removeIfEmptyRecursively(parent);
}
}
};

const doNodesMatch = (first: Node | undefined, second: Node | undefined) => {
if (first instanceof Rule && second instanceof Rule) {
return (
first.selector === second.selector ||
second.selector.includes("*") ||
second.selector.includes(":root")
);
}

return first === second;
};

return {
postcssPlugin: "CSS Variables Resolver",
Once(root) {
root.walkRules((rule) => {
const declarationsForAtRules = new Map<AtRule, Set<Declaration>>();
const valueReplacingInformation = new Set<{
declaration: Declaration;
newValue: string;
}>();

rule.walkDecls((decl) => {
if (/var\(--[^\s)]+\)/.test(decl.value)) {
/**
* @example ['var(--width)', 'var(--length)']
*/
const variablesUsed = /var\(--[^\s)]+\)/gm.exec(decl.value)!;
root.walkDecls((otherDecl) => {
if (/--[^\s]+/.test(otherDecl.prop)) {
const variable = `var(${otherDecl.prop})`;
if (
variablesUsed.includes(variable) &&
doNodesMatch(decl.parent, otherDecl.parent)
) {
if (
otherDecl.parent?.parent instanceof AtRule &&
otherDecl.parent !== decl.parent
) {
const atRule = otherDecl.parent.parent;

const clonedDeclaration = createDeclaration();
clonedDeclaration.prop = decl.prop;
clonedDeclaration.value = decl.value.replaceAll(
variable,
otherDecl.value,
);
clonedDeclaration.important = decl.important;
if (declarationsForAtRules.has(atRule)) {
declarationsForAtRules
.get(otherDecl.parent.parent)!
.add(clonedDeclaration);
} else {
declarationsForAtRules.set(
otherDecl.parent.parent,
new Set([clonedDeclaration]),
);
}
return;
}

valueReplacingInformation.add({
declaration: decl,
newValue: decl.value.replaceAll(variable, otherDecl.value),
});
}
}
});
}
});

for (const { declaration, newValue } of valueReplacingInformation) {
declaration.value = newValue;
}

for (const [atRule, declarations] of declarationsForAtRules.entries()) {
const equivalentRule = createRule();
equivalentRule.selector = rule.selector;
equivalentRule.append(...declarations);

atRule.append(equivalentRule);
}
});

// Removes all variable definitions and then removes the rules that are empty
root.walkDecls((decl) => {
if (/--[^\s]+/.test(decl.prop)) {
const parent = decl.parent;
decl.remove();
if (parent) {
removeIfEmptyRecursively(parent);
}
}
});
},
} satisfies Plugin;
};
4 changes: 2 additions & 2 deletions packages/tailwind/src/utils/tailwindcss/get-css-for-markup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import tailwindcss from "tailwindcss";
import type { CorePluginsConfig } from "tailwindcss/types/config";
import postcssCssVariables from "postcss-css-variables";
import postcss from "postcss";
import { cssVariablesResolver } from "../css/css-variables-resolver";
import type { TailwindConfig } from "../../tailwind";

declare global {
Expand Down Expand Up @@ -33,7 +33,7 @@ export const getCssForMarkup = async (
...tailwindConfig,
content: [{ raw: markup, extension: "html" }],
}) as postcss.AcceptedPlugin,
postcssCssVariables() as postcss.AcceptedPlugin,
cssVariablesResolver(),
]);
const result = await processor.process(
String.raw`
Expand Down
1 change: 0 additions & 1 deletion packages/tailwind/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export default defineConfig({
// in summary, this bundles the following since vite defaults to bundling
// - tailwindcss
// - postcss
// - postcss-css-variables
// - polyfill libraries
// - process
// - memfs
Expand Down
38 changes: 0 additions & 38 deletions patches/[email protected]

This file was deleted.

28 changes: 0 additions & 28 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7daf214

Please sign in to comment.