Skip to content

WIP: fix(router-devtools-core): ship SSR-safe build to avoid window crash#7207

Open
brenelz wants to merge 1 commit intoTanStack:mainfrom
brenelz:devtools-core-fix
Open

WIP: fix(router-devtools-core): ship SSR-safe build to avoid window crash#7207
brenelz wants to merge 1 commit intoTanStack:mainfrom
brenelz:devtools-core-fix

Conversation

@brenelz
Copy link
Copy Markdown
Contributor

@brenelz brenelz commented Apr 16, 2026

vite-plugin-solid's DOM JSX transform emits module-scope delegateEvents() and template() calls that read window.document at import time, crashing SSR consumers (nitro, Node) as soon as the pre-bundled chunks are loaded.

Produce a second build with solid({ ssr: true }) + build.ssr, emitted to dist/esm/index.server.js, and expose it via the 'node' export condition so server bundlers pick it up automatically.

Closes #7205

Summary by CodeRabbit

  • Chores
    • Enhanced build configuration to support server-side rendering (SSR) with separate output artifacts.
    • Improved package exports with explicit condition mappings for different runtime environments (browser, worker, and Node.js).

vite-plugin-solid's DOM JSX transform emits module-scope delegateEvents()
and template() calls that read window.document at import time, crashing
SSR consumers (nitro, Node) as soon as the pre-bundled chunks are loaded.

Produce a second build with solid({ ssr: true }) + build.ssr, emitted to
dist/esm/index.server.js, and expose it via the 'node' export condition
so server bundlers pick it up automatically.

Closes TanStack#7205
@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented Apr 16, 2026

View your CI Pipeline Execution ↗ for commit 18809d9

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 5m 6s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 43s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-16 22:31:00 UTC

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

The package now supports SSR-safe builds by introducing a separate server entry point and environment-specific export conditions. The build process generates both client and server outputs, with package.json exports directing consumers to the appropriate bundle based on their environment.

Changes

Cohort / File(s) Summary
Export Conditions Configuration
packages/router-devtools-core/package.json
Added explicit exports["."].import condition mappings for worker, browser (pointing to ./dist/esm/index.js) and node (pointing to ./dist/esm/index.server.js), enabling environment-specific entry point resolution.
SSR Build Configuration
packages/router-devtools-core/vite.config.ts
Introduced conditional SSR build via BUILD_SSR environment variable. When enabled, configures vite-plugin-solid for SSR transforms, skips CJS output, disables output directory clearing, restricts formats to ESM, enables Vite SSR bundling, bundles Solid as external-free, sets output filenames to esm/index.server.js, and excludes DTS generation to rely on client build declarations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Two bundles now, both shiny and bright,
One for the browser, one for the night,
The server no longer says "window? What's that?"
With separate builds, we've solved this problem, stat! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: shipping an SSR-safe build to prevent window crashes. It directly relates to the core objective of addressing the SSR compatibility issue.
Linked Issues check ✅ Passed The PR implements option 3 from issue #7205: providing a separate server-safe entry point via export conditions. The SSR build configuration with node export mapping directly addresses the requirement to avoid window crashes in SSR environments.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing the SSR safety issue: build script modifications, export condition mappings, and Vite SSR configuration. No unrelated changes detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 3485a880478e
  • Measured at: 2026-04-16T22:26:59.172Z
  • Baseline source: history:22dc15203379
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 87.35 KiB 0 B (0.00%) 274.60 KiB 75.97 KiB ▁▁▁▁▁██████
react-router.full 90.63 KiB 0 B (0.00%) 285.74 KiB 78.87 KiB ▁▁▁▁▁▂▂████
solid-router.minimal 35.55 KiB 0 B (0.00%) 106.71 KiB 31.96 KiB ▁▁▁▁▁▂▂▂▂▆█
solid-router.full 40.02 KiB 0 B (0.00%) 120.20 KiB 35.94 KiB ▁▁▁▁▁▂▂▂▂▇█
vue-router.minimal 53.30 KiB 0 B (0.00%) 152.01 KiB 47.88 KiB ▁▁▁▁▁██████
vue-router.full 58.20 KiB 0 B (0.00%) 167.43 KiB 52.06 KiB ▁▁▁▁▁██████
react-start.minimal 101.77 KiB 0 B (0.00%) 322.39 KiB 88.05 KiB ▁▁▁▁▁▃▃████
react-start.full 105.21 KiB 0 B (0.00%) 332.72 KiB 90.89 KiB ▁▁▁▁▁▃▃████
solid-start.minimal 49.53 KiB 0 B (0.00%) 152.52 KiB 43.68 KiB ▁▁▁▁▁▄▄▄▄█▇
solid-start.full 55.07 KiB 0 B (0.00%) 168.73 KiB 48.43 KiB ▁▁▁▁▁▂▂▂▂▅█

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 16, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7207

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7207

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7207

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7207

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7207

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7207

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7207

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7207

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7207

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7207

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7207

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7207

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7207

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7207

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7207

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7207

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7207

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7207

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7207

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7207

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7207

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7207

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7207

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7207

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7207

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7207

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7207

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7207

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7207

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7207

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7207

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7207

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7207

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7207

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7207

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7207

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7207

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7207

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7207

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7207

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7207

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7207

