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:
Alex Kirszenberg 2022-10-19 18:06:40 +02:00 committed by GitHub
parent 381badcde3
commit aff0a0f7c5
5 changed files with 35 additions and 17 deletions

View file

@ -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,

View file

@ -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": {

View file

@ -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));

View file

@ -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: {

View file

@ -0,0 +1 @@
export type ChunkGroup = Array<{ path: string; chunkId: string }>;