Skip to content

Start manifest capture fails when a shared Vite client environment is built multiple times #7912

Description

@LeonMueller-OneAndOnly

Which project does this relate to?

Start

Describe the bug

tanstack-start:start-manifest-capture-client-build assumes every build of the Vite environment named client is the TanStack Start client build.

When TanStack Start is embedded under Astro using the Vite builder API, both frameworks use the environment name client. The composed build intentionally builds that environment twice:

  1. TanStack Start client build with the single named index entry.
  2. Astro client build with its island and renderer entries.

The first build is captured correctly. The second build invokes the same stateful generateBundle hook and fails while normalizing the legitimate Astro multi-entry bundle:

multiple entries detected: _astro/Header.seLcSLqc.js _astro/client.B8ptpwhy.js
  at normalizeViteClientBuild (.../@tanstack/start-plugin-core/dist/esm/vite/start-manifest-plugin/normalized-client-build.js:35:34)

This is related to #7032, but it is not the same case. That issue has multiple entries injected into one Start client bundle. Here there are two separate builds of a shared Vite environment, and the second build does not contain the Start client entry at all.

The failure appears in @tanstack/start-plugin-core@1.171.24. The older 1.132.51 capture timing happened not to normalize the later Astro bundle.

Reproduction outline

Configure Astro 7 and React Start in one Vite builder, preserving both buildApp hooks. Build the shared client environment before Astro so the Start manifest is available to the subsequent Start server build, then run the Astro build followed by the TanStack build:

async buildApp(builder: ViteBuilder) {
  const clientEnvironment = builder.environments.client
  if (clientEnvironment && !clientEnvironment.isBuilt) {
    await builder.build(clientEnvironment)
  }

  await astroBuildApp(builder)
  await tanstackBuildApp(builder)
}

Add any hydrated Astro React island. The later Astro client build then has at least the island entry and @astrojs/react/client.js, causing the Start capture hook to reject it.

Verified workaround

Wrapping tanstack-start:start-manifest-capture-client-build so its original generateBundle handler runs only for the bundle containing the Start named index entry fixes the build while preserving both Start and Astro assets:

const isTanStackClientBuild = Object.values(bundle).some(
  (entry) => entry.type === "chunk" && entry.isEntry && entry.name === "index",
)

if (!isTanStackClientBuild) return
return originalGenerateBundle.call(this, options, bundle, isWrite)

With that guard, typechecking, tests, and the production Astro server build all succeed.

Expected behavior

The manifest capture plugin should identify the build containing the configured Start client entry, preferably using its resolved entry path and facadeModuleId, and ignore client-environment builds that do not contain that entry.

Alternatively, Start could expose configurable environment names or a supported composition hook so embedded frameworks do not need to wrap an internal plugin by name.

Platform

  • astro: 7.0.0
  • @astrojs/react: 6.0.0
  • @tanstack/react-start: 1.168.32
  • @tanstack/start-plugin-core: 1.171.24
  • @tanstack/react-router: 1.170.18
  • vite: 8.0.16
  • Node.js: 24.13.1
  • Package manager: pnpm 10.33.2

Additional context

This is a custom Astro/TanStack Start embedding rather than an officially documented integration. The underlying concern is generic Vite Environment API composability: environment names are not exclusive, and a stateful plugin can observe more than one build of the same environment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions