fix: make lazy facade entry reclassification effective under rolldown-vite - #269
Merged
Merged
Conversation
…-vite normalizeEmittedLazyEntries used `delete entry.isEntry` to reclassify emitted lazy facade chunks as dynamic entries. Under rolldown-vite (Vite 8) the bundle passed to generateBundle materializes chunks as proxies over native data: property assignment is trapped and synced back to the native bundle after the hook, but a delete falls through to the throwaway JS target while the proxy's read cache keeps serving the previously read value. The reclassification therefore silently no-oped and downstream plugins still saw every facade chunk flagged isEntry, e.g. TanStack Start failing client builds with "multiple entries detected". Assign `isEntry = false` instead, which behaves identically on plain objects (Rollup bundles and the parsed Vite manifest.json). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: e081f02 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
birkskyum
approved these changes
Jul 15, 2026
Remove comments explaining the entry assignment logic.
brenelz
added a commit
to TanStack/router
that referenced
this pull request
Jul 15, 2026
next.10 ships the fix for the lazy facade entry reclassification under rolldown-vite (solidjs/solid-vite-plugin#269): assigning isEntry = false instead of deleting the property, so the reclassification actually propagates through rolldown's bundle proxies and downstream plugins no longer see every lazy facade chunk as an application entry. This is the proper replacement for the reverted start-plugin-core workaround (see a1ee163) — the strict single-entry assertion now holds again. Verified: e2e/solid-start/basic build + e2e suite (80 passed), solid-router test:unit client (814) + server (2) modes, start-plugin-core unit tests (486). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
birkskyum
pushed a commit
to TanStack/router
that referenced
this pull request
Jul 16, 2026
* chore: upgrade Solid v2 packages to 2.0.0-beta.16 Bumps solid-js, @solidjs/web, and babel-preset-solid from 2.0.0-beta.15 to 2.0.0-beta.16 across packages, examples, e2e apps, benchmarks, and the pnpm overrides in pnpm-workspace.yaml. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: upgrade Solid v2 packages to 2.0.0-beta.17 Bumps solid-js, @solidjs/web, and babel-preset-solid from 2.0.0-beta.16 to 2.0.0-beta.17 across packages, examples, e2e apps, and benchmarks. The scoped babel-preset-solid overrides in pnpm-workspace.yaml are removed instead of bumped: vite-plugin-solid@3.0.0-next.5 and @rsbuild/plugin-solid@2.0.0-beta.0 both declare '>=2.0.0-beta.0 <2.0.0-experimental.0', which resolves to the newest v2 beta on its own, while Solid 1.x consumers keep babel-preset-solid 1.x. A stale override here silently kept the beta.16 compiler (with its broken omitLastClosingTag marker handling) in the build pipeline. The @dom-expressions/* scope is added to minimumReleaseAgeExclude, covering the renamed compiler package (@dom-expressions/babel-plugin-jsx, formerly babel-plugin-jsx-dom-expressions). Two unrelated loose specs are pinned to their locked versions because re-resolving the lockfile pulled broken releases: @platformatic/flame 1.7.0 fails pnpm's trust-downgrade policy, and the nitro-nightly 'latest' tag depends on an unpublished h3 version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(solid-router): suspend on the match loadPromise only during SSR On the client, returning the loadPromise from MatchInner's pending branch keeps a live async source in the tree whenever a match is pending. Since Solid 2.0.0-beta.16, that routes every signal write into a transition hold for the rest of the flush, which breaks router-core's synchronous write-then-load flows: invalidate() on a notFound match never re-ran the loader, and writes to unrelated signals were held too. SSR keeps the suspension so the streaming engine waits for the match to settle; the client now renders just the pending component. Verified on beta.17: without this, "re-runs loaders that throw notFound() when invalidated via HMR filter" and "keeps rendering a route notFoundComponent when loader returns notFound() after invalidate" fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(lockfile): restore all @rspack/binding@2.0.8 platform binaries The lockfile entry for @rspack/binding@2.0.8 only listed the darwin-arm64 binding, so linux CI runners had no matching native binding and rspack fell back to a hoisted 2.0.0 binary, crashing the bundle-size benchmark with a core/binding version mismatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: add changesets for solid-js beta.17 upgrade and SSR-only match suspension Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(examples): replace broken nitro-nightly pin with nitro beta in start-rscs The pinned nitro-nightly@4.0.0-20251010 self-imports 'nitro/runtime/meta', which under pnpm's isolated layout resolves to the hoisted nitro 3.x beta and crashes vite config loading. Mirrors upstream fix e65f0c8 (#7809). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: upgrade vite-plugin-solid to next.7 * fix(solid-router): retain manifest stylesheets across navigation * chore: upgrade Solid v2 packages to 2.0.0-beta.18 and vite-plugin-solid to next.8 Bumps solid-js, @solidjs/web, and babel-preset-solid from 2.0.0-beta.17 to 2.0.0-beta.18 and vite-plugin-solid from 3.0.0-next.7 to 3.0.0-next.8 (which pins @dom-expressions/jsx-compiler to 0.50.0-next.21) across packages, examples, e2e apps, and benchmarks. vite-plugin-solid is added to minimumReleaseAgeExclude: Solid publishes plugin prereleases in lockstep with the runtime betas, often minutes before we bump, and pnpm's 24h release-age gate otherwise filters the new version with a misleading NO_MATCHING_VERSION error. Verified: solid-router unit tests (client + server modes), solid-start basic e2e (80 passed), solid-router basic-file-based e2e (120 passed), and all 17 bundle-size benchmark scenarios build cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(start-plugin-core): select client entry chunk by facade module instead of asserting a single entry vite-plugin-solid >= 3.0.0-next.6 emits a facade chunk for every dynamically imported project module during SSR client builds (to keep its own manifest lookups stable), and emitted chunks are flagged `isEntry` just like the configured entry. The start manifest capture plugin asserted the client bundle contains exactly one entry chunk, so every solid-start app failed to build with "multiple entries detected". The capture plugin now receives the resolved client entry path and picks the entry chunk whose facadeModuleId matches it; other entry-flagged chunks are treated as ordinary chunks. When no chunk matches, a single entry is still accepted as before, and the multiple-entries error now lists every entry chunk with its facade module to ease diagnosis. Verified with fresh package builds: previously failing memory-server solid benchmark targets, all 17 bundle-size scenarios, solid-start basic build + e2e (80 passed), react-start basic build, and the start-plugin-core unit suite (494 tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: apply automated fixes * test(start-plugin-core): pass getClientEntry to startManifestPlugin in dev manifest test Fixes the type tests after startManifestPlugin gained a required getClientEntry option. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Revert "fix(start-plugin-core): select client entry chunk by facade module instead of asserting a single entry" This reverts commit d0f4926. * chore: upgrade vite-plugin-solid to 3.0.0-next.10 next.10 ships the fix for the lazy facade entry reclassification under rolldown-vite (solidjs/solid-vite-plugin#269): assigning isEntry = false instead of deleting the property, so the reclassification actually propagates through rolldown's bundle proxies and downstream plugins no longer see every lazy facade chunk as an application entry. This is the proper replacement for the reverted start-plugin-core workaround (see a1ee163) — the strict single-entry assertion now holds again. Verified: e2e/solid-start/basic build + e2e suite (80 passed), solid-router test:unit client (814) + server (2) modes, start-plugin-core unit tests (486). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: upgrade solid-js/@solidjs/web to 2.0.0-beta.19 and vite-plugin-solid to 3.0.0-next.11 vite-plugin-solid 3.0.0-next.11 peer-requires solid-js and @solidjs/web >=2.0.0-beta.19, so the Solid v2 packages are bumped in lockstep across all workspace pins (packages, examples, e2e, benchmarks). Verified: e2e/solid-start/basic build succeeds — no "multiple entries detected" error from start-manifest-capture-client-build; solid-router test:unit client (814 passed) + server (2 passed) modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: upgrade solid-js/@solidjs/web to 2.0.0-beta.19 and vite-plugin-solid to 3.0.0-next.11 [Self-Healing CI Rerun] * chore: upgrade solid-js/@solidjs/web to 2.0.0-beta.19 and vite-plugin-solid to 3.0.0-next.11 [Self-Healing CI Rerun] * fix(e2e): make prerender builds hermetic to deflake build:vite:prerender The prerender crawl runs inside `vite build` and SSRs /posts and every crawled /posts/$postId. The `import.meta.env.VITE_NODE_ENV === 'test'` switch to the local dummy server was inlined away at build time because Playwright only sets that env for the test-time webServer, never for the nx build target — so build-time SSR fetched the real jsonplaceholder.typicode.com from CI. When that public API stalled, the page never sent headers and the build died after undici's 5-minute headers timeout (UND_ERR_HEADERS_TIMEOUT). Fix, applied to the react/solid/vue basic apps alike: - Resolve the posts/users API base URL at request time from process.env (same pattern users.tsx already used) instead of build-time inlining, so the built app honors whatever dummy server the harness is running. - Add e2eStartDummyServerForBuild() to @tanstack/router-e2e-utils and call it from the apps' vite configs in prerender mode: it starts the dummy server on a free port for the lifetime of the build, exposes it via VITE_NODE_ENV/VITE_EXTERNAL_PORT, and unrefs everything so the build process can exit. Verified: nx run tanstack-solid-start-e2e-basic:build:vite:prerender passes (47 pages prerendered, dummy server used, tsc clean), prerendered posts/index.html contains the expected content, and the server bundle now keeps the runtime env check instead of a hardcoded external URL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: apply automated fixes * chore: upgrade solid-js/@solidjs/web to 2.0.0-beta.19 and vite-plugin-solid to 3.0.0-next.11 [Self-Healing CI Rerun] * fix(e2e): harden prerender-build dummy server and log fetch targets CI still hit UND_ERR_HEADERS_TIMEOUT during build:vite:prerender after de2132a. The failure pattern (fetch logs print instantly, then a 5-minute stall) means the posts fetches still reached the real external API in that run: the dummy server resolves in milliseconds in-process, and a non-listening port would fail instantly with ECONNREFUSED. One way that can happen: e2eStartDummyServerForBuild() early-returned whenever VITE_EXTERNAL_PORT was already present in the environment, skipping both the server start AND setting VITE_NODE_ENV — leaving the runtime URL check pointing at jsonplaceholder. Guard on a globalThis flag instead and always set both env vars. Also, to make the next failure diagnosable and survivable: - fetchPost/fetchPosts (react/solid/vue basic) now log the base URL they fetch from, so CI output shows whether the dummy server was used. - prerender configs set retryCount: 2 / retryDelay: 1000 so a transient hang retries instead of failing the whole build. Verified: nx run tanstack-solid-start-e2e-basic:build:vite:prerender passes with "Fetching posts from http://localhost:<port>..." in the log and 47 pages prerendered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
normalizeEmittedLazyEntriesreclassifies the emitted lazy facade chunks (isEntry→isDynamicEntry) ingenerateBundleso that downstream plugins don't mistake them for application entries. It does this withdelete entry.isEntry— which works on Rollup's plain bundle objects, but silently no-ops under rolldown-vite (Vite 8).Rolldown materializes each chunk in the
generateBundlebundle as a proxy over native data: property assignment is trapped, cached, and synced back to the native bundle after the hook returns, but a delete is not trapped at the chunk level — it falls through to the throwaway JS target while the proxy's read cache keeps serving the previously readisEntry: true. The mutation never reaches the native bundle, so it isn't even visible later in the same hook, let alone to downstream plugins.Observed impact: every TanStack Start + Solid app on Vite 8 fails its client build with
multiple entries detected— TanStack Start's manifest plugin runs after this plugin'sgenerateBundle, sees all lazy facade chunks still flaggedisEntry, and can't identify the real client entry. Instrumenting the current code during such a build showsreclassified=63yet 64 entry-flagged chunks remain afterwards.Fix
Assign
entry.isEntry = falseinstead of deleting the property. On plain objects (Rollup bundles, and the parsedmanifest.jsonthis helper also normalizes) the behavior is identical for every truthiness check; under rolldown the assignment propagates correctly.Verification
isEntry; reverting just this line reproduces themultiple entries detectedfailure.pnpm build(rollup + tsc) passes.🤖 Generated with Claude Code