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:
+1
@@ -37,6 +37,7 @@ async function doFullRebuild(params) {
|
||||
global.BUNEXT_ROUTER.reload();
|
||||
await disposeBundlerContexts();
|
||||
await allPagesESBuildContextBundler({
|
||||
refresh_props: true,
|
||||
post_build_fn: async (params) => {
|
||||
await serverPostBuildFn(params);
|
||||
},
|
||||
|
||||
@@ -10,5 +10,14 @@ type Params = {
|
||||
*/
|
||||
changed_pages?: string[];
|
||||
};
|
||||
/**
|
||||
* # Push a client-only HMR update
|
||||
* Sends the new client bundle to every controller whose page is in
|
||||
* `changed_pages`, without re-running server functions. The payload carries
|
||||
* no `page_props`, so the browser keeps its current props and only swaps the
|
||||
* hydration script/CSS. Used right after the hydration build so the browser
|
||||
* doesn't wait for the SSR rebuild.
|
||||
*/
|
||||
export declare function pushClientUpdate(changed_pages: string[]): void;
|
||||
export default function serverPostBuildFn(params?: Params): Promise<void>;
|
||||
export {};
|
||||
|
||||
+34
@@ -1,5 +1,39 @@
|
||||
import _ from "lodash";
|
||||
import grabPageComponent from "./web-pages/grab-page-component";
|
||||
/**
|
||||
* # Push a client-only HMR update
|
||||
* Sends the new client bundle to every controller whose page is in
|
||||
* `changed_pages`, without re-running server functions. The payload carries
|
||||
* no `page_props`, so the browser keeps its current props and only swaps the
|
||||
* hydration script/CSS. Used right after the hydration build so the browser
|
||||
* doesn't wait for the SSR rebuild.
|
||||
*/
|
||||
export function pushClientUpdate(changed_pages) {
|
||||
if (!global.BUNEXT_HMR_CONTROLLERS?.[0] || !global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||
return;
|
||||
}
|
||||
const changed_set = new Set(changed_pages);
|
||||
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||
const local_path = controller?.target_map?.local_path;
|
||||
if (!controller || !local_path || !changed_set.has(local_path)) {
|
||||
continue;
|
||||
}
|
||||
const target_artifact = global.BUNEXT_BUNDLER_CTX_MAP[local_path];
|
||||
const payload = target_artifact?.path
|
||||
? { target_map: target_artifact }
|
||||
: { reload: true };
|
||||
try {
|
||||
controller.controller.enqueue(`event: update\ndata: ${JSON.stringify(payload)}\n\n`);
|
||||
if (target_artifact?.path) {
|
||||
controller.target_map = target_artifact;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default async function serverPostBuildFn(params) {
|
||||
if (!global.BUNEXT_HMR_CONTROLLERS?.[0] || !global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||
return;
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ const TARGET_FILES_MATCH = /\.(tsx?|jsx?|css)$/;
|
||||
// atomic-swap noise), and edits can land while a rebuild is running.
|
||||
// Events are debounced into a single runner; the runner loops until the
|
||||
// event queue drains, so a mid-build edit is never silently dropped.
|
||||
const REBUILD_DEBOUNCE_MS = 200;
|
||||
const REBUILD_DEBOUNCE_MS = 50;
|
||||
let pending_structural = false;
|
||||
let pending_content_file = undefined;
|
||||
let debounce_timer = undefined;
|
||||
|
||||
@@ -5,6 +5,7 @@ import getCache from "../../cache/get-cache";
|
||||
import generateWebPageResponseFromComponentReturn from "./generate-web-page-response-from-component-return";
|
||||
import grabPageComponent from "./grab-page-component";
|
||||
import grabPageErrorComponent from "./grab-page-error-component";
|
||||
import { awaitSSRRebuild } from "../../bundler/pages-ssr-context-bundler";
|
||||
export default async function handleWebPages({ req, }) {
|
||||
try {
|
||||
const is_dev = isDevelopment();
|
||||
@@ -31,6 +32,11 @@ export default async function handleWebPages({ req, }) {
|
||||
}
|
||||
catch (error) { }
|
||||
}
|
||||
// HMR pushes client updates before the SSR rebuild finishes; a page
|
||||
// load in that window waits so SSR HTML matches the new bundle.
|
||||
if (is_dev) {
|
||||
await awaitSSRRebuild();
|
||||
}
|
||||
const componentRes = await grabPageComponent({
|
||||
req,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
import * as esbuild from "esbuild";
|
||||
type Params = {
|
||||
/**
|
||||
* Run `.css` files through Tailwind/PostCSS. When `false`, every `.css`
|
||||
* import loads as an empty stylesheet (for builds whose CSS output is
|
||||
* never used, e.g. SSR).
|
||||
*/
|
||||
process_css?: boolean;
|
||||
};
|
||||
export declare function createTailwindEsbuildPlugin({ process_css, }?: Params): esbuild.Plugin;
|
||||
/**
|
||||
* Resolves `.css` imports like the Tailwind plugin but loads them empty.
|
||||
*/
|
||||
export declare const cssStubEsbuildPlugin: esbuild.Plugin;
|
||||
declare const tailwindEsbuildPlugin: esbuild.Plugin;
|
||||
export default tailwindEsbuildPlugin;
|
||||
|
||||
+51
-38
@@ -8,42 +8,55 @@ import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import { log } from "../../../utils/log";
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
let error_logged = false;
|
||||
const tailwindEsbuildPlugin = {
|
||||
name: "tailwindcss",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||
try {
|
||||
const source = await readFile(args.path, "utf-8");
|
||||
const result = await postcss([tailwindcss()]).process(source, {
|
||||
from: args.path,
|
||||
});
|
||||
error_logged = false;
|
||||
return { contents: result.css, loader: "css" };
|
||||
}
|
||||
catch (error) {
|
||||
return { errors: [{ text: error.message }] };
|
||||
}
|
||||
});
|
||||
build.onResolve({ filter: /\.css$/ }, async (args) => {
|
||||
const css_path = path.resolve(args.resolveDir, args.path.replace(/\@\//g, ROOT_DIR + "/"));
|
||||
const does_path_exist = existsSync(css_path);
|
||||
if (!does_path_exist && !error_logged) {
|
||||
const err_msg = `CSS Error: ${css_path} file does not exist.`;
|
||||
log.error(err_msg);
|
||||
error_logged = true;
|
||||
// return {
|
||||
// errors: [
|
||||
// {
|
||||
// text: err_msg,
|
||||
// },
|
||||
// ],
|
||||
// pluginName: "tailwindcss",
|
||||
// };
|
||||
}
|
||||
return {
|
||||
path: css_path,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
// One processor for the whole process, so the Tailwind plugin's internal
|
||||
// state survives across rebuilds instead of being recreated per file load.
|
||||
let css_processor;
|
||||
function grabCSSProcessor() {
|
||||
if (!css_processor) {
|
||||
css_processor = postcss([tailwindcss()]);
|
||||
}
|
||||
return css_processor;
|
||||
}
|
||||
export function createTailwindEsbuildPlugin({ process_css = true, } = {}) {
|
||||
return {
|
||||
name: process_css ? "tailwindcss" : "css-stub",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||
if (!process_css) {
|
||||
return { contents: "", loader: "css" };
|
||||
}
|
||||
try {
|
||||
const source = await readFile(args.path, "utf-8");
|
||||
const result = await grabCSSProcessor().process(source, {
|
||||
from: args.path,
|
||||
});
|
||||
error_logged = false;
|
||||
return { contents: result.css, loader: "css" };
|
||||
}
|
||||
catch (error) {
|
||||
return { errors: [{ text: error.message }] };
|
||||
}
|
||||
});
|
||||
build.onResolve({ filter: /\.css$/ }, async (args) => {
|
||||
const css_path = path.resolve(args.resolveDir, args.path.replace(/\@\//g, ROOT_DIR + "/"));
|
||||
const does_path_exist = existsSync(css_path);
|
||||
if (!does_path_exist && !error_logged) {
|
||||
const err_msg = `CSS Error: ${css_path} file does not exist.`;
|
||||
log.error(err_msg);
|
||||
error_logged = true;
|
||||
}
|
||||
return {
|
||||
path: css_path,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Resolves `.css` imports like the Tailwind plugin but loads them empty.
|
||||
*/
|
||||
export const cssStubEsbuildPlugin = createTailwindEsbuildPlugin({
|
||||
process_css: false,
|
||||
});
|
||||
const tailwindEsbuildPlugin = createTailwindEsbuildPlugin();
|
||||
export default tailwindEsbuildPlugin;
|
||||
|
||||
Reference in New Issue
Block a user