radish
Version:
Radish is a React-based static site generator that outputs plain HTML and CSS.
194 lines (193 loc) • 7.91 kB
JavaScript
import * as fs from "node:fs";
import * as path from "node:path";
import { globby } from "globby";
import grayMatter from "gray-matter";
import { compile as compileMdx } from "@mdx-js/mdx";
import remarkGfm from "remark-gfm";
import rehypeHighlight from "rehype-highlight";
import toml from "toml";
import yaml from "js-yaml";
import fetch from "../util/fetch.js";
const content = new Map();
export const contentMap = () => {
const map = {};
for (const [file, matter] of content) {
const dirs = file.split(path.sep);
let current = map;
for (let i = 0; i < dirs.length; i++) {
const dir = dirs[i];
if (dir === undefined)
break;
if (i === dirs.length - 1)
current[dir] = matter;
else
current[dir] ??= {};
current = current[dir];
}
}
return map;
};
export const contentPlugin = (options) => ({
name: "content",
setup(build) {
build.onResolve({ filter: /CONTENT_INDEX$/ }, args => {
return { path: args.path, namespace: "content" };
});
build.onLoad({ filter: /.*/, namespace: "content" }, async () => {
const paths = await globby([
path.join(options.src, "**/!(_)*.{md,mdx,json,toml,yaml,yml,js,ts}")
]);
const files = paths.map(filepath => path.relative(options.src, filepath));
const src = ["export const content = {};\n"];
const levels = new Set();
let i = 1;
for (const file of files) {
const filepath = path.parse(file);
const dirs = filepath.dir
.split(path.sep)
.filter(Boolean)
.map(dir => `["${dir}"]`);
for (let j = 1; j <= dirs.length; j++) {
const nested = dirs.slice(0, j).join("");
if (levels.has(nested))
continue;
levels.add(nested);
src.push(`content${nested} = {};`);
}
const joined = dirs.join("") + `["${filepath.name}"]`;
src.push(`import * as content${i} from "./${file}";`);
if (/\.mdx?$/.test(filepath.base)) {
src.push(`content${joined} = { ...content${i}, Component: content${i}.default };`, `delete content${joined}.default;\n`);
}
else
src.push(`content${joined} = content${i};`);
i += 1;
}
return {
contents: src.join("\n"),
loader: "js",
resolveDir: options.src
};
});
const src = options.src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
build.onLoad({ filter: new RegExp(`${src}/.*\\.toml$`) }, async (args) => {
const file = await fs.promises.readFile(args.path, "utf-8");
const json = toml.parse(file);
const filepath = path.parse(args.path);
const key = path.relative(options.src, path.join(filepath.dir, filepath.name));
content.set(key, json);
return {
contents: compileDataFile(json),
loader: "js",
resolveDir: path.dirname(args.path)
};
});
build.onLoad({ filter: new RegExp(`${src}/.*\\.ya?ml$`) }, async (args) => {
const file = await fs.promises.readFile(args.path, "utf-8");
const json = yaml.load(file);
const filepath = path.parse(args.path);
const key = path.relative(options.src, path.join(filepath.dir, filepath.name));
content.set(key, json);
return {
contents: compileDataFile(json),
loader: "js",
resolveDir: path.dirname(args.path)
};
});
build.onLoad({ filter: new RegExp(`${src}/.*\\.json$`) }, async (args) => {
const file = await fs.promises.readFile(args.path, "utf-8");
const json = JSON.parse(file);
const filepath = path.parse(args.path);
const key = path.relative(options.src, path.join(filepath.dir, filepath.name));
content.set(key, json);
return {
contents: compileDataFile(json),
loader: "js",
resolveDir: path.dirname(args.path)
};
});
build.onLoad({ filter: new RegExp(`${src}/.*\\.mdx?$`) }, async (args) => {
const md = await fs.promises.readFile(args.path, "utf-8");
const filepath = path.parse(args.path);
const key = path.relative(options.src, path.join(filepath.dir, filepath.name));
let language;
const delimiters = md.substring(0, md.indexOf("\n")).trim();
if (delimiters === "+++")
language = "toml";
const matter = grayMatter(md, {
language,
delimiters,
engines: { toml: toml.parse.bind(toml) }
});
content.set(key, matter.data);
const file = await compileMdx(matter.content, {
jsx: true,
rehypePlugins: [rehypeHighlight],
remarkPlugins: [remarkGfm]
});
const lines = file.value.toString().split("\n");
const from = path.join(filepath.dir, filepath.name + ".mdjson");
const imports = Object.keys(matter.data).join(", ");
lines.splice(1, 0, `import { ${imports} } from "${from}"`, `export { ${imports} } from "${from}"`);
return { contents: lines.join("\n"), loader: "jsx" };
});
build.onResolve({ filter: /\.mdjson$/ }, args => ({
path: args.path,
namespace: "mdjson"
}));
build.onLoad({ filter: /.*/, namespace: "mdjson" }, async (args) => {
const filepath = path.parse(args.path);
const key = path.relative(options.src, path.join(filepath.dir, filepath.name));
const matter = content.get(key);
if (!matter)
return;
return {
contents: compileDataFile(matter),
loader: "js",
resolveDir: path.dirname(args.path)
};
});
build.onResolve({ filter: /^https?:\/\// }, args => ({
path: args.path,
namespace: "remote"
}));
const http = new Map();
build.onLoad({ filter: /.*/, namespace: "remote" }, async (args) => {
let contents = http.get(args.path);
if (!contents) {
contents = await fetch(args.path);
http.set(args.path, contents);
}
return { contents, loader: "json" };
});
}
});
function compileDataFile(obj) {
const src = [];
for (const [key, value] of Object.entries(obj)) {
src.push(`export let ${key} = ${JSON.stringify(value)};`);
}
const index = indexObject(obj);
let i = 1;
for (const [path, value] of index) {
if (typeof value !== "string")
continue;
const [, url] = value.match(/^url\(["'](.*)["']\)/) ?? [];
if (!url)
continue;
const first = path.shift();
if (!first)
continue;
const bracketed = path.map(key => `["${key}"]`).join("");
src.push("", `import file${i} from "${url}";`, `${first}${bracketed} = file${i};`);
i += 1;
}
return src.join("\n");
}
function indexObject(obj) {
if (Object(obj) !== obj)
return [[[], obj]];
return Object.entries(obj).flatMap(([key, nested]) => {
return indexObject(nested).map(([path, val]) => [[Array.isArray(obj) ? Number(key) : key, ...path], val]);
});
}