import { readdir, readFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import { join, resolve } from "node:path"; import type { ContentSchema, Integration, IntegrationContext, } from "../contracts/index.ts"; import { compileCss, type CompileOptions } from "./css/index.ts"; import { compileStcssModule } from "./css/modules.ts"; import { markdownLoader } from "./markdown.ts"; import { sassLoader, type SassLoaderOptions } from "./css/sass.ts"; export { shadcnIntegration, type ShadcnIntegrationOptions, } from "../integrations/shadcn.ts"; export interface MarkdownIntegrationOptions { /** Name of the collection, defaults to `notes`. */ name?: string; /** Directory of the collection, relative to the project root. */ dir?: string; /** Schema of the frontmatter. */ schema?: ContentSchema; } /** * Markdown integration. * * The integration registers the markdown loader and adds the collection of * the content directory to the project. */ export function markdownIntegration( options: MarkdownIntegrationOptions = {}, ): Integration { const name = options.name ?? "notes"; const dir = options.dir ?? "content/notes"; return { name: "markdown", setup(context: IntegrationContext) { context.loader("markdown", markdownLoader()); const collections = context.config.collections ?? []; if (collections.some((collection) => collection.name === name)) return; collections.push({ name, dir, loader: "markdown", schema: options.schema, }); context.config.collections = collections; }, }; } export interface StcssIntegrationOptions extends CompileOptions { /** Directory of the stylesheets, relative to the project root. */ dir?: string; /** Route the compiled css is served at. */ route?: string; } /** * Css integration. * * The integration compiles the structured stylesheets of the project and * serves them at a single route, the browser caches the result. */ export function stcssIntegration( options: StcssIntegrationOptions = {}, ): Integration { const route = options.route ?? "/_muddle/styles.css"; return { name: "stcss", async setup(context: IntegrationContext) { context.loader("stcss", { name: "stcss", extensions: [".stcss"], load(file) { return { data: {}, content: compileCss(file.source, options) }; }, }); const dir = resolve(context.config.root, options.dir ?? "styles"); if (!existsSync(dir)) return; const files = await stcssFiles(dir); const parts: string[] = []; for (const file of files.sort()) { const path = join(dir, file); const source = await readFile(path, "utf8"); parts.push( file.endsWith(".module.stcss") ? ( await compileStcssModule(source, { ...options, filename: path, root: context.config.root, }) ).css : compileCss(source, options), ); } const css = parts.join("\n"); context.route( "GET", route, () => new Response(css, { headers: { "content-type": "text/css; charset=utf-8", "cache-control": "public, max-age=3600", }, }), ); }, }; } async function stcssFiles(dir: string, prefix = ""): Promise { const files: string[] = []; for (const entry of await readdir(dir, { withFileTypes: true })) { const relative = `${prefix}${entry.name}`; if (entry.isDirectory()) files.push(...(await stcssFiles(join(dir, entry.name), `${relative}/`))); else if (entry.name.endsWith(".stcss")) files.push(relative); } return files.sort(); } /** Registers Sass files as loaders and serves their compiled CSS alongside STCSS. */ export function sassIntegration(options: SassLoaderOptions = {}): Integration { const route = "/_muddle/sass.css"; return { name: "sass", async setup(context) { const loader = sassLoader(options); context.loader("sass", loader); const dir = resolve(context.config.root, options.dir ?? "styles"); if (!existsSync(dir)) return; const files = await sassFiles(dir); context.route("GET", route, async () => { const css = await Promise.all( files .filter((file) => !file.split("/").at(-1)?.startsWith("_")) .map( async (file) => ( await loader.load({ path: join(dir, file), relativePath: `styles/${file}`, source: await readFile(join(dir, file), "utf8"), }) ).content ?? "", ), ); return new Response(css.join("\n"), { headers: { "content-type": "text/css; charset=utf-8" }, }); }); }, }; } async function sassFiles(dir: string, prefix = ""): Promise { const files: string[] = []; for (const entry of await readdir(dir, { withFileTypes: true })) { const relative = `${prefix}${entry.name}`; if (entry.isDirectory()) files.push(...(await sassFiles(join(dir, entry.name), `${relative}/`))); else if (/\.s[ac]ss$/i.test(entry.name)) files.push(relative); } return files.sort(); }