- 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]>
99 lines
4.1 KiB
JavaScript
99 lines
4.1 KiB
JavaScript
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();
|
|
const { root_file_path } = grabRootFilePath();
|
|
// const target_path = pagePathTransform({ page_path: page_local_path });
|
|
// const target_root_path = root_file_path
|
|
// ? pagePathTransform({ page_path: root_file_path })
|
|
// : undefined;
|
|
if (!existsSync(page_local_path)) {
|
|
return undefined;
|
|
}
|
|
if (root_file_path) {
|
|
if (!existsSync(root_file_path)) {
|
|
return undefined;
|
|
}
|
|
const root_content = await Bun.file(root_file_path).text();
|
|
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 (!hasBundlableDefaultExport({
|
|
file_path: page_local_path,
|
|
content: page_content,
|
|
})) {
|
|
return undefined;
|
|
}
|
|
let txt = ``;
|
|
txt += `import { hydrateRoot } from "${ROOT_DIR}/node_modules/react-dom/client.js";\n`;
|
|
// txt += `import react from "${ROOT_DIR}/node_modules/react/index.js";\n`;
|
|
if (root_file_path) {
|
|
txt += `import Root from "${root_file_path}";\n`;
|
|
}
|
|
txt += `import Page from "${page_local_path}";\n\n`;
|
|
txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
|
|
if (root_file_path) {
|
|
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
|
}
|
|
else {
|
|
txt += `const component = <Page {...pageProps} />\n`;
|
|
}
|
|
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
|
|
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;
|
|
txt += `} else {\n`;
|
|
txt += ` const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component, { onRecoverableError: () => {\n\n`;
|
|
txt += ` console.log(\`Hydration Error.\`)\n\n`;
|
|
txt += ` } });\n\n`;
|
|
txt += ` window.${ClientRootComponentWindowName} = root;\n`;
|
|
txt += ` window.__BUNEXT_RERENDER__ = (NewPage) => {\n`;
|
|
txt += ` const props = window.${ClientWindowPagePropsName} || {};\n`;
|
|
txt += ` root.render(<NewPage {...props} />);\n`;
|
|
txt += ` };\n`;
|
|
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;
|
|
}
|