commit: 18809d9

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/router-devtools-core/package.json (1)

44-55: Consider a server-safe fallback for non-Node SSR runtimes.

The default condition falls back to the client build, so SSR runtimes that don't assert the node condition (e.g., Deno by default, some edge/SSR pipelines) will still load index.js and hit the original window crash. If Deno/edge SSR support matters for devtools, either add a deno/workerd mapping to index.server.js, or swap the fallback so unknown environments prefer the safer server bundle.

💡 Example (explicit Deno mapping)
       "import": {
         "types": "./dist/esm/index.d.ts",
         "worker": "./dist/esm/index.js",
         "browser": "./dist/esm/index.js",
+        "deno": "./dist/esm/index.server.js",
         "node": "./dist/esm/index.server.js",
         "default": "./dist/esm/index.js"
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-devtools-core/package.json` around lines 44 - 55, The
package.json exports currently fall back to the client bundle via the "default"
condition which causes non-Node SSR runtimes to load index.js and crash; update
the exports so unknown environments prefer the server-safe bundle
(index.server.js) or add explicit SSR runtime conditions (e.g., "deno" or
"workerd") that map to ./dist/esm/index.server.js; modify the "." export object
(keys "default", "node", "browser", "import") to either change "default" to
./dist/esm/index.server.js or add new "deno"/"workerd" conditions pointing to
./dist/esm/index.server.js while keeping "browser" -> index.js for client
builds.
packages/router-devtools-core/vite.config.ts (1)

60-62: Avoid any on the plugin predicate.

Typing p as any disables the strict type-safety the repo targets. Vite's Plugin/PluginOption type already covers falsy entries and has a name field, so an explicit import keeps this safe without losing readability.

🧹 Suggested change
-import solid from 'vite-plugin-solid'
+import solid from 'vite-plugin-solid'
+import type { PluginOption } from 'vite'
@@
-  merged.plugins = merged.plugins.filter(
-    (p: any) => !p || p.name !== 'vite:dts',
-  )
+  merged.plugins = (merged.plugins as Array<PluginOption>).filter(
+    (p) => !p || typeof p !== 'object' || !('name' in p) || p.name !== 'vite:dts',
+  )

As per coding guidelines: "Use TypeScript strict mode with extensive type safety".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-devtools-core/vite.config.ts` around lines 60 - 62, The
filter currently types the parameter as any; change it to use Vite's Plugin (or
PluginOption) types instead and add a proper nullable type guard so the
predicate is type-safe; import { Plugin } from 'vite' and update the predicate
on merged.plugins.filter to accept p as Plugin | null | undefined (or use a type
guard like p is Plugin) and check p.name !== 'vite:dts' while accounting for
falsy p, targeting the existing merged.plugins filter expression and the
'vite:dts' plugin name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/router-devtools-core/vite.config.ts`:
- Around line 43-57: The SSR build is writing un-hashed chunks into the same
dist/esm/ folder and can overwrite client chunks; update the Vite SSR output
config (merged.build.ssr and merged.build.rolldownOptions.output) so SSR
artifacts are written to a distinct namespace or include a hash: change
merged.build.rolldownOptions.output.chunkFileNames and entryFileNames to use
either an SSR subdirectory (e.g., 'esm-ssr/[name].js') or include a content/hash
token (e.g., 'esm/[name].[hash].js') so SSR chunks cannot collide with client
chunks; ensure both merged.build.rolldownOptions.output.entryFileNames and
chunkFileNames are updated consistently.

---

Nitpick comments:
In `@packages/router-devtools-core/package.json`:
- Around line 44-55: The package.json exports currently fall back to the client
bundle via the "default" condition which causes non-Node SSR runtimes to load
index.js and crash; update the exports so unknown environments prefer the
server-safe bundle (index.server.js) or add explicit SSR runtime conditions
(e.g., "deno" or "workerd") that map to ./dist/esm/index.server.js; modify the
"." export object (keys "default", "node", "browser", "import") to either change
"default" to ./dist/esm/index.server.js or add new "deno"/"workerd" conditions
pointing to ./dist/esm/index.server.js while keeping "browser" -> index.js for
client builds.

In `@packages/router-devtools-core/vite.config.ts`:
- Around line 60-62: The filter currently types the parameter as any; change it
to use Vite's Plugin (or PluginOption) types instead and add a proper nullable
type guard so the predicate is type-safe; import { Plugin } from 'vite' and
update the predicate on merged.plugins.filter to accept p as Plugin | null |
undefined (or use a type guard like p is Plugin) and check p.name !== 'vite:dts'
while accounting for falsy p, targeting the existing merged.plugins filter
expression and the 'vite:dts' plugin name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40b19a03-ad75-4154-b038-b4c216e02563

📥 Commits

Reviewing files that changed from the base of the PR and between 3485a88 and 18809d9.

📒 Files selected for processing (2)
  • packages/router-devtools-core/package.json
  • packages/router-devtools-core/vite.config.ts

Comment on lines +43 to +57
if (ssr) {
// Don't wipe the client build produced in the previous step.
merged.build.emptyOutDir = false
merged.build.lib.formats = ['es']
// Build in SSR mode so Vite resolves `solid-js/web` via the `node` export
// condition and pulls in Solid's server build.
merged.build.ssr = true
merged.ssr = {
// Still bundle Solid so the server artifact is self-contained.
noExternal: ['solid-js', 'solid-js/web'],
}
// `lib.fileName` isn't honored when `build.ssr` is set, so drive the
// output names via rolldown directly.
merged.build.rolldownOptions.output.entryFileNames = 'esm/index.server.js'
merged.build.rolldownOptions.output.chunkFileNames = 'esm/[name].js'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

SSR chunks can overwrite client chunks in dist/esm/.

With emptyOutDir = false the SSR step writes into the same dist/esm/ directory as the client build, and chunkFileNames = 'esm/[name].js' has no hash and no SSR-specific namespace. Today it happens to produce a single bundle (single entry, noExternal for Solid, no manualChunks), but any future dynamic-import or code-split would silently overwrite same-named client chunks with SSR-compiled content — causing "window is not defined" at runtime on the browser side.

Prefer either a hashed name or an SSR-scoped subdirectory for chunks.

🛡️ Defensive fix
   merged.build.rolldownOptions.output.entryFileNames = 'esm/index.server.js'
-  merged.build.rolldownOptions.output.chunkFileNames = 'esm/[name].js'
+  merged.build.rolldownOptions.output.chunkFileNames = 'esm/server/[name]-[hash].js'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (ssr) {
// Don't wipe the client build produced in the previous step.
merged.build.emptyOutDir = false
merged.build.lib.formats = ['es']
// Build in SSR mode so Vite resolves `solid-js/web` via the `node` export
// condition and pulls in Solid's server build.
merged.build.ssr = true
merged.ssr = {
// Still bundle Solid so the server artifact is self-contained.
noExternal: ['solid-js', 'solid-js/web'],
}
// `lib.fileName` isn't honored when `build.ssr` is set, so drive the
// output names via rolldown directly.
merged.build.rolldownOptions.output.entryFileNames = 'esm/index.server.js'
merged.build.rolldownOptions.output.chunkFileNames = 'esm/[name].js'
if (ssr) {
// Don't wipe the client build produced in the previous step.
merged.build.emptyOutDir = false
merged.build.lib.formats = ['es']
// Build in SSR mode so Vite resolves `solid-js/web` via the `node` export
// condition and pulls in Solid's server build.
merged.build.ssr = true
merged.ssr = {
// Still bundle Solid so the server artifact is self-contained.
noExternal: ['solid-js', 'solid-js/web'],
}
// `lib.fileName` isn't honored when `build.ssr` is set, so drive the
// output names via rolldown directly.
merged.build.rolldownOptions.output.entryFileNames = 'esm/index.server.js'
merged.build.rolldownOptions.output.chunkFileNames = 'esm/server/[name]-[hash].js'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-devtools-core/vite.config.ts` around lines 43 - 57, The SSR
build is writing un-hashed chunks into the same dist/esm/ folder and can
overwrite client chunks; update the Vite SSR output config (merged.build.ssr and
merged.build.rolldownOptions.output) so SSR artifacts are written to a distinct
namespace or include a hash: change
merged.build.rolldownOptions.output.chunkFileNames and entryFileNames to use
either an SSR subdirectory (e.g., 'esm-ssr/[name].js') or include a content/hash
token (e.g., 'esm/[name].[hash].js') so SSR chunks cannot collide with client
chunks; ensure both merged.build.rolldownOptions.output.entryFileNames and
chunkFileNames are updated consistently.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 16, 2026

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing brenelz:devtools-core-fix (18809d9) with main (22dc152)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (3485a88) during the generation of this report, so 22dc152 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ReferenceError: window is not defined in @tanstack/router-devtools-core during SSR — delegateEvents() called at module scope

1 participant