127 lines
3.1 KiB
TypeScript
127 lines
3.1 KiB
TypeScript
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<string[]> {
|
|
const candidates = new Set<string>();
|
|
const visit = async (dir: string): Promise<void> => {
|
|
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<string> {
|
|
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",
|
|
},
|
|
}),
|
|
);
|
|
},
|
|
};
|
|
}
|