Push HMR updates before the SSR rebuild in dev
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]>
This commit is contained in:
@@ -6,6 +6,11 @@ type Params = {
|
||||
}) => Promise<void> | void;
|
||||
build_only?: boolean;
|
||||
start?: boolean;
|
||||
/**
|
||||
* Re-fetch server props for every HMR client after the build, even if
|
||||
* only client code changed. Only applies when the context is recreated.
|
||||
*/
|
||||
refresh_props?: boolean;
|
||||
};
|
||||
/**
|
||||
* True when `local_path` is a hydration entry of the current esbuild context.
|
||||
|
||||
@@ -88,6 +88,7 @@ export default async function allPagesESBuildContextBundler(params) {
|
||||
entryToPage,
|
||||
post_build_fn: params?.post_build_fn,
|
||||
build_only: params?.build_only || params?.start,
|
||||
refresh_props_on_first_build: params?.refresh_props,
|
||||
}),
|
||||
],
|
||||
jsx: "automatic",
|
||||
|
||||
@@ -15,4 +15,9 @@ type RebuildParams = {
|
||||
* hydration context rebuild, which re-enters this queue).
|
||||
*/
|
||||
export default function enqueueSSRRebuild(params?: RebuildParams): Promise<void>;
|
||||
/**
|
||||
* Resolves once the in-flight SSR rebuild (if any) has finished. Lets page
|
||||
* requests avoid rendering stale SSR modules against a newer client bundle.
|
||||
*/
|
||||
export declare function awaitSSRRebuild(): Promise<void>;
|
||||
export {};
|
||||
|
||||
+14
-2
@@ -2,7 +2,7 @@ import * as esbuild from "esbuild";
|
||||
import grabAllPages from "../../utils/grab-all-pages";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||
import { cssStubEsbuildPlugin } from "../server/web-pages/tailwind-esbuild-plugin";
|
||||
import grabPageReactComponentString from "../server/web-pages/grab-page-react-component-string";
|
||||
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||
import ssrVirtualFilesPlugin from "./plugins/ssr-virtual-files-plugin";
|
||||
@@ -87,6 +87,16 @@ export default async function enqueueSSRRebuild(params) {
|
||||
rebuild_in_flight = run;
|
||||
return run;
|
||||
}
|
||||
/**
|
||||
* Resolves once the in-flight SSR rebuild (if any) has finished. Lets page
|
||||
* requests avoid rendering stale SSR modules against a newer client bundle.
|
||||
*/
|
||||
export async function awaitSSRRebuild() {
|
||||
try {
|
||||
await rebuild_in_flight;
|
||||
}
|
||||
catch (error) { }
|
||||
}
|
||||
async function doSSRRebuild(params) {
|
||||
const force = Boolean(params?.force);
|
||||
const dev = isDevelopment();
|
||||
@@ -137,7 +147,9 @@ async function doSSRRebuild(params) {
|
||||
entryNames: "[dir]/[hash]",
|
||||
metafile: true,
|
||||
plugins: [
|
||||
tailwindEsbuildPlugin,
|
||||
// SSR CSS output is never served (HTML links the
|
||||
// hydration build's CSS), so skip Tailwind here.
|
||||
cssStubEsbuildPlugin,
|
||||
ssrVirtualFilesPlugin({
|
||||
getEntryToPage: () => current_entry_map,
|
||||
}),
|
||||
|
||||
@@ -9,6 +9,12 @@ type Params = {
|
||||
changed_pages?: string[];
|
||||
}) => Promise<void> | void;
|
||||
build_only?: boolean;
|
||||
/**
|
||||
* Always re-fetch server props after this context's first build, even
|
||||
* when only client code changed (set by `fullRebuild`, e.g. after a
|
||||
* config change).
|
||||
*/
|
||||
refresh_props_on_first_build?: boolean;
|
||||
};
|
||||
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, }: Params): Plugin;
|
||||
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, refresh_props_on_first_build, }: Params): Plugin;
|
||||
export {};
|
||||
|
||||
@@ -11,9 +11,11 @@ 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, }) {
|
||||
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) {
|
||||
@@ -78,9 +80,12 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn,
|
||||
}
|
||||
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);
|
||||
// SSR must finish before HMR so server props are fresh.
|
||||
// The rebuild is single-flighted: concurrent hydration
|
||||
// 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) {
|
||||
@@ -98,6 +103,35 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn,
|
||||
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();
|
||||
@@ -126,3 +160,18 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn,
|
||||
};
|
||||
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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user