// This implements the ability to use TS, TSX, and more plugins // in Node.js. It cannot be built on the ES module loader, // because there is no exposed way to replace modules when // needed (see nodejs#49442). // // It also allows using a simple compile cache, which is used by // the site generator to determine when code changes. export const projectRoot = path.resolve(import.meta.dirname, "../"); export const projectSrc = path.resolve(projectRoot, "src"); // Create a project-relative require. For convenience, it is generic-typed. export const load = createRequire( pathToFileURL(path.join(projectRoot, "run.js")).toString(), ) as { (id: string): T; extensions: NodeJS.Dict<(mod: NodeJS.Module, file: string) => unknown>; cache: NodeJS.Dict; resolve: (id: string, o?: { paths: string[] }) => string; }; export const { cache } = load; // Register extensions by overwriting `require.extensions` const require = load; const exts = require.extensions; exts[".ts"] = loadEsbuild; exts[".tsx"] = loadEsbuild; exts[".jsx"] = loadEsbuild; exts[".marko"] = loadMarko; exts[".mdx"] = loadMdx; exts[".css"] = loadCss; // Intercept all module load calls to track CSS imports + file times. export interface FileStat { cssImportsRecursive: string[] | null; lastModified: number; imports: string[]; /* Used by 'incremental.ts' */ srcIds: string[]; } let fsGraph = new Map(); export function setFsGraph(g: Map) { if (fsGraph.size > 0) { throw new Error("Cannot restore fsGraph when it has been written into"); } fsGraph = g; } export function getFsGraph() { return fsGraph; } function shouldTrackPath(filename: string) { return !filename.includes("node_modules") && !filename.includes(import.meta.dirname); } const Module = load("node:module"); const ModulePrototypeUnderscoreCompile = Module.prototype._compile; Module.prototype._compile = function ( content: string, filename: string, format: "module" | "commonjs", ) { fs.writeMkdirSync( ".clover/debug-transpilation/" + path.relative(projectRoot, filename).replaceAll("\\", "/").replaceAll( "../", "_/", ).replaceAll("/", "."), content, ); const result = ModulePrototypeUnderscoreCompile.call( this, content, filename, format, ); const stat = fs.statSync(filename); if (shouldTrackPath(filename)) { const cssImportsMaybe: string[] = []; const imports: string[] = []; for (const { filename: file } of this.children) { const relative = path.relative(projectRoot, file); if (file.endsWith(".css")) cssImportsMaybe.push(relative); else { const child = fsGraph.get(relative); if (!child) continue; const { cssImportsRecursive } = child; if (cssImportsRecursive) cssImportsMaybe.push(...cssImportsRecursive); imports.push(relative); } } const relative = path.relative(projectRoot, filename); fsGraph.set(relative, { cssImportsRecursive: cssImportsMaybe.length > 0 ? Array.from(new Set(cssImportsMaybe)) : null, imports, lastModified: stat.mtimeMs, srcIds: [], }); } return result; }; // Implement @/ prefix const ModuleUnderscoreResolveFilename = Module._resolveFilename; Module._resolveFilename = (...args) => { if (args[0].startsWith("@/")) { const replacedPath = "." + args[0].slice(1); try { return require.resolve(replacedPath, { paths: [projectSrc] }); } catch (err: any) { if (err.code === "MODULE_NOT_FOUND" && err.requireStack.length <= 1) { err.message.replace(replacedPath, args[0]); } } } return ModuleUnderscoreResolveFilename(...args); }; function loadEsbuild(module: NodeJS.Module, filepath: string) { let src = fs.readFileSync(filepath, "utf8"); return loadEsbuildCode(module, filepath, src); } function loadEsbuildCode(module: NodeJS.Module, filepath: string, src: string) { if (filepath === import.meta.filename) { module.exports = self; return; } let loader: any = "tsx"; if (filepath.endsWith(".ts")) loader = "ts"; else if (filepath.endsWith(".jsx")) loader = "jsx"; else if (filepath.endsWith(".js")) loader = "js"; if (src.includes("import.meta")) { src = ` import.meta.url = ${JSON.stringify(pathToFileURL(filepath).toString())}; import.meta.dirname = ${JSON.stringify(path.dirname(filepath))}; import.meta.filename = ${JSON.stringify(filepath)}; ` + src; } src = esbuild.transformSync(src, { loader, format: "cjs", target: "esnext", jsx: "automatic", jsxImportSource: "#ssr", }).code; return module._compile(src, filepath, "commonjs"); } function loadMarko(module: NodeJS.Module, filepath: string) { let src = fs.readFileSync(filepath, "utf8"); // A non-standard thing here is Clover Sitegen implements // its own client side scripting stuff, so it overrides // bare client import statements to it's own usage. if (src.match(/^\s*client\s+import\s+["']/m)) { src = src.replace( /^\s*client\s+import\s+("[^"]+|'[^']+)[^\n]+/m, "", ) + '\nimport { Script as CloverScriptInclude } from "#sitegen";'; } src = marko.compileSync(filepath, {}).code; src = src.replace("marko/debug/html", "#ssr/marko"); return loadEsbuildCode(module, filepath, src); } function loadMdx(module: NodeJS.Module, filepath: string) { const input = fs.readFileSync(filepath); const out = mdx.compileSync(input, { jsxImportSource: "#ssr" }).value; const src = typeof out === "string" ? out : Buffer.from(out).toString("utf8"); return loadEsbuildCode(module, filepath, src); } function loadCss(module: NodeJS.Module, filepath: string) { module.exports = {}; } export function reloadRecursive(filepath: string) { filepath = path.resolve(filepath); const existing = cache[filepath]; if (existing) deleteRecursive(filepath, existing); fsGraph.clear(); return require(filepath); } function deleteRecursive(id: string, module: any) { if (id.includes(path.sep + "node_modules" + path.sep)) { return; } delete cache[id]; for (const child of module.children) { if (child.filename.includes("/engine/")) return; const existing = cache[child.filename]; if (existing === child) deleteRecursive(child.filename, existing); } } export function getCssImports(filepath: string) { filepath = path.resolve(filepath); if (!require.cache[filepath]) throw new Error(filepath + " was never loaded"); return fsGraph.get(path.relative(projectRoot, filepath)) ?.cssImportsRecursive ?? []; } export function resolveFrom(src: string, dest: string) { try { return createRequire(src).resolve(dest); } catch (err: any) { if (err.code === "MODULE_NOT_FOUND" && err.requireStack.length <= 1) { err.message = err.message.split("\n")[0] + " from '" + src + "'"; } throw err; } } declare global { namespace NodeJS { interface Module { _compile( this: NodeJS.Module, content: string, filepath: string, format: "module" | "commonjs", ): unknown; } } } import * as fs from "./fs.ts"; import * as path from "node:path"; import { pathToFileURL } from "node:url"; import * as esbuild from "esbuild"; import * as marko from "@marko/compiler"; import { createRequire } from "node:module"; import * as mdx from "@mdx-js/mdx"; import * as self from "./hot.ts"; import { Buffer } from "node:buffer";