1#!/usr/bin/env node2/**3 * Post-build step for the static export.4 *5 * The API serves the console with `Content-Security-Policy: script-src 'self'` (no 'unsafe-inline').6 * Next.js App Router emits the React Server Components payload as inline7 * `<script>self.__next_f.push([...])</script>` tags, which such a CSP blocks — hydration would never run.8 *9 * This script rewrites every `out/**\/*.html`: the inline scripts (all of them are `__next_f` pushes) are10 * concatenated, in document order, into one external file `out/_next/static/flight/<hash>.js`, and a single11 * blocking `<script src=…>` is inserted where the first inline script was. Pushing the flight data from an12 * external file is order-independent for Next's client runtime (it buffers pushes until the app chunk takes13 * over and closes the stream on DOMContentLoaded, which a blocking script always precedes).14 *15 * Fails loudly if an inline script that is not a `__next_f` push is found, so the CSP contract stays explicit.16 */17import { createHash } from "node:crypto";18import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";19import { dirname, join, relative } from "node:path";20import { fileURLToPath } from "node:url";2122const root = join(dirname(fileURLToPath(import.meta.url)), "..");23const out = join(root, "out");24const flightDir = join(out, "_next", "static", "flight");2526function walk(dir, acc = []) {27 for (const name of readdirSync(dir)) {28 const p = join(dir, name);29 const st = statSync(p);30 if (st.isDirectory()) {31 if (name === "_next") continue;32 walk(p, acc);33 } else if (name.endsWith(".html")) acc.push(p);34 }35 return acc;36}3738const INLINE = /<script>([\s\S]*?)<\/script>/g;39let files = 0, scripts = 0, bytes = 0;40mkdirSync(flightDir, { recursive: true });4142for (const file of walk(out)) {43 const html = readFileSync(file, "utf8");44 const chunks = [];45 let first = -1;46 for (const m of html.matchAll(INLINE)) {47 const body = m[1];48 if (!/^\s*\(?self\.__next_f/.test(body)) {49 throw new Error(`${relative(root, file)}: unexpected inline <script> (not a __next_f push):\n${body.slice(0, 200)}`);50 }51 if (first < 0) first = m.index;52 chunks.push(body.trim().replace(/;?$/, ";"));53 }54 if (!chunks.length) continue;55 const js = chunks.join("\n") + "\n";56 const hash = createHash("sha256").update(js).digest("hex").slice(0, 16);57 const name = `${hash}.js`;58 writeFileSync(join(flightDir, name), js);59 let i = 0;60 const rewritten = html.replace(INLINE, () => (i++ === 0 ? `<script src="/_next/static/flight/${name}"></script>` : ""));61 writeFileSync(file, rewritten);62 files++;63 scripts += chunks.length;64 bytes += js.length;65}66console.log(`externalize-inline-scripts: ${files} html files, ${scripts} inline scripts moved to _next/static/flight (${(bytes / 1024).toFixed(1)} kB)`);67