1export interface MarkoCacheEntry {
2 src: string;
3 scannedClientRefs: string[];
4}
5
6export const markoCache = new Map<string, MarkoCacheEntry>();
7
8export function loadMarko(module: NodeJS.Module, filepath: string) {
9 return loadMarkoExtra(module, filepath, "html");
10}
11export function loadMarkodown(module: NodeJS.Module, filepath: string) {
12 return loadMarkoExtra(module, filepath, "markdown");
13}
14
15function loadMarkoExtra(
16 module: NodeJS.Module,
17 filepath: string,
18 mode: "html" | "markdown",
19) {
20 let cache = markoCache.get(filepath);
21 if (!cache) markoCache.set(filepath, cache = buildMarkoCacheEntry(filepath, mode));
22
23 const { src, scannedClientRefs } = cache;
24 return hot.loadEsbuildCode(module, filepath, src, {
25 scannedClientRefs,
26 });
27}
28
29export function buildMarkoCacheEntry(
30 filepath: string,
31 mode: "html" | "markdown" = "html",
32): MarkoCacheEntry {
33 let src = fs.readFileSync(filepath, "utf8");
34 if (mode === "markdown") {
35 let transform;
36 try {
37 transform = markodown.transform({
38 source: src,
39 selfImport: `./${path.basename(filepath)}`,
40 });
41 } catch (err) {
42 throw new Error(`Failed ${filepath}: Markodown Compiler Panic`, {
43 cause: err,
44 });
45 }
46 if (!transform.success) {
47 // TODO: better error handling
48 throw new Error(
49 "Failed " + filepath + ": " + JSON.stringify(transform.errors),
50 );
51 }
52 src = transform.text;
53 fs.writeMkdirSync(getMarkoCachePath(filepath, ".marko"), src);
54 }
55
56 const resolved = resolveClientImports(src, filepath);
57 let compiled = marko.compileSync(resolved.src, filepath, {
58 sourceMaps: true,
59 });
60 // workaround an esbuild bug?
61 (compiled.map as unknown as { sourceRoot: string }).sourceRoot = "/";
62 compiled.map.sources[0] = filepath;
63 const compiledSrc = compiled.code
64 + "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,"
65 + Buffer.from(JSON.stringify(compiled.map), "utf-8").toString("base64");
66 fs.writeMkdirSync(getMarkoCachePath(filepath, ".js"), compiledSrc);
67 return {
68 src: compiledSrc,
69 scannedClientRefs: resolved.scannedClientRefs,
70 };
71}
72
73export function getMarkoCachePath(
74 filepath: string,
75 ext: ".js" | ".marko",
76) {
77 return ".clover/marko/" + path.relative(".", filepath).replace(/^src\//, "")
78 + ext;
79}
80
81export function scanMarkoClientImports(src: string, filepath: string) {
82 // Clover Sitegen uses `client import` as a convenience shorthand
83 // and rewrites it to the runtime script include component.
84 const scannedClientRefs = new Set<string>();
85 let found = false;
86 src = src.replace(
87 /^\s*client\s+import\s+("[^"]+"|'[^']+')[^\n]*$/gm,
88 (_, rawRef) => {
89 found = true;
90 const ref = JSON.parse(`"${rawRef.slice(1, -1)}"`);
91 const resolved = hot.resolveClientRef(filepath, ref);
92 scannedClientRefs.add(resolved);
93 return `<CloverScriptInclude=${JSON.stringify(hot.getScriptId(resolved))} />`;
94 },
95 );
96 if (found) {
97 src += "\nimport { CloverScriptInclude } from \"#sitegen\";\n";
98 }
99 return {
100 src,
101 scannedClientRefs: Array.from(scannedClientRefs),
102 };
103}
104
105function resolveClientImports(src: string, filepath: string) {
106 return scanMarkoClientImports(src, filepath);
107}
108
109import * as fs from "#sitegen/fs";
110import * as markodown from "@clo/markodown";
111import * as marko from "@marko/compiler";
112import * as path from "node:path";
113import * as hot from "./hot.ts";