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", }, }), ); }, }; }