diff --git a/.changeset/dev-toolbar-portal-hydration.md b/.changeset/dev-toolbar-portal-hydration.md new file mode 100644 index 000000000..977d57a55 --- /dev/null +++ b/.changeset/dev-toolbar-portal-hydration.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +Fix the dev overlay corrupting the first client-side navigation after an SSR page load. The toolbar's `` handed the hydrating tree a client-created marker node that hydration never inserts into the DOM, and that phantom bookkeeping entry made the first route swap orphan the previous page's DOM (both pages visible at once). The toolbar now renders in its own root outside the app tree, mounted only after the SSR stream completes, since rendering during streaming steals hydration keys from pending suspense chunks. diff --git a/.changeset/dev-toolbar-prebundle-trace-mapping.md b/.changeset/dev-toolbar-prebundle-trace-mapping.md new file mode 100644 index 000000000..68d70cfb6 --- /dev/null +++ b/.changeset/dev-toolbar-prebundle-trace-mapping.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +Pre-bundle `@jridgewell/trace-mapping` for the client in dev so the dev overlay's lazily-loaded error viewer stops rejecting with `The requested module ... does not provide an export named 'default'`. The viewer's import chain is only reachable through `@solidjs/start` itself, so Vite's dep scanner never discovers it and served its CJS/UMD dependencies raw. diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 9ad41a3f7..70debd297 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -291,6 +291,16 @@ export function solidStart(options?: SolidStartOptions): Array { environments: { [VITE_ENVIRONMENTS.client]: { consumer: "client", + optimizeDeps: { + // The dev toolbar's lazily-imported error viewer reaches + // @jridgewell/trace-mapping (and its CJS/UMD deps) only through + // @solidjs/start itself, so the dep scanner never discovers it; + // unprebundled, the browser gets the raw UMD files and the + // import rejects ("does not provide an export named ..."). + // The "a > b" form resolves through @solidjs/start under + // strict (pnpm) node_modules layouts. + include: start.devOverlay ? ["@solidjs/start > @jridgewell/trace-mapping"] : [], + }, build: { write: true, manifest: true, diff --git a/packages/start/src/shared/dev-toolbar/index.tsx b/packages/start/src/shared/dev-toolbar/index.tsx index e92610c94..9466b5041 100644 --- a/packages/start/src/shared/dev-toolbar/index.tsx +++ b/packages/start/src/shared/dev-toolbar/index.tsx @@ -3,12 +3,13 @@ import { createSignal, ErrorBoundary, onCleanup, + onMount, resetErrorBoundaries, Show, type JSX, } from "solid-js"; import { createStore } from "solid-js/store"; -import { Portal } from "solid-js/web"; +import { isServer, render } from "solid-js/web"; import { Toolbar } from "terracotta"; import info from "../../../package.json" with { type: "json" }; import clientOnly from "../clientOnly.ts"; @@ -208,38 +209,73 @@ export function DevToolbar(props: DevToolbarProps) { ); }); + const ToolbarUI = () => ( +
+ +
+ toggleContent("err")} disabled={errors().length === 0}> + + + toggleContent("fn")}> + + +
+
+ +
+ + {info.version as string} + +
+
+
+ + { + setStore("instances", value, undefined); + }} + /> +
+ ); + + // The toolbar must stay out of the hydrated app tree. A here hands + // the hydrating parent a client-created marker node that hydration never + // inserts into the DOM, and that phantom entry in insert()'s bookkeeping + // corrupts the first reconcile after a navigation (the previous page's DOM + // is left behind). So it gets its own render root — but that root must not + // be created while the SSR stream is still open either, or hydration of the + // pending suspense chunks breaks. The document stays in "loading" readyState + // for the whole stream, so DOMContentLoaded is the stream-end signal. + if (!isServer) { + onMount(() => { + let dispose: (() => void) | undefined; + let container: HTMLElement | undefined; + let timer: ReturnType | undefined; + const scheduleMount = () => { + timer = setTimeout(() => { + container = document.createElement("div"); + document.body.appendChild(container); + dispose = render(ToolbarUI, container); + }, 0); + }; + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", scheduleMount, { once: true }); + } else { + scheduleMount(); + } + onCleanup(() => { + document.removeEventListener("DOMContentLoaded", scheduleMount); + clearTimeout(timer); + dispose?.(); + container?.remove(); + }); + }); + } + return ( <> - -
- -
- toggleContent("err")} disabled={errors().length === 0}> - - - toggleContent("fn")}> - - -
-
- -
- - {info.version as string} - -
-
-
- - { - setStore("instances", value, undefined); - }} - /> -
-
{ pushError(error);