177 lines
4.9 KiB
TypeScript
177 lines
4.9 KiB
TypeScript
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<string[]> {
|
|
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<string[]> {
|
|
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();
|
|
}
|