diff --git a/.changeset/clean-server-component-hydration.md b/.changeset/clean-server-component-hydration.md new file mode 100644 index 0000000..7d3c0b6 --- /dev/null +++ b/.changeset/clean-server-component-hydration.md @@ -0,0 +1,5 @@ +--- +'vite-plugin-solid': patch +--- + +Prevent generated server-component hydration from shifting the `` structure. The bootstrap now runs at the start of the existing hydration script instead of adding an unexpected first child to ``. diff --git a/examples/turnkey/test/run.mjs b/examples/turnkey/test/run.mjs index 91ffa56..471f6fd 100644 --- a/examples/turnkey/test/run.mjs +++ b/examples/turnkey/test/run.mjs @@ -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 `` put it after - // , 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(', 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 = [ @@ -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. // sits inside , so anchoring on `` - // 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 = /]*)?>/.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) +`, - ` '