-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow static content generation in MDX files
- Loading branch information
Showing
4 changed files
with
450 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}, | ||
}, | ||
}); | ||
} | ||
}; |
Oops, something went wrong.