Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clean-server-component-hydration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'vite-plugin-solid': patch
---

Prevent generated server-component hydration from shifting the `<head>` structure. The bootstrap now runs at the start of the existing hydration script instead of adding an unexpected first child to `<head>`.
19 changes: 11 additions & 8 deletions examples/turnkey/test/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1433,19 +1433,22 @@ async function runFramesChecks(mode, origin) {
html.split(`${FRAMES_SECRET}-one`).length === 2,
);
record(mode, 'document', 't=0 slot records shipped (sc:slot)', html.includes('sc:slot:'));
// Presence is not enough: the render plugin serializes a placeholder as
// `self._$SC.r(id)`, so the registry must be defined BEFORE the hydration
// data script runs. Anchoring the injection on `</head>` put it after
// <HydrationScript />, and any document whose payload carried a frame ref
// threw "Cannot read properties of undefined (reading 'r')" — killing
// hydration, so nothing in the page was interactive.
// Presence is not enough: the bootstrap must run before hydration data,
// but adding a separate script before the document's declared head nodes
// shifts Solid's structural hydration cursor. It belongs at the front of
// the existing HydrationScript instead.
const scBootstrapAt = html.indexOf('self._$SC=');
const hydrationRuntimeAt = html.indexOf('window._$HY');
const hydrationDataAt = html.indexOf('_$HY.r[');
record(
mode,
'document',
'SC bootstrap inline in head, before the hydration data script',
scBootstrapAt !== -1 && (hydrationDataAt === -1 || scBootstrapAt < hydrationDataAt),
'SC bootstrap starts the hydration script before hydration data',
scBootstrapAt !== -1 &&
hydrationRuntimeAt > scBootstrapAt &&
html.lastIndexOf('<script', scBootstrapAt) ===
html.lastIndexOf('<script', hydrationRuntimeAt) &&
(hydrationDataAt === -1 || scBootstrapAt < hydrationDataAt),
);

const chrome = startProcess(CHROME, [
Expand Down
17 changes: 8 additions & 9 deletions src/ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,9 @@ export function ssrServe(
const composeServerFunctions = (isBuild || externalDev) && internal.serverFunctions;
// Generated entries own the whole document wiring, so the handler also
// injects the server-component bootstrap (the per-function-id
// placeholder registry the render plugin references; must be inline at
// the TOP of <head>, before the hydration data script streams in — see
// the transform below). Authored entries inject it themselves.
// placeholder registry the render plugin references; must start the
// hydration runtime script, before hydration data streams in — see the
// transform below). Authored entries inject it themselves.
const injectComponentBootstrap = generated && serverComponents;

const lines = [
Expand Down Expand Up @@ -419,17 +419,16 @@ export function ssrServe(
// placeholder as `self._$SC.r(id)`, so a document whose payload carries
// one throws on that reference if the registry is not defined yet.
// <HydrationScript /> sits inside <head>, so anchoring on `</head>`
// always lands after it the opening tag is the only anchor that is
// reliably before it.
// always lands after it. Prepend the bootstrap to that existing script
// so it runs first without adding a node that shifts hydration.
lines.push(
` if (!bootstrapped) {`,
` const headOpen = /<head(\\s[^>]*)?>/.exec(chunk);`,
` if (headOpen) {`,
` const at = chunk.indexOf('window._$HY');`,
` if (at !== -1) {`,
` bootstrapped = true;`,
` const at = headOpen.index + headOpen[0].length;`,
` chunk =`,
` chunk.slice(0, at) +`,
` '<script>' + SERVER_COMPONENT_BOOTSTRAP + '</' + 'script>' +`,
` SERVER_COMPONENT_BOOTSTRAP +`,
` chunk.slice(at);`,
` }`,
` }`,
Expand Down
Loading