Client-only edits are sent to the browser right after the hydration build, without re-running server functions; SSR rebuilds afterwards and props are re-fetched only when a .server.ts output changed or after fullRebuild. Page requests wait for an in-flight SSR rebuild. The SSR build no longer runs Tailwind, the PostCSS processor is reused across rebuilds, and the watcher debounce drops from 200ms to 50ms. Co-Authored-By: Claude Opus 5.5 <[email protected]>
178 lines
8.5 KiB
JavaScript
178 lines
8.5 KiB
JavaScript
import {} from "esbuild";
|
|
import { log } from "../../../utils/log";
|
|
import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result";
|
|
import _ from "lodash";
|
|
import enqueueSSRRebuild from "../pages-ssr-context-bundler";
|
|
import grabDirNames from "../../../utils/grab-dir-names";
|
|
import { logBuildErrorsOnce, resetBuildErrorLog, } from "../../../utils/build-error-log";
|
|
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
|
|
import fullRebuild from "../../server/full-rebuild";
|
|
import path from "path";
|
|
import cleanupLogsDirs from "../../cleanup-logs-dir";
|
|
import grabRelativePath from "../../../utils/grab-relative-path";
|
|
import cleanupArtifacts from "../../server/cleanup-artifacts";
|
|
import { pushClientUpdate } from "../../server/server-post-build-fn";
|
|
const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames();
|
|
let build_start = 0;
|
|
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, refresh_props_on_first_build, }) {
|
|
let force_props_refresh = Boolean(refresh_props_on_first_build);
|
|
const artifactTracker = {
|
|
name: "artifact-tracker",
|
|
setup(build) {
|
|
build.onStart(async () => {
|
|
build_start = performance.now();
|
|
});
|
|
build.onEnd(async (result) => {
|
|
if (result.errors.length > 0) {
|
|
global.BUNEXT_BUILD_FAILED = true;
|
|
global.BUNEXT_RECOMPILING = false;
|
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
|
// Rebuild attempts repeat for every file edit / request
|
|
// while the error persists; only log the error once.
|
|
logBuildErrorsOnce(result.errors, (line) => log.error(` ${line}`));
|
|
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
|
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
|
try {
|
|
controller?.controller?.enqueue(`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`);
|
|
}
|
|
catch {
|
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
const artifacts = grabArtifactsFromBundledResults({
|
|
result,
|
|
entryToPage,
|
|
});
|
|
// Pages whose entry output hash changed since the previous
|
|
// build. With `splitting`, a change to any shared chunk also
|
|
// re-hashes every page entry that imports it, so this set
|
|
// covers all pages whose client graph actually changed.
|
|
const changed_pages = [];
|
|
if (artifacts?.[0] && artifacts.length > 0) {
|
|
for (let i = 0; i < artifacts.length; i++) {
|
|
const artifact = artifacts[i];
|
|
if (artifact?.local_path &&
|
|
global.BUNEXT_BUNDLER_CTX_MAP) {
|
|
const relative_path = grabRelativePath(artifact.local_path);
|
|
const prior = global.BUNEXT_BUNDLER_CTX_MAP[relative_path];
|
|
if (!prior?.path ||
|
|
prior.path !== artifact.path ||
|
|
prior.css_path !== artifact.css_path) {
|
|
changed_pages.push(relative_path);
|
|
}
|
|
global.BUNEXT_BUNDLER_CTX_MAP[relative_path] =
|
|
_.merge(prior, {
|
|
...artifact,
|
|
local_path: relative_path,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const elapsed = (performance.now() - build_start).toFixed(0);
|
|
log.success(`[Built] in ${elapsed}ms`);
|
|
// A successful build clears the "last error" so a NEW incident
|
|
// with the same signature is reported again.
|
|
resetBuildErrorLog();
|
|
if (result.warnings.length > 0) {
|
|
logBuildErrorsOnce(result.warnings, (line) => log.warn(line));
|
|
}
|
|
global.BUNEXT_BUILD_FAILED = false;
|
|
global.BUNEXT_BUNDLER_CTX_DISPOSED = false;
|
|
const props_refresh_forced = force_props_refresh;
|
|
force_props_refresh = false;
|
|
const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
|
// Client-only changes are pushed before the SSR rebuild;
|
|
// anything that may change server props waits for SSR so
|
|
// props are fresh. The rebuild is single-flighted: concurrent hydration
|
|
// rebuilds join the in-flight SSR rebuild instead of
|
|
// starting overlapping ones.
|
|
if (build_only) {
|
|
try {
|
|
await enqueueSSRRebuild();
|
|
}
|
|
catch (error) {
|
|
log.error(`SSR Bundler Error: ${error}`);
|
|
}
|
|
}
|
|
else if (does_error_file_exist) {
|
|
mkdirSync(BUNX_ERROR_LOGS_DIR, { recursive: true });
|
|
cpSync(BUNX_BUNDLER_ERROR_EXIT_FILE, path.join(BUNX_ERROR_LOGS_DIR, `${Date.now()}.log`));
|
|
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
|
|
cleanupLogsDirs();
|
|
await fullRebuild();
|
|
}
|
|
else if (changed_pages.length > 0 &&
|
|
post_build_fn &&
|
|
!props_refresh_forced) {
|
|
// Client graph changed: push the new bundle right away.
|
|
// Server functions live in `.server.ts` files, outside
|
|
// the client graph, so the browser's current props are
|
|
// still valid and it needn't wait for the SSR rebuild.
|
|
pushClientUpdate(changed_pages);
|
|
const server_hashes_before = grabServerEntryPaths();
|
|
try {
|
|
await enqueueSSRRebuild();
|
|
}
|
|
catch (error) {
|
|
log.error(`SSR Bundler Error: ${error}`);
|
|
}
|
|
// A module shared by a page and its server file changed:
|
|
// props may differ, so re-fetch them for every tab.
|
|
if (server_hashes_before !== grabServerEntryPaths()) {
|
|
try {
|
|
await post_build_fn({
|
|
artifacts: artifacts || [],
|
|
changed_pages: [],
|
|
});
|
|
}
|
|
catch (error) {
|
|
log.error(`Post-build Error: ${error}`);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
await enqueueSSRRebuild();
|
|
}
|
|
catch (error) {
|
|
log.error(`SSR Bundler Error: ${error}`);
|
|
}
|
|
if (artifacts?.[0] && artifacts.length > 0) {
|
|
try {
|
|
await post_build_fn?.({
|
|
artifacts,
|
|
changed_pages,
|
|
});
|
|
}
|
|
catch (error) {
|
|
log.error(`Post-build Error: ${error}`);
|
|
}
|
|
}
|
|
}
|
|
// Prune orphaned entry outputs from previous rebuilds.
|
|
await cleanupArtifacts({ paradigm: "hydration" });
|
|
global.BUNEXT_RECOMPILING = false;
|
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
|
});
|
|
},
|
|
};
|
|
return artifactTracker;
|
|
}
|
|
/**
|
|
* Fingerprint of the SSR outputs of all `.server.ts(x)` entries. Outputs are
|
|
* content-hashed, so a changed fingerprint means some server function's code
|
|
* (or a module it imports) changed.
|
|
*/
|
|
function grabServerEntryPaths() {
|
|
const map = global.BUNEXT_SSR_BUNDLER_CTX_MAP;
|
|
if (!map)
|
|
return "";
|
|
return Object.keys(map)
|
|
.filter((key) => key.match(/\.server\.tsx?$/))
|
|
.sort()
|
|
.map((key) => `${key}:${map[key]?.path || ""}`)
|
|
.join("\n");
|
|
}
|