react-email
Version:
A live preview of your emails right in your browser.
237 lines (216 loc) • 6.7 kB
text/typescript
import {
type CssNode,
type Declaration,
generate,
type List,
type ListItem,
parse,
type Raw,
type SelectorList,
type Value,
walk,
} from 'css-tree';
interface VariableUse {
declaration: Declaration;
path: CssNode[];
fallback?: string;
variableName: string;
raw: string;
}
export interface VariableDefinition {
declaration: Declaration;
path: CssNode[];
variableName: string;
definition: string;
}
function doSelectorsIntersect(
first: SelectorList | Raw,
second: SelectorList | Raw,
): boolean {
const firstStringified = generate(first);
const secondStringified = generate(second);
if (firstStringified === secondStringified) {
return true;
}
let hasSomeUniversal = false;
const walker = (
node: CssNode,
_parentListItem: ListItem<CssNode>,
parentList: List<CssNode>,
) => {
if (hasSomeUniversal) return;
if (node.type === 'PseudoClassSelector' && node.name === 'root') {
hasSomeUniversal = true;
}
if (
node.type === 'TypeSelector' &&
node.name === '*' &&
parentList.size === 1
) {
hasSomeUniversal = true;
}
};
walk(first, walker);
walk(second, walker);
if (hasSomeUniversal) {
return true;
}
return false;
}
export function resolveAllCssVariables(node: CssNode) {
const variableDefinitions = new Set<VariableDefinition>();
const variableUses = new Set<VariableUse>();
const path: CssNode[] = [];
walk(node, {
leave() {
path.shift();
},
enter(node: CssNode) {
if (node.type === 'Declaration') {
const declaration = node;
// Ignores @layer (properties) { ... } to avoid variable resolution conflicts
if (
path.some(
(ancestor) =>
ancestor.type === 'Atrule' &&
ancestor.name === 'layer' &&
ancestor.prelude !== null &&
generate(ancestor.prelude).includes('properties'),
)
) {
path.unshift(node);
return;
}
if (/--[\S]+/.test(declaration.property)) {
variableDefinitions.add({
declaration,
path: [...path],
variableName: declaration.property,
definition: generate(declaration.value),
});
} else {
function parseVariableUsesFrom(node: CssNode) {
walk(node, {
visit: 'Function',
enter(funcNode) {
if (funcNode.name === 'var') {
const children = funcNode.children.toArray();
const name = generate(children[0]!);
const fallback =
// The second argument should be an "," Operator Node,
// such that the actual fallback is only in the third argument
children[2] ? generate(children[2]) : undefined;
variableUses.add({
declaration,
path: [...path],
fallback,
variableName: name,
raw: generate(funcNode),
});
if (fallback?.includes('var(')) {
const parsedFallback = parse(fallback, {
context: 'value',
});
parseVariableUsesFrom(parsedFallback);
}
}
},
});
}
parseVariableUsesFrom(declaration.value);
}
}
path.unshift(node);
},
});
for (const use of variableUses) {
let hasReplaced = false;
for (const definition of variableDefinitions) {
if (use.variableName !== definition.variableName) {
continue;
}
if (
use.path[0]?.type === 'Block' &&
use.path[1]?.type === 'Atrule' &&
use.path[2]?.type === 'Block' &&
use.path[3]?.type === 'Rule' &&
definition.path[0]!.type === 'Block' &&
definition.path[1]!.type === 'Rule' &&
doSelectorsIntersect(use.path[3].prelude, definition.path[1].prelude)
) {
use.declaration.value = parse(
generate(use.declaration.value).replaceAll(
use.raw,
definition.definition,
),
{
context: 'value',
},
) as Raw | Value;
hasReplaced = true;
break;
}
if (
use.path[0]?.type === 'Block' &&
use.path[1]?.type === 'Rule' &&
definition.path[0]?.type === 'Block' &&
definition.path[1]?.type === 'Rule' &&
doSelectorsIntersect(use.path[1].prelude, definition.path[1].prelude)
) {
use.declaration.value = parse(
generate(use.declaration.value).replaceAll(
use.raw,
definition.definition,
),
{
context: 'value',
},
) as Raw | Value;
hasReplaced = true;
break;
}
// Both use and definition live inside the same nested @media (or other
// at-rule) block of the same rule — e.g. Tailwind v4's
// .print_invert { @media print { --tw-invert: invert(100%); filter: var(--tw-invert,) ... } }
// The previous two checks only cover a Rule directly containing the
// declaration; this one covers Rule → Block → Atrule → Block → Declaration
// on both sides.
if (
use.path[0]?.type === 'Block' &&
use.path[1]?.type === 'Atrule' &&
use.path[2]?.type === 'Block' &&
use.path[3]?.type === 'Rule' &&
definition.path[0]?.type === 'Block' &&
definition.path[1]?.type === 'Atrule' &&
definition.path[2]?.type === 'Block' &&
definition.path[3]?.type === 'Rule' &&
use.path[1].name === definition.path[1].name &&
(use.path[1].prelude
? definition.path[1].prelude
? generate(use.path[1].prelude) ===
generate(definition.path[1].prelude)
: false
: definition.path[1].prelude === null) &&
doSelectorsIntersect(use.path[3].prelude, definition.path[3].prelude)
) {
use.declaration.value = parse(
generate(use.declaration.value).replaceAll(
use.raw,
definition.definition,
),
{
context: 'value',
},
) as Raw | Value;
hasReplaced = true;
break;
}
}
if (!hasReplaced && use.fallback) {
use.declaration.value = parse(
generate(use.declaration.value).replaceAll(use.raw, use.fallback),
{ context: 'value' },
) as Raw | Value;
}
}
}