SPB Git

spb/findata

Public
2.8 KB · 67 lines javascript
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
Files · 8a7c5bd1586c5a07c48686688f6b66cf82f186ca
Menu

Theme