Fix CSS HMR for SSR (vercel/turbo#85)
Since we used to build the HTML using our own `<Document>` component, we were previously adding a data-turbopack-chunk-id attribute to our `<link>` tags to reconcile chunk paths with their chunk ids when initializing HMR. However, Next.js is now responsible for building the HTML, and it has no such mechanism. **NOTE:** HMR is currently broken for non-Next-SSR rendering (HtmlAsset). This PR does not fix that.
This commit is contained in:
parent
381badcde3
commit
aff0a0f7c5
5 changed files with 35 additions and 17 deletions
|
@ -2,7 +2,9 @@ import { connect } from "./hmr-client";
|
|||
import { connectHMR } from "./websocket";
|
||||
|
||||
export function initializeHMR(options: { assetPrefix: string }) {
|
||||
connect();
|
||||
connect({
|
||||
assetPrefix: options.assetPrefix,
|
||||
});
|
||||
connectHMR({
|
||||
path: "/turbopack-hmr",
|
||||
assetPrefix: options.assetPrefix,
|
||||
|
|
|
@ -11,7 +11,11 @@ import { addEventListener, sendMessage } from "./websocket";
|
|||
|
||||
declare var globalThis: TurbopackGlobals;
|
||||
|
||||
export function connect() {
|
||||
export type ClientOptions = {
|
||||
assetPrefix: string;
|
||||
};
|
||||
|
||||
export function connect({ assetPrefix }: ClientOptions) {
|
||||
addEventListener((event) => {
|
||||
switch (event.type) {
|
||||
case "connected":
|
||||
|
@ -39,7 +43,7 @@ export function connect() {
|
|||
}
|
||||
}
|
||||
|
||||
subscribeToInitialCssChunksUpdates();
|
||||
subscribeToInitialCssChunksUpdates(assetPrefix);
|
||||
}
|
||||
|
||||
const chunkUpdateCallbacks: Map<string, ChunkUpdateCallback[]> = new Map();
|
||||
|
@ -102,15 +106,21 @@ function triggerChunkUpdate(update: ServerMessage) {
|
|||
|
||||
// Unlike ES chunks, CSS chunks cannot contain the logic to accept updates.
|
||||
// They must be reloaded here instead.
|
||||
function subscribeToInitialCssChunksUpdates() {
|
||||
function subscribeToInitialCssChunksUpdates(assetPrefix: string) {
|
||||
const initialCssChunkLinks: NodeListOf<HTMLLinkElement> =
|
||||
document.head.querySelectorAll("link");
|
||||
const cssChunkPrefix = `${assetPrefix}/`;
|
||||
initialCssChunkLinks.forEach((link) => {
|
||||
if (!link.href) return;
|
||||
const url = new URL(link.href);
|
||||
if (url.origin !== location.origin) return;
|
||||
const chunkPath = url.pathname.slice(1);
|
||||
const href = link.href;
|
||||
if (href == null) {
|
||||
return;
|
||||
}
|
||||
const { pathname, origin } = new URL(href);
|
||||
if (origin !== location.origin || !pathname.startsWith(cssChunkPrefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chunkPath = pathname.slice(cssChunkPrefix.length);
|
||||
onChunkUpdate(chunkPath, (update) => {
|
||||
switch (update.type) {
|
||||
case "restart": {
|
||||
|
|
|
@ -2,6 +2,7 @@ import "@vercel/turbopack-next/internal/shims";
|
|||
|
||||
import { initialize, hydrate } from "next/dist/client";
|
||||
import { initializeHMR } from "@vercel/turbopack-next/dev/client";
|
||||
import { displayContent } from "next/dist/client/dev/fouc";
|
||||
|
||||
import * as _app from "@vercel/turbopack-next/pages/_app";
|
||||
import * as page from ".";
|
||||
|
@ -9,23 +10,23 @@ import * as page from ".";
|
|||
(async () => {
|
||||
console.debug("Initializing Next.js");
|
||||
|
||||
initializeHMR({
|
||||
assetPrefix: "",
|
||||
});
|
||||
|
||||
await initialize({
|
||||
const { assetPrefix } = await initialize({
|
||||
webpackHMR: {
|
||||
// Expected when `process.env.NODE_ENV === 'development'`
|
||||
onUnrecoverableError() {},
|
||||
},
|
||||
});
|
||||
|
||||
initializeHMR({
|
||||
assetPrefix,
|
||||
});
|
||||
|
||||
window.__NEXT_P.push(["/_app", () => _app]);
|
||||
window.__NEXT_P.push([window.__NEXT_DATA__.page, () => page]);
|
||||
|
||||
console.debug("Hydrating the page");
|
||||
|
||||
await hydrate();
|
||||
await hydrate({ beforeRender: displayContent });
|
||||
|
||||
console.debug("The page has been hydrated");
|
||||
})().catch((err) => console.error(err));
|
||||
|
|
|
@ -10,6 +10,8 @@ import Document from "@vercel/turbopack-next/pages/_document";
|
|||
import Component, * as otherExports from ".";
|
||||
("TURBOPACK { transition: next-client }");
|
||||
import chunkGroup from ".";
|
||||
import { BuildManifest } from "next/dist/server/get-page-files";
|
||||
import { ChunkGroup } from "types/next";
|
||||
|
||||
const END_OF_OPERATION = process.argv[2];
|
||||
const NEW_LINE = "\n".charCodeAt(0);
|
||||
|
@ -168,14 +170,14 @@ async function operation(renderData: RenderData) {
|
|||
// TODO(alexkirsz) This is missing *a lot* of data, but it's enough to get a
|
||||
// basic render working.
|
||||
|
||||
/* BuildManifest */
|
||||
const buildManifest = {
|
||||
const group = chunkGroup as ChunkGroup;
|
||||
const buildManifest: BuildManifest = {
|
||||
pages: {
|
||||
// TODO(alexkirsz) We should separate _app and page chunks. Right now, we
|
||||
// computing the chunk items of `next-hydrate.js`, so they contain both
|
||||
// _app and page chunks.
|
||||
"/_app": [],
|
||||
[renderData.path]: chunkGroup.map((c: { path: string }) => c.path),
|
||||
[renderData.path]: group.map((chunk) => chunk.path),
|
||||
},
|
||||
|
||||
devFiles: [],
|
||||
|
@ -202,6 +204,8 @@ async function operation(renderData: RenderData) {
|
|||
buildId: "",
|
||||
|
||||
/* RenderOptsPartial */
|
||||
dev: true,
|
||||
runtimeConfig: {},
|
||||
assetPrefix: "",
|
||||
canonicalBase: "",
|
||||
previewProps: {
|
||||
|
|
1
packages/next-swc/crates/next-core/js/types/next.d.ts
vendored
Normal file
1
packages/next-swc/crates/next-core/js/types/next.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
export type ChunkGroup = Array<{ path: string; chunkId: string }>;
|
Loading…
Reference in a new issue