Files
James Anderson 2a2ce31ecc fix(app-router): support soft navigation in static exports (#3112)
* fix(app-router): support soft navigation in static exports

* test(stream): drain transforms before releasing readers

* fix(cache): preserve prerender stale metadata across footer race

* fix(app-router): validate static export navigation payloads

* fix(app-router): handle static export basePath roots

* fix(export): align basePath artifact namespace

* fix(export): namespace basePath static artifacts

* fix(export): normalize trailing basePath artifacts

* fix(export): namespace public assets under basePath

* test(export): avoid tainted fixture HTML
2026-08-27 11:01:04 +01:00

66 lines
2.7 KiB
JavaScript

import { access, readFile, readdir } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("../dist/client/", import.meta.url));
const expected = new Map([
["index.html", "Static does not mean inert"],
["index.txt", "Static does not mean inert"],
["catalog/pocket-observatory/index.html", "Pocket Observatory"],
["catalog/pocket-observatory/index.txt", "Pocket Observatory"],
["catalog/tide-clock/index.html", "Tide Clock"],
["catalog/trail-journal/index.html", "Trail Journal"],
["docs/building/the-artifact/index.html", "Read the artifact"],
["docs/deployment/cloudflare/index.html", "Deploy without a server"],
["browser-state/index.html", "Interactivity survives the export"],
["search/index.html", "The browser owns the query string"],
["search/index.txt", "The browser owns the query string"],
["legacy/index.html", "Two routers, one static artifact"],
["products/atlas/index.html", "Atlas Field Kit"],
["products/lantern/index.html", "Low-light Lantern"],
["404.html", "This route was never in the map"],
["404/index.html", "This route was never in the map"],
["robots.txt", "User-agent: *"],
["icon.svg", "<svg"],
["island-grid.svg", "<svg"],
]);
const failures = [];
for (const [relativePath, marker] of expected) {
const path = join(root, relativePath);
try {
await access(path);
const contents = await readFile(path, "utf8");
// React may insert text separators between adjacent JSX expressions.
const comparable = relativePath.endsWith(".html")
? contents.replaceAll("<!-- -->", "")
: contents;
if (!comparable.includes(marker)) failures.push(`${relativePath} does not contain ${JSON.stringify(marker)}`);
} catch {
failures.push(`${relativePath} is missing`);
}
}
async function listFiles(directory, prefix = "") {
const files = [];
for (const entry of await readdir(directory, { withFileTypes: true })) {
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) files.push(...await listFiles(join(directory, entry.name), relative));
else files.push(relative);
}
return files;
}
const files = await listFiles(root);
for (const template of ["catalog/[slug]", "docs/[...slug]", "products/[id]"]) {
if (files.some((file) => file.includes(template))) failures.push(`dynamic template leaked into output: ${template}`);
}
if (failures.length > 0) {
console.error("Static export verification failed:\n" + failures.map((failure) => ` - ${failure}`).join("\n"));
process.exit(1);
}
console.log(`Verified ${expected.size} representative files in dist/client (${files.length} files total).`);