From 9843cab166f94b192d783d038460ba4a9bc505f0 Mon Sep 17 00:00:00 2001 From: Hellings Date: Fri, 23 Oct 2026 08:53:07 -0400 Subject: [PATCH] Initial commit --- index.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 12 ++++++++++ 2 files changed, 74 insertions(+) create mode 100644 index.ts create mode 100644 package.json diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..0b5eaeb --- /dev/null +++ b/index.ts @@ -0,0 +1,62 @@ +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" } })); + }, + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b3bb253 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "@muddle/tailwind", + "type": "module", + "exports": "./index.ts", + "version": "0.1.0", + "peerDependencies": { + "muddle": "*" + }, + "dependencies": { + "tailwindcss": "^4.3.3" + } +}