import { readdir, readFile } from "node:fs/promises"; import { dirname, extname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { compile } from "tailwindcss"; import type { Integration, IntegrationContext } from "muddle/contracts"; export interface TailwindOptions { route?: string; css?: string; content?: string[]; } const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".html", ".spug", ".mdx"]); export async function tailwindCandidates(root: string, paths?: string[]): Promise { const candidates = new Set(); const visit = async (dir: string): Promise => { for (const entry of await readdir(dir, { withFileTypes: true })) { if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) continue; const path = join(dir, entry.name); if (entry.isDirectory()) { await visit(path); continue; } if (!SCANNED_EXTENSIONS.has(extname(entry.name))) continue; if (paths && !paths.some((value) => path.includes(value))) continue; const source = await readFile(path, "utf8"); for (const match of source.matchAll(/(?:class|className)\s*=\s*["'`]([^"'`]+)["'`]/g)) { for (const value of (match[1] ?? "").split(/\s+/)) if (value) candidates.add(value); } } }; await visit(root); return [...candidates]; } export async function compileTailwind(css: string, candidates: string[] = []): Promise { const tailwindCss = join(dirname(fileURLToPath(import.meta.resolve("tailwindcss/package.json"))), "index.css"); const compiler = await compile(css, { base: process.cwd(), loadStylesheet: async (id) => { if (!["tailwindcss", "tailwindcss/theme.css", "tailwindcss/utilities.css"].includes(id)) throw new Error(`Unsupported Tailwind stylesheet "${id}"`); const path = id === "tailwindcss" ? tailwindCss : join(dirname(tailwindCss), id.split("/")[1]!); return { path, base: process.cwd(), content: await readFile(path, "utf8") }; }, }); return compiler.build(candidates); } export function tailwindIntegration(options: TailwindOptions = {}): Integration { const route = options.route ?? "/_muddle/tailwind.css"; const source = options.css ?? '@import "tailwindcss";'; return { name: "tailwind", async setup(context: IntegrationContext) { const build = () => tailwindCandidates(context.config.root, options.content).then((candidates) => compileTailwind(source, candidates)); context.loader("tailwind", { name: "tailwind", extensions: [".tailwind.css"], async load(file) { return { data: {}, content: await compileTailwind(file.source, await tailwindCandidates(context.config.root, options.content)) }; }, }); context.route("GET", route, async () => new Response(await build(), { headers: { "content-type": "text/css; charset=utf-8", "cache-control": "public, max-age=60" } })); }, }; }