@maizzle/framework
Version:
Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.
119 lines (118 loc) • 4.44 kB
JavaScript
import { walk } from "../utils/ast/walker.js";
import "../utils/ast/index.js";
import { compileTailwindCss } from "../utils/compileTailwindCss.js";
import { decodeStyleEntities } from "../utils/decodeStyleEntities.js";
import { dirname, relative, resolve } from "pathe";
//#region src/transformers/tailwindcss.ts
/**
* Check if CSS content uses Tailwind features that require source scanning.
*
* Only CSS that imports Tailwind (or @maizzle/tailwindcss) needs @source
* directives. Plain CSS without Tailwind imports doesn't need scanning
* and would pass through @source directives unconsumed.
*/
function usesTailwind(css) {
return /((@import|@reference)\s+["'](tailwindcss|@maizzle\/tailwindcss)|@tailwind\s)/.test(css);
}
/**
* Build @source directives for Tailwind CSS scanning.
*
* Configures two types of sources:
* 1. Exclusions for output dir and user-configured paths
* 2. Inline source with all class attribute values from the rendered DOM,
* capturing classes from all components (built-in + user), dynamic
* expressions, and the template itself — Tailwind's scanner handles
* the actual class extraction from these raw values
*/
function buildSourceDirectives(dom, config, fromDir) {
const directives = [];
const excludePaths = [resolve(config.output?.path ?? "dist"), ...(config.css?.exclude ?? []).map((p) => resolve(p))];
for (const p of excludePaths) directives.push(`@source not "${relative(fromDir, resolve(p))}";`);
/**
* Inline source: collect all class attribute values from the rendered DOM.
* After Vue SSR, the DOM contains every class from every component
* (built-in framework components, user components, dynamic
* bindings). We pass these raw values to Tailwind's
* scanner via @source inline().
*/
const classes = [];
walk(dom, (n) => {
const cls = n.attribs?.class;
if (cls) classes.push(cls);
});
if (classes.length) directives.push(`@source inline("${classes.join(" ")}");`);
return directives.join("\n");
}
/**
* Tailwind CSS transformer.
*
* Compiles CSS inside <style> tags in the DOM using
* @tailwindcss/postcss, then lowers modern CSS syntax with lightningcss.
*
* Configures Tailwind sources to scan:
* - Rendered class attributes (via `@source inline`) for all classes from all components
* - User project files (via Tailwind's auto-detection from base/from path)
*
* User `@source` and `@source not directives` in style tags are preserved.
* Source directives are only added to style tags that import Tailwind.
*
* Runs as the first transformer in the pipeline so that subsequent
* transformers (inliner, purge, etc.) work with fully compiled CSS.
*/
async function tailwindcss(dom, config, filePath) {
const styleTags = [];
walk(dom, (node) => {
if (node.name !== "style") return;
const el = node;
/**
* `raw` opts out of compilation entirely (marker is consumed here).
* `embed`/`data-embed` only signal "preserve tag after inlining"
* — they still need to go through compile so Tailwind/@apply
* resolves.
*/
if ("raw" in el.attribs) {
delete el.attribs.raw;
return;
}
const rawContent = el.children.filter((child) => child.type === "text").map((child) => child.data).join("");
if (!rawContent.trim()) return;
styleTags.push({
node: el,
cssContent: decodeStyleEntities(rawContent)
});
});
if (!styleTags.length) return dom;
const fromPath = filePath ?? resolve(process.cwd(), "template.vue");
const fromDir = dirname(fromPath);
const sourceDirectives = styleTags.some(({ cssContent }) => usesTailwind(cssContent)) ? buildSourceDirectives(dom, config, fromDir) : "";
for (let i = 0; i < styleTags.length; i++) {
const { node, cssContent } = styleTags[i];
/**
* Only add source directives to style tags that import Tailwind —
* plain CSS doesn't need them and @tailwindcss/postcss would
* leave the directives unconsumed in the output.
*/
const fullCss = usesTailwind(cssContent) ? `${cssContent}\n${sourceDirectives}` : cssContent;
try {
node.children = [{
type: "text",
data: await compileTailwindCss(fullCss, config, `${fromPath}?style=${i}`),
parent: node
}];
} catch {
/**
* If CSS processing fails, still replace with decoded content
* so HTML entities don't break the CSS.
*/
node.children = [{
type: "text",
data: cssContent,
parent: node
}];
}
}
return dom;
}
//#endregion
export { tailwindcss };
//# sourceMappingURL=tailwindcss.js.map