diff --git a/framework/generate.ts b/framework/generate.ts index 57c7d957c76f3de166603aea6f6ed136ec5a3aae..0eb6d5ccdac23e9751123eba0c10a4e24d4a0cd0 100644 --- a/framework/generate.ts +++ b/framework/generate.ts @@ -40,7 +40,7 @@ export async function generate(p: progress.Ref) { const builtFonts = fonts.buildFonts(p, siteConfig.fonts); - // TODO: loadMarkoCache + await loadMarkoCache(p); const pageNode = p.start("generate static pages", {}); const viewNode = p.start("compile dynamic views", {}); @@ -245,6 +245,57 @@ export async function scanSiteSection(io: Io, section: sg.Section) { return { staticFiles, pages, views, scripts }; } +export async function loadMarkoCache(p: progress.Ref) { + using node = p.start("load marko cache"); + + markoCache.clear(); + const files = fs.readDirRecOptionalSync(".clover/marko") + .filter((file) => file.endsWith(".js")); + let restored = 0; + let rebuilt = 0; + await Promise.all( + files.map(async (cachedPath) => { + const source = resolveMarkoCacheSource(cachedPath); + if (!source) return; + + const cachePath = path.join(".clover/marko", cachedPath); + const [sourceStat, cacheStat] = await Promise.all([ + fs.stat(source).catch(() => null), + fs.stat(cachePath).catch(() => null), + ]); + if (!sourceStat) return; + + const absSource = path.resolve(source); + if (!cacheStat || cacheStat.mtimeMs < sourceStat.mtimeMs) { + rebuilt += 1; + markoCache.set( + absSource, + buildMarkoCacheEntry(absSource, source.endsWith(".mdo") ? "markdown" : "html"), + ); + return; + } + + restored += 1; + const sourceText = await fs.readFile(source, "utf-8"); + markoCache.set(absSource, { + src: await fs.readFile(cachePath, "utf-8"), + scannedClientRefs: scanMarkoClientImports(sourceText, absSource) + .scannedClientRefs, + }); + }), + ); + + node.text = `load marko cache (${restored} restored, ${rebuilt} rebuilt)`; +} + +function resolveMarkoCacheSource(cachedPath: string) { + const source = cachedPath.slice(0, -".js".length); + if (fs.existsSync(source)) return source; + const srcSource = path.join("src", source); + if (fs.existsSync(srcSource)) return srcSource; + return null; +} + export async function preparePage( io: Io, { item, metaTemplate }: { item: sg.FileItem; metaTemplate: meta.Template }, @@ -428,3 +479,4 @@ import * as fonts from "./font.ts"; import * as hot from "./hot.ts"; import * as incr from "./incremental.ts"; import { Io } from "./incremental.ts"; +import { buildMarkoCacheEntry, markoCache, scanMarkoClientImports } from "./marko.ts"; diff --git a/framework/hot.ts b/framework/hot.ts index 82311f8239c40ff6cc7a4c823bbdec8818d864c3..2ad66b476968f19c412a6bd4c7086701b65f222a 100644 --- a/framework/hot.ts +++ b/framework/hot.ts @@ -223,7 +223,8 @@ export function resolveFrom(src: string, dest: string) { return createRequire(src).resolve(dest); } catch (err: any) { if ( - err.code === "MODULE_NOT_FOUND" && err.requireStack + err.code === "MODULE_NOT_FOUND" + && err.requireStack && err.requireStack.length <= 1 ) { err.message = err.message.split("\n")[0] + " from '" + src + "'"; diff --git a/framework/marko.ts b/framework/marko.ts index e1719d850e8a571139bbc3af324a96ec911fdceb..466b4f9b86c7aef5b5a2efb3b62f634c03d2ebe8 100644 --- a/framework/marko.ts +++ b/framework/marko.ts @@ -18,72 +18,7 @@ function loadMarkoExtra( mode: "html" | "markdown", ) { let cache = markoCache.get(filepath); - if (!cache) { - let src = fs.readFileSync(filepath, "utf8"); - if (mode === "markdown") { - let transform; - try { - transform = markodown.transform({ - source: src, - selfImport: `./${path.basename(filepath)}`, - }); - } catch (err) { - throw new Error(`Failed ${filepath}: Markodown Compiler Panic`, { - cause: err, - }); - } - if (!transform.success) { - // TODO: better error handling - throw new Error( - "Failed " + filepath + ": " + JSON.stringify(transform.errors), - ); - } - src = transform.text; - - fs.writeMkdirSync( - ".clover/marko/" + path.relative(".", filepath).replace(/^src\//, "") - + ".marko", - src, - ); - } - - // 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. - const scannedClientRefs = new Set(); - if (src.match(/^\s*client\s+import\s+["']/m)) { - src = src.replace( - /^\s*client\s+import\s+("[^"]+"|'[^']+')[^\n]+/m, - (_, src) => { - const ref = JSON.parse(`"${src.slice(1, -1)}"`); - const resolved = hot.resolveClientRef(filepath, ref); - scannedClientRefs.add(resolved); - return ``; - }, - ) + "\nimport { CloverScriptInclude } from \"#sitegen\";\n"; - } - - let compiled = marko.compileSync(src, filepath, { - sourceMaps: true, - }); - // workaround an esbuild bug? - (compiled.map as unknown as { sourceRoot: string }).sourceRoot = "/"; - compiled.map.sources[0] = filepath; - src = compiled.code - + "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," - + Buffer.from(JSON.stringify(compiled.map), "utf-8").toString("base64"); - cache = { src, scannedClientRefs: Array.from(scannedClientRefs) }; - markoCache.set(filepath, cache); - fs.writeMkdirSync( - ".clover/marko/" + path.relative(".", filepath).replace(/^src\//, "") - + ".js", - src, - ); - } + if (!cache) markoCache.set(filepath, cache = buildMarkoCacheEntry(filepath, mode)); const { src, scannedClientRefs } = cache; return hot.loadEsbuildCode(module, filepath, src, { @@ -91,6 +26,86 @@ function loadMarkoExtra( }); } +export function buildMarkoCacheEntry( + filepath: string, + mode: "html" | "markdown" = "html", +): MarkoCacheEntry { + let src = fs.readFileSync(filepath, "utf8"); + if (mode === "markdown") { + let transform; + try { + transform = markodown.transform({ + source: src, + selfImport: `./${path.basename(filepath)}`, + }); + } catch (err) { + throw new Error(`Failed ${filepath}: Markodown Compiler Panic`, { + cause: err, + }); + } + if (!transform.success) { + // TODO: better error handling + throw new Error( + "Failed " + filepath + ": " + JSON.stringify(transform.errors), + ); + } + src = transform.text; + fs.writeMkdirSync(getMarkoCachePath(filepath, ".marko"), src); + } + + const resolved = resolveClientImports(src, filepath); + let compiled = marko.compileSync(resolved.src, filepath, { + sourceMaps: true, + }); + // workaround an esbuild bug? + (compiled.map as unknown as { sourceRoot: string }).sourceRoot = "/"; + compiled.map.sources[0] = filepath; + const compiledSrc = compiled.code + + "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," + + Buffer.from(JSON.stringify(compiled.map), "utf-8").toString("base64"); + fs.writeMkdirSync(getMarkoCachePath(filepath, ".js"), compiledSrc); + return { + src: compiledSrc, + scannedClientRefs: resolved.scannedClientRefs, + }; +} + +export function getMarkoCachePath( + filepath: string, + ext: ".js" | ".marko", +) { + return ".clover/marko/" + path.relative(".", filepath).replace(/^src\//, "") + + ext; +} + +export function scanMarkoClientImports(src: string, filepath: string) { + // Clover Sitegen uses `client import` as a convenience shorthand + // and rewrites it to the runtime script include component. + const scannedClientRefs = new Set(); + let found = false; + src = src.replace( + /^\s*client\s+import\s+("[^"]+"|'[^']+')[^\n]*$/gm, + (_, rawRef) => { + found = true; + const ref = JSON.parse(`"${rawRef.slice(1, -1)}"`); + const resolved = hot.resolveClientRef(filepath, ref); + scannedClientRefs.add(resolved); + return ``; + }, + ); + if (found) { + src += "\nimport { CloverScriptInclude } from \"#sitegen\";\n"; + } + return { + src, + scannedClientRefs: Array.from(scannedClientRefs), + }; +} + +function resolveClientImports(src: string, filepath: string) { + return scanMarkoClientImports(src, filepath); +} + import * as fs from "#sitegen/fs"; import * as markodown from "@clo/markodown"; import * as marko from "@marko/compiler"; diff --git a/framework/watch.ts b/framework/watch.ts index 0d4503d66a5abc7c30867bca3a23460afabc3f9a..c639d69a5655222e3b6575d3e23ede4c810b6a57 100644 --- a/framework/watch.ts +++ b/framework/watch.ts @@ -45,7 +45,10 @@ async function rebuild(files: string[]) { if (isRunning) return; isRunning = true; try { - child_process.execSync("./node_modules/.bin/dprint fmt " + files.join(" "), { stdio: "inherit" }); + const formattingFiles = files.filter(x => !x.endsWith(".mdo")); + if (formattingFiles.length > 0) { + child_process.execSync("./node_modules/.bin/dprint fmt " + formattingFiles.join(" "), { stdio: "inherit" }); + } } catch (err) { isRunning = false; throw err;