Skip to content

Commit

Permalink
Allow static content generation in MDX files
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-lee committed Nov 18, 2024
1 parent b234641 commit 7ffd749
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 91 deletions.
5 changes: 5 additions & 0 deletions packages/zudoku/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
"cmdk": "1.0.4",
"dotenv": "16.4.5",
"embla-carousel-react": "8.3.1",
"estree-util-value-to-estree": "3.2.1",
"express": "4.21.1",
"glob": "11.0.0",
"graphql": "16.9.0",
Expand Down Expand Up @@ -224,6 +225,7 @@
"unist-util-visit": "5.0.0",
"urql": "4.1.0",
"vaul": "1.1.0",
"vfile": "6.0.3",
"vite": "5.4.9",
"yaml": "2.6.0",
"yargs": "17.7.2",
Expand All @@ -232,15 +234,18 @@
"zustand": "5.0.0"
},
"devDependencies": {
"@types/estree": "1.0.6",
"@types/express": "5.0.0",
"@types/har-format": "1.2.15",
"@types/json-schema": "7.0.15",
"@types/mdast": "4.0.4",
"@types/mdx": "2.0.13",
"@types/node": "20.16.11",
"@types/object-hash": "3.0.6",
"@types/react-is": "18.3.0",
"@types/semver": "7.5.8",
"@types/yargs": "17.0.33",
"mdast-util-mdx": "3.0.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"rollup-plugin-visualizer": "5.12.0",
Expand Down
8 changes: 5 additions & 3 deletions packages/zudoku/src/vite/plugin-mdx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import rehypeMetaAsAttributes from "@lekoarts/rehype-meta-as-attributes";
import mdx, { type Options } from "@mdx-js/rollup";
import withToc from "@stefanprobst/rehype-extract-toc";
import withTocExport from "@stefanprobst/rehype-extract-toc/mdx";
import type { Root } from "mdast";
import path from "node:path";
import rehypeSlug from "rehype-slug";
import remarkComment from "remark-comment";
Expand All @@ -13,18 +14,18 @@ import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import { visit } from "unist-util-visit";
import { type Plugin } from "vite";
import { type ZudokuPluginOptions } from "../config/config.js";
import { remarkStaticGeneration } from "./remarkStaticGeneration.js";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rehypeCodeBlockPlugin = () => (tree: any) => {
visit(tree, "element", (node, index, parent) => {
if (node.tagName === "code") {
if (node.type === "element" && node.tagName === "code") {
node.properties.inline = parent?.tagName !== "pre";
}
});
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const remarkLinkRewritePlugin = () => (tree: any) => {
const remarkLinkRewritePlugin = () => (tree: Root) => {
visit(tree, "link", (node) => {
if (!node.url) return;

Expand Down Expand Up @@ -70,6 +71,7 @@ const viteMdxPlugin = (getConfig: () => ZudokuPluginOptions): Plugin => {
mdxExtensions: [".md", ".mdx"],
format: "mdx",
remarkPlugins: [
remarkStaticGeneration,
remarkComment,
remarkGfm,
remarkFrontmatter,
Expand Down
145 changes: 145 additions & 0 deletions packages/zudoku/src/vite/remarkStaticGeneration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { type ImportDeclaration, Program } from "estree";
import { valueToEstree } from "estree-util-value-to-estree";
import { Root } from "mdast";
import type { MdxjsEsm } from "mdast-util-mdx";
import vm, { type Context as VmContext } from "node:vm";
import { SKIP, visit } from "unist-util-visit";
import { VFile } from "vfile";

const FUNCTION_NAME = "_static";
const VARIABLE_NAME = "STATIC_CONTENT";

const isStaticExport = (program: Program) => {
for (const node of program.body) {
if (node.type === "ExportNamedDeclaration" && node.declaration) {
const decl = node.declaration;
if (decl.type === "VariableDeclaration") {
// Check variable declarations like: export const _static = () => { ... }
for (const declarator of decl.declarations) {
if (
declarator.id.type === "Identifier" &&
declarator.id.name === FUNCTION_NAME &&
declarator.init?.type === "ArrowFunctionExpression"
) {
return true;
}
}
} else if (
decl.type === "FunctionDeclaration" &&
decl.id.name === FUNCTION_NAME
) {
// Check function declarations like: export function _static() { ... }
return true;
}
}
}
return false;
};

const executeFunction = async (
file: VFile,
code: string,
importNodes: ImportDeclaration[],
) => {
const context: VmContext = {
result: "",
process,
console,
};

code = code.replace(/^\s*export/, "");

for (const { source, specifiers } of importNodes) {
const modulePath = source.value;
if (typeof modulePath !== "string") continue;

const module = await import(modulePath);

for (const specifier of specifiers) {
const localName = specifier.local.name;
switch (specifier.type) {
case "ImportSpecifier": {
if (specifier.imported.type !== "Identifier") continue;
const importedName = specifier.imported.name;
context[localName] = module[importedName];
break;
}
case "ImportDefaultSpecifier":
context[localName] = module.default ?? module;
break;
case "ImportNamespaceSpecifier":
context[localName] = module;
break;
}
}
}

const script = new vm.Script(`
(async () => {
result = await (async () => { ${code}; return ${FUNCTION_NAME}(); })();
})();
`);

const prevCwd = process.cwd();
// Run VM relative to the file's directory
process.chdir(file.dirname ?? file.cwd);
await script.runInNewContext(context);
process.chdir(prevCwd);

return context.result;
};

export const remarkStaticGeneration = () => async (tree: Root, file: VFile) => {
const collectedImports = new Set<string>();

const imports: ImportDeclaration[] = [];
const nodesToProcess: MdxjsEsm[] = [];

visit(tree, "mdxjsEsm", (node, index) => {
const innerTree = node.data?.estree;
if (!innerTree) return;

if (innerTree.body[0]?.type === "ImportDeclaration") {
imports.push(innerTree.body[0]);
collectedImports.add(node.value);
}

if (isStaticExport(innerTree)) {
nodesToProcess.push(node);
return SKIP;
}
});

for (const node of nodesToProcess) {
const executed = await executeFunction(file, node.value, imports);
if (!executed) continue;

const estreeValue = valueToEstree(executed, {
preserveReferences: true,
instanceAsObject: true,
});
tree.children.unshift({
type: "mdxjsEsm",
value: "",
data: {
estree: {
type: "Program",
body: [
{
type: "VariableDeclaration",
declarations: [
{
type: "VariableDeclarator",
id: { type: "Identifier", name: VARIABLE_NAME },
init: estreeValue,
},
],
kind: "const",
},
],
sourceType: "module",
},
},
});
}
};
Loading

0 comments on commit 7ffd749

Please sign in to comment.