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')) .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); }