Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/router-devtools-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
"test:types:ts60": "tsc",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
"build": "vite build"
"build": "vite build && BUILD_SSR=true vite build"
},
"type": "module",
"types": "./dist/esm/index.d.ts",
Expand All @@ -44,6 +44,9 @@
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"worker": "./dist/esm/index.js",
"browser": "./dist/esm/index.js",
"node": "./dist/esm/index.server.js",
"default": "./dist/esm/index.js"
},
"require": {
Expand Down
45 changes: 44 additions & 1 deletion packages/router-devtools-core/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,26 @@ import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackViteConfig } from '@tanstack/vite-config'
import solid from 'vite-plugin-solid'

// Produce two builds:
// - default (client/DOM): `dist/esm/index.js` + `dist/cjs/index.cjs`
// - SSR (BUILD_SSR=true): `dist/esm/index.server.js`
//
// vite-plugin-solid's default DOM JSX transform emits module-scope
// `delegateEvents([...])` and `template(...)` calls that touch `window` /
// throw "client-only API" when the bundled Solid runtime is its DOM build.
// See https://github.com/TanStack/router/issues/7205.
//
// Neither just externalizing `solid-js/web` nor just swapping conditions is
// enough: Solid's SSR build exports those functions as stubs that throw on
// call, and consumer bundlers (e.g. nitro) eagerly concatenate the lazy
// chunks into the server entry, so the module-scope calls fire at import.
// Shipping a second bundle compiled with `ssr: true` makes vite-plugin-solid
// emit `ssr()` / `ssrElement()` calls instead, which are real SSR primitives.
// The `node` export condition in package.json picks this build automatically.
const ssr = process.env.BUILD_SSR === 'true'

const config = defineConfig({
plugins: [solid()],
plugins: [solid({ ssr })],
})

const merged = mergeConfig(
Expand All @@ -13,10 +31,35 @@ const merged = mergeConfig(
entry: './src/index.tsx',
srcDir: './src',
bundledDeps: ['solid-js', 'solid-js/web'],
// Only emit CJS and declaration files for the client build — the SSR
// build only needs ESM, and the types are identical.
cjs: !ssr,
}),
)

merged.build.rolldownOptions.output.manualChunks = undefined
merged.build.rolldownOptions.output.preserveModules = false

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'
Comment on lines +43 to +57
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.

// Skip declaration-file generation — the client build already produced
// `.d.ts`/`.d.cts` files that are valid for both.
merged.plugins = merged.plugins.filter(
(p: any) => !p || p.name !== 'vite:dts',
)
}

export default merged
Loading