From 0b5fbc98114bb08584bafcc9e3604e2ae24e0a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Artur=20M=C4=99dryga=C5=82?= Date: Thu, 20 Aug 2026 13:37:01 +0200 Subject: [PATCH 1/3] fix(runner): keep what we inject out of a hydrated preview document (DEV-2580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remix @ 15/16/17/18 went red in the first nightly starter matrix: every container boot logged "Hydration failed because the initial UI does not match what was rendered on the server" at RenderErrorBoundary, which the matrix's console-cleanliness assertion catches and a visitor sees as an SSR flash. The proxy seam injects the monitor and the colour-scheme receiver into the of a document the container's dev server has already rendered. remix's client entry is hydrateRoot(document, …) on React 18, which strict-matches every child of — so anything of ours there is a mismatch and the whole document is thrown away and client-rendered. next.js passes on React 19's hoistable-head semantics; astro, angular and nuxt never React-hydrate the document. MONITOR_DEMOS is "1" in production, so both injections were live and fixing one would have left remix red. Measured against the remix starter, three factors that each reproduce React #418 on their own: - the injected `; +} diff --git a/runner/packages/runtime/src/monitor.ts b/runner/packages/runtime/src/monitor.ts index d4cb8cf3e..d7a25d7aa 100644 --- a/runner/packages/runtime/src/monitor.ts +++ b/runner/packages/runtime/src/monitor.ts @@ -12,6 +12,8 @@ // `@handsontable/demo-runtime`, so the reporter source must never be duplicated // into workers/api — a second copy is a second set of caps to keep in sync. +import { injectedScriptTag } from "./inject-html.js"; + /** The `postMessage` discriminator. Also the injection idempotency marker. */ export const MONITOR_MESSAGE_TYPE = "hot-runner-monitor"; @@ -646,22 +648,29 @@ function alreadyInjected(source: string): boolean { * fault raised while the demo's own scripts evaluate is exactly the class we are * here for, so the reporter has to be hooked before them. * + * Inserted with no surrounding whitespace, and the tag deletes its own element + * (see `inject-html.ts`): a React 18 hydrator that owns the whole document — remix's + * `hydrateRoot(document, …)` — strict-matches every child of ``, and a leftover + * newline text node fails that match exactly as the ``; + const tag = injectedScriptTag(REPORTER_SOURCE); const head = /]*>/i.exec(html); if (head) { const at = head.index + head[0].length; - return html.slice(0, at) + "\n" + tag + html.slice(at); + return html.slice(0, at) + tag + html.slice(at); } const body = /]*>/i.exec(html); if (body) { const at = body.index + body[0].length; - return html.slice(0, at) + "\n" + tag + html.slice(at); + return html.slice(0, at) + tag + html.slice(at); } - return tag + "\n" + html; + return tag + html; } /** diff --git a/runner/packages/runtime/src/scheme.ts b/runner/packages/runtime/src/scheme.ts index 58082e440..4d84125ef 100644 --- a/runner/packages/runtime/src/scheme.ts +++ b/runner/packages/runtime/src/scheme.ts @@ -25,6 +25,8 @@ // reason: the API worker already depends on this package, so a second copy in // `workers/api` would be a second set of rules to keep in sync. +import { injectedScriptTag } from "./inject-html.js"; + /** The `postMessage` discriminator. Also the injection idempotency marker. */ export const SCHEME_MESSAGE_TYPE = "hot-runner-scheme"; @@ -59,8 +61,10 @@ export function isSchemeReady(message: unknown): message is SchemeReadyMessage { return !!value && value.source === SCHEME_MESSAGE_TYPE && value.ready === true; } -/** The element id the receiver owns, so the override can be found and replaced - * rather than accumulating one `