Summary
In dev mode, server-rendering any lazy() component inside a <Loading> boundary serializes a garbage module URL into the hydration payload:
_$HY.r["30_assets"]=($R[0]={"src/routes/Playground.tsx":"[object Object]/src/routes/Playground.tsx"});
On the client, loadModuleAssets tries to import() that specifier, which rejects:
Uncaught (in promise) TypeError: Failed to resolve module specifier '[object Object]/src/routes/Playground.tsx'
at Object.loadModuleAssets (dev.js:461:38)
The rejection is unhandled and boundary hydration never resumes. The page looks fully rendered but is permanently dead
Reproduction
https://stackblitz.com/edit/solidjs-templates-rq2kuqlp?file=src%2FApp.tsx,src%2Fentry-server.tsx
Any dev-mode SSR app where a lazy() component actually renders during SSR:
// App.tsx
const Page = lazy(() => import('./Page'));
export default function App() {
return (
<Loading fallback={<p>Loading...</p>}>
<Page />
</Loading>
);
}
// entry-server.tsx
import manifest from 'virtual:solid-manifest';
export const render = () => renderToStream(() => <App />, { manifest });
Root cause — three layers
Layer 1: vite-plugin-solid dev manifest answers _base with an object
DEV_MANIFEST_CODE is a Proxy that answers every string key:
// vite-plugin-solid, DEV_MANIFEST_CODE
export default new Proxy({}, {
get(_, key) {
if (typeof key !== "string") return undefined;
return { file: "/" + key };
}
});
The server runtime reads a reserved key from the same manifest:
// @dom-expressions/runtime src/server.js (resolveAssets)
const base = manifest._base || "/";
...
js.push(base + e.file);
Layer 2: even with _base fixed, the dev proxy's leading slash breaks the URL
If the proxy is patched to return undefined for _base, base becomes "/" and the result is:
"/" + "/src/routes/Playground.tsx" → "//src/routes/Playground.tsx"
Layer 3: a failed module-asset import hangs hydration forever, silently
// @dom-expressions/runtime src/client.js (loadModuleAssets)
hy.loading[moduleUrl] = import(entryUrl).then(mod => {
hy.modules[moduleUrl] = mod;
});
No .catch. The rejected promise is returned into the boundary-resume path (scheduleResumeAfterAssets), which never resumes, and the rejection surfaces only as an unhandled-rejection console entry. Hydration stays incomplete indefinitely:
Even with layers 1–2 fixed, any transient network failure loading a chunk during hydration reproduces this hang, so layer 3 is worth fixing independently: fail the boundary loudly (route the error to the nearest <Errored> / emit a diagnostic) rather than waiting forever.
Summary
In dev mode, server-rendering any
lazy()component inside a<Loading>boundary serializes a garbage module URL into the hydration payload:On the client,
loadModuleAssetstries toimport()that specifier, which rejects:The rejection is unhandled and boundary hydration never resumes. The page looks fully rendered but is permanently dead
Reproduction
https://stackblitz.com/edit/solidjs-templates-rq2kuqlp?file=src%2FApp.tsx,src%2Fentry-server.tsx
Any dev-mode SSR app where a
lazy()component actually renders during SSR:Root cause — three layers
Layer 1:
vite-plugin-soliddev manifest answers_basewith an objectDEV_MANIFEST_CODEis a Proxy that answers every string key:The server runtime reads a reserved key from the same manifest:
Layer 2: even with
_basefixed, the dev proxy's leading slash breaks the URLIf the proxy is patched to return
undefinedfor_base,basebecomes"/"and the result is:Layer 3: a failed module-asset import hangs hydration forever, silently
No
.catch. The rejected promise is returned into the boundary-resume path (scheduleResumeAfterAssets), which never resumes, and the rejection surfaces only as an unhandled-rejection console entry. Hydration stays incomplete indefinitely:Even with layers 1–2 fixed, any transient network failure loading a chunk during hydration reproduces this hang, so layer 3 is worth fixing independently: fail the boundary loudly (route the error to the nearest
<Errored>/ emit a diagnostic) rather than waiting forever.