HTML Builder (generate-static-page.js)
Examine how Dinou renders individual routes to static HTML files and generates metadata JSON files to control cache revalidation.
Key File Location: ./dinou/core/generate-static-page.js💡 Overview
In Incremental Static Regeneration (ISR) and Dynamic Pre-rendering (ISG), we need to refresh the HTML file on demand. The generate-static-page.js module compiles a single route to HTML using renderAppToHtml(), extracts side-effects, writes index.html, and writes metadata.json.
📊 HTML Generation Flow
The flowchart below shows how routes are compiled to HTML in the single-page builder:
⚡ Stale Safety & Double-Buffer Builds
To avoid serving corrupt or partially written files to visitors:
- Double-Buffering: Renders and writes the page to a temporary
.tmpfile path. - Atomic Swaps: Calling engines use
safeRename()to overwrite the activeindex.htmlinstantaneously once writing completes.
⚙️ Complete Code Walkthrough
Below is the full code of generate-static-page.js:
const path = require("path");
const { mkdirSync, createWriteStream, existsSync } = require("fs");
const fs = require("fs").promises;
const renderAppToHtml = require("./render-app-to-html.js");
const { getStaticMetadata } = require("./build-static-pages.js");
const { processMetadata } = require("./get-ssg-metadata.js");
const OUT_DIR = path.resolve("dist2");
async function generateStaticPage(reqPath) {
const finalReqPath = reqPath.endsWith("/") ? reqPath : reqPath + "/";
const htmlPath = path.join(OUT_DIR, finalReqPath, "index.html");
// 1. Double-buffer file generation
const tempHtmlPath = path.join(OUT_DIR, finalReqPath, `index.html.${Date.now()}-\0.004134198777008713.tmp`);
const query = {};
const paramsString = JSON.stringify(query);
const capturedStatus = {};
const contextForChild = {
req: {
query,
cookies: {},
headers: {
"user-agent": "Dinou-ISR-Revalidator",
host: "localhost",
"x-forwarded-proto": "http",
},
path: finalReqPath,
method: "GET",
},
};
try {
mkdirSync(path.dirname(htmlPath), { recursive: true });
const fileStream = createWriteStream(tempHtmlPath);
let htmlStream = null;
const mockRes = {
headersSent: true,
_cookies: [],
cookie(name, value, options) {
this._cookies.push({ name, value, options });
},
write: (chunk) => {
if (!fileStream.writableEnded) fileStream.write(chunk);
},
end: (chunk) => {
if (chunk && !fileStream.writableEnded) fileStream.write(chunk);
if (htmlStream) htmlStream.unpipe(fileStream);
if (!fileStream.writableEnded) fileStream.end();
},
status: (code) => {
capturedStatus.value = code;
},
setHeader: () => {},
clearCookie: () => {},
redirect: () => {},
};
// 2. Render App to readable HTML Stream
htmlStream = renderAppToHtml(
finalReqPath,
paramsString,
contextForChild,
mockRes,
capturedStatus
);
// 3. Process dynamic compile side-effects (cookies, redirects)
const metadata = getStaticMetadata(finalReqPath);
let sideEffectScripts = "";
if (metadata && metadata.effects) {
sideEffectScripts = processMetadata(metadata.effects);
}
// 4. Pipe to temporary build path
await new Promise((resolve, reject) => {
if (sideEffectScripts) fileStream.write(sideEffectScripts);
htmlStream.pipe(fileStream, { end: false });
htmlStream.on("end", () => {
if (!fileStream.writableEnded) fileStream.end();
resolve();
});
htmlStream.on("error", (err) => {
fileStream.end();
if (err.code === "ERR_STREAM_WRITE_AFTER_END") resolve();
else reject(err);
});
fileStream.on("error", reject);
});
// 5. Commit route metadata file detailing caching and tags
if (metadata) {
const metadataPath = path.join(OUT_DIR, finalReqPath, "metadata.json");
await fs.writeFile(
metadataPath,
JSON.stringify({
revalidate: metadata.revalidate,
generatedAt: Date.now(),
effects: metadata.effects,
tags: metadata.tags || [],
}, null, 2),
"utf8"
);
}
const status = capturedStatus.value || 200;
const success = status !== 500;
return {
success,
type: "html",
reqPath: finalReqPath,
tempPath: tempHtmlPath,
finalPath: htmlPath,
status: status,
};
} catch (error) {
await fs.unlink(tempHtmlPath).catch(() => {});
return { success: false, tempPath: tempHtmlPath };
}
}
module.exports = generateStaticPage;