Skip to content

2.0.0-beta.15: Dev SSR of a lazy() component produces [object Object]/... module URLs #2817

Description

@brenelz

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions