Summary
ripple.config.ts gets bundled into the client build because the generated hydration entry (create_client_entry_source in @ripple-ts/vite-plugin) does import rippleConfig from '<configPath>' directly. Since a typical ripple.config.ts imports defineConfig from @ripple-ts/vite-plugin, the whole plugin module ends up in the client bundle too — including its module-level const IS_WINDOWS = process.platform === 'win32'; (packages/vite-plugin/src/index.js:48).
In the browser this throws ReferenceError: process is not defined at the top level of the client entry chunk. Because it's a top-level throw (not inside the try/catch that wraps hydrate()/mount()), it aborts the entire client bootstrap script before hydrate() ever runs. The result: SSR output looks correct, no console error about hydration mismatch, but every interactive component on the page is inert — click handlers never get attached, and there's no error surfaced that points at the real cause.
Reproduction
create-ripple app (or any app) using @ripple-ts/vite-plugin's RenderRoute, with ripple.config.ts importing defineConfig from @ripple-ts/vite-plugin.
- Add any component with a click handler (e.g. a toggle/switch) to a render route page.
vite build for the client, then serve the built output (node dist/server/entry.js with the adapter-node/bun/vercel entry, or preview).
- Open the page in a browser and click the interactive element.
Observed: nothing happens. DevTools console shows:
ReferenceError: process is not defined
at .../assets/main-<hash>.js:<line>
pointing into the bundled @ripple-ts/vite-plugin code, not into any application code.
This reproduces identically in dev mode and in a production build — it is not an optimizeDeps/dep-scan artifact. (There's a related, separate dev-only noise issue where @tsrx/core's vite/dep-scan.js — which imports node:fs/promises — gets pulled into the client dep-scan; that one only affects the dev console and does not break interactivity, so it's not the same bug, just easy to conflate since both surface as "Node module in browser" errors.)
Root cause
packages/vite-plugin/src/index.js:48:
const IS_WINDOWS = process.platform === 'win32';
This is evaluated eagerly at module load, with no guard. IS_WINDOWS is only read inside createVirtualImportId (line ~132), a server-only helper for building Vite virtual module ids from Windows-style /@fs/C:/... paths — it has no reason to exist in a client bundle at all, but it still gets executed because the whole module is imported.
Suggested minimal fix
Guard the top-level access so an accidental client-side import doesn't crash the whole script:
const IS_WINDOWS = typeof process !== 'undefined' && process.platform === 'win32';
This alone would prevent the hard crash, though the deeper issue is that create_client_entry_source importing the whole ripple.config.ts (transitively pulling in @ripple-ts/vite-plugin, and whatever else the config file imports) into the client graph seems unintentional — SERVER_ONLY_ADAPTER_IDS already stubs out @ripple-ts/adapter-node/-bun/-vercel for exactly this reason, but @ripple-ts/vite-plugin itself isn't in that list. A more complete fix might extract only the route table from the config at build time rather than importing the live config module client-side, but I wanted to report the observable symptom first since it's a pretty severe one (silently breaks all interactivity with no actionable error).
Environment
ripple / @ripple-ts/vite-plugin / @ripple-ts/adapter-node: 0.3.119
- Reproduced via SSR render routes (
RenderRoute + Node adapter), both vite dev and production build (vite build + node dist/server/entry.js)
Summary
ripple.config.tsgets bundled into the client build because the generated hydration entry (create_client_entry_sourcein@ripple-ts/vite-plugin) doesimport rippleConfig from '<configPath>'directly. Since a typicalripple.config.tsimportsdefineConfigfrom@ripple-ts/vite-plugin, the whole plugin module ends up in the client bundle too — including its module-levelconst IS_WINDOWS = process.platform === 'win32';(packages/vite-plugin/src/index.js:48).In the browser this throws
ReferenceError: process is not definedat the top level of the client entry chunk. Because it's a top-level throw (not inside thetry/catchthat wrapshydrate()/mount()), it aborts the entire client bootstrap script beforehydrate()ever runs. The result: SSR output looks correct, no console error about hydration mismatch, but every interactive component on the page is inert — click handlers never get attached, and there's no error surfaced that points at the real cause.Reproduction
create-rippleapp (or any app) using@ripple-ts/vite-plugin'sRenderRoute, withripple.config.tsimportingdefineConfigfrom@ripple-ts/vite-plugin.vite buildfor the client, then serve the built output (node dist/server/entry.jswith the adapter-node/bun/vercel entry, or preview).Observed: nothing happens. DevTools console shows:
pointing into the bundled
@ripple-ts/vite-plugincode, not into any application code.This reproduces identically in dev mode and in a production build — it is not an
optimizeDeps/dep-scan artifact. (There's a related, separate dev-only noise issue where@tsrx/core'svite/dep-scan.js— which importsnode:fs/promises— gets pulled into the client dep-scan; that one only affects the dev console and does not break interactivity, so it's not the same bug, just easy to conflate since both surface as "Node module in browser" errors.)Root cause
packages/vite-plugin/src/index.js:48:This is evaluated eagerly at module load, with no guard.
IS_WINDOWSis only read insidecreateVirtualImportId(line ~132), a server-only helper for building Vite virtual module ids from Windows-style/@fs/C:/...paths — it has no reason to exist in a client bundle at all, but it still gets executed because the whole module is imported.Suggested minimal fix
Guard the top-level access so an accidental client-side import doesn't crash the whole script:
This alone would prevent the hard crash, though the deeper issue is that
create_client_entry_sourceimporting the wholeripple.config.ts(transitively pulling in@ripple-ts/vite-plugin, and whatever else the config file imports) into the client graph seems unintentional —SERVER_ONLY_ADAPTER_IDSalready stubs out@ripple-ts/adapter-node/-bun/-vercelfor exactly this reason, but@ripple-ts/vite-pluginitself isn't in that list. A more complete fix might extract only the route table from the config at build time rather than importing the live config module client-side, but I wanted to report the observable symptom first since it's a pretty severe one (silently breaks all interactivity with no actionable error).Environment
ripple/@ripple-ts/vite-plugin/@ripple-ts/adapter-node: 0.3.119RenderRoute+ Node adapter), bothvite devand production build (vite build+node dist/server/entry.js)