Date sitemap lastmod from the build pipeline; warn on skipped pages

- bunext build records when each page's client bundle hash last changed
  in .bunext/page-lastmod.json (sitemap.lastmod_file to relocate). The
  hash covers the page's whole import graph, so component edits re-date
  every page that uses them. The sitemap uses these dates as lastmod
- Add sitemap.default_lastmod
- BREAKING: use_file_lastmod now defaults to false (file mtimes are reset
  by checkouts); it is a fallback after the build record
- Warn when a page or __root.tsx is skipped because its default export
  is not at the start of a line (e.g. export { Page as default })
- Replace leaking module mocks in tests (log, grab-root-file-path) with
  spies and fixtures

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-14 18:25:38 +01:00
co-authored by Claude Opus 5.5
parent be4bebaa22
commit 3a75880ae4
26 changed files with 877 additions and 54 deletions
+44 -2
View File
@@ -2,6 +2,8 @@ import { existsSync } from "fs";
import grabDirNames from "../../utils/grab-dir-names";
import grabConstants from "../../utils/grab-constants";
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
import grabRelativePath from "../../utils/grab-relative-path";
import { log } from "../../utils/log";
const { ROOT_DIR } = grabDirNames();
export default async function grabClientHydrationScript({ page_local_path, }) {
const { ClientRootElementIDName, ClientRootComponentWindowName, ClientWindowPagePropsName, } = grabConstants();
@@ -18,12 +20,19 @@ export default async function grabClientHydrationScript({ page_local_path, }) {
return undefined;
}
const root_content = await Bun.file(root_file_path).text();
if (!root_content.match(/^export default/m)) {
if (!hasBundlableDefaultExport({
file_path: root_file_path,
content: root_content,
is_root: true,
})) {
return undefined;
}
}
const page_content = await Bun.file(page_local_path).text();
if (!page_content.match(/^export default/m)) {
if (!hasBundlableDefaultExport({
file_path: page_local_path,
content: page_content,
})) {
return undefined;
}
let txt = ``;
@@ -54,3 +63,36 @@ export default async function grabClientHydrationScript({ page_local_path, }) {
txt += `}\n`;
return txt;
}
// File path -> content hash of the last warning, so rebuilds don't repeat
// a warning until the file changes.
const warned_files = new Map();
/**
* # Check a page or root file can be bundled
* Files are only bundled when `export default` starts a line. Files with
* no default export at all (e.g. a page still being written) are skipped
* silently; this warns about the confusing case where one exists
* elsewhere, like `import X from "x"; export default X` on one line, or
* `export { Page as default }`.
*/
function hasBundlableDefaultExport({ file_path, content, is_root, }) {
if (/^export default/m.test(content)) {
warned_files.delete(file_path);
return true;
}
const has_default_export = /\bexport\s+default\b/.test(content) ||
/\bas\s+default\b/.test(content);
if (!has_default_export) {
warned_files.delete(file_path);
return false;
}
const content_hash = Bun.hash(content);
if (warned_files.get(file_path) === content_hash)
return false;
warned_files.set(file_path, content_hash);
log.warn(`${grabRelativePath(file_path)} was skipped: ` +
`\`export default\` must be at the start of a line. ` +
(is_root
? `No pages can be bundled until the root layout is fixed.`
: `This page will not be bundled.`));
return false;
}