import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs"; import { join, dirname, basename } from "node:path"; // Usage: node extract-static.mjs // e.g. node extract-static.mjs .next\server\app ..\refs-html const root = process.argv[2]; const dest = process.argv[3]; // --- inject a minimal theme toggle (extraction strips all `; function injectThemeToggle(html) { if (html.includes("__themeToggleInjected")) return html; const tag = ""; if (html.includes(tag)) return html.replace(tag, THEME_TOGGLE + "\n" + tag); if (html.includes("")) return html.replace("", THEME_TOGGLE + "\n"); return html + "\n" + THEME_TOGGLE; } // --- CSS: find the compiled chunk and pretty-print it --- const cssChunks = join(root, "..", "..", "static", "chunks"); const cssFile = readdirSync(cssChunks) .filter((f) => f.endsWith(".css")) .sort((a, b) => statSync(join(cssChunks, b)).size - statSync(join(cssChunks, a)).size)[0]; const css = readFileSync(join(cssChunks, cssFile), "utf8"); const prettyCss = css .replace(/\r/g, "") .replace(/\{/g, " {\n ") .replace(/;/g, ";\n ") .replace(/\}/g, "\n}\n") .replace(/\n{3,}/g, "\n\n") .trim(); // --- conservative html formatter --- const VOID = new Set([ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr", ]); const RAW = new Set(["pre", "textarea", "script", "style"]); function fmtHtml(html) { const tokens = html.match(/|]*>|<\/?[a-zA-Z][^>]*>|[^<]+|`, "i").test(tk)) { depth--; rawTag = null; } continue; } const isOpen = /^<[a-zA-Z]/.test(tk); const isClose = /^<\//.test(tk); let name = ""; if (isOpen || isClose) { name = (tk.match(/^<\/?([a-zA-Z0-9-]+)/) || [])[1]?.toLowerCase() || ""; } if (isOpen && RAW.has(name)) { out.push("\n" + " ".repeat(depth) + tk); if (!/\/>$/.test(tk)) { depth++; rawTag = name; } } else if (isOpen && !RAW.has(name)) { out.push("\n" + " ".repeat(depth) + tk); if (!/\/>$/.test(tk) && !VOID.has(name)) depth++; } else if (isClose) { depth = Math.max(0, depth - 1); out.push("\n" + " ".repeat(depth) + tk); } else if (/^ !basename(rel).startsWith("_") && !basename(rel).startsWith("404")) // `index.html` нам не нужен как реф, поэтому не генерируем его. .filter((rel) => basename(rel).toLowerCase() !== "index.html") .map((rel) => [rel, basename(rel)]); console.log(`discovered ${pages.length} page(s):`, pages.map((p) => p[1]).join(", ")); for (const [src, out] of pages) { let html = readFileSync(join(root, src), "utf8"); // strip scripts — pure static snapshot html = html.replace(//g, ""); html = html.replace(/]+rel="preload"[^>]+as="script"[^>]*>/g, ""); // inline compiled css instead of linking /_next/... html = html.replace(/]+rel="stylesheet"[^>]*>/g, () => ""); html = fmtHtml(html); html = injectThemeToggle(html); const file = join(dest, out); mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, html); console.log("written:", file); }