diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..dbb2ecb --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [["@module-federation/tanstack"]], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1ce0e02 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + name: Verify (Node ${{ matrix.node }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: ["22.18.0", "24", "26"] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ matrix.node }} + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm format:check + - run: pnpm typecheck + - run: pnpm build + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm test + - run: pnpm pack:tanstack diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml new file mode 100644 index 0000000..0e0a314 --- /dev/null +++ b/.github/workflows/pkg-pr-new.yml @@ -0,0 +1,22 @@ +name: pkg-pr-new + +on: + push: + branches: [main] + pull_request: + +permissions: {} + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build:package + - run: pnpm dlx pkg-pr-new@0.0.54 publish ./packages/tanstack diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..c0e9964 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,21 @@ +name: Validate PR title + +on: + pull_request: + branches: [main] + +permissions: + pull-requests: read + +jobs: + semantic-pr: + runs-on: ubuntu-latest + steps: + - name: Accept generated release title + if: ${{ startsWith(github.event.pull_request.title, 'Release ') }} + run: echo "Generated release PR title accepted." + + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + if: ${{ !startsWith(github.event.pull_request.title, 'Release ') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-pull-request.yml b/.github/workflows/release-pull-request.yml new file mode 100644 index 0000000..a48475c --- /dev/null +++ b/.github/workflows/release-pull-request.yml @@ -0,0 +1,44 @@ +name: Release Pull Request + +on: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: release-pull-request + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Validate release request + env: + REPO_SCOPED_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }} + run: | + test "$GITHUB_REF" = "refs/heads/main" || { echo "Release PRs are main-only."; exit 1; } + test -n "$REPO_SCOPED_TOKEN" || { echo "REPO_SCOPED_TOKEN is required."; exit 1; } + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 10 + token: ${{ secrets.REPO_SCOPED_TOKEN }} + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile --ignore-scripts + - uses: module-federation/actions@842df3a7740093a5ff44de2b9a890106046b8e32 # v2 + with: + version: latest + versionNumber: auto + type: pull request + tools: changeset + coreNpmName: "@module-federation/tanstack" + env: + GITHUB_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }} + REPOSITORY: ${{ github.repository }} + REF: ${{ github.ref }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..09a12cc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,99 @@ +name: Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + type: choice + required: true + default: next + options: [latest, next] + branch: + required: true + default: main + +permissions: + contents: read + id-token: write + +concurrency: + group: publish-module-federation-tanstack + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + environment: Publish + steps: + - name: Validate manual target + if: github.event_name == 'workflow_dispatch' + env: + RELEASE_BRANCH: ${{ inputs.branch }} + run: test "$RELEASE_BRANCH" = "main" || { echo "Releases are main-only."; exit 1; } + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + ref: ${{ github.event.release.tag_name || inputs.branch }} + - name: Verify source branch + run: | + git fetch --no-tags origin main:refs/remotes/origin/main + git merge-base --is-ancestor HEAD origin/main || { echo "Source is not on main."; exit 1; } + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: pnpm + registry-url: https://registry.npmjs.org/ + - run: pnpm install --frozen-lockfile + - name: Verify release tag + if: github.event_name == 'release' + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + test "${RELEASE_TAG#v}" = "$RELEASE_TAG" || { echo "Tags must not start with v."; exit 1; } + test "$(node -p "require('./packages/tanstack/package.json').version")" = "$RELEASE_TAG" || { echo "Tag/version mismatch."; exit 1; } + - name: Generate preview version + if: github.event_name == 'workflow_dispatch' && inputs.version == 'next' + run: | + pnpm changeset version --snapshot next + SNAPSHOT_VERSION="$(node -p "require('./packages/tanstack/package.json').version")" + BASE_VERSION="${SNAPSHOT_VERSION%%-*}" + npm pkg set version="${BASE_VERSION}-next.${GITHUB_RUN_ID}" --prefix packages/tanstack + - run: pnpm format:check + - run: pnpm typecheck + - run: pnpm build + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm test + - run: pnpm pack:tanstack + - name: Compute dist-tag + id: dist + env: + INPUT_VERSION: ${{ inputs.version }} + PRERELEASE: ${{ github.event.release.prerelease || false }} + run: | + TAG=latest + if [ "$INPUT_VERSION" = "next" ] || [ "$PRERELEASE" = "true" ]; then TAG=next; fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + - name: Refuse conflicting republish + id: publish-check + env: + PACKAGE: "@module-federation/tanstack" + DIST_TAG: ${{ steps.dist.outputs.tag }} + run: | + VERSION="$(node -p "require('./packages/tanstack/package.json').version")" + EXISTING="$(npm view "$PACKAGE@$VERSION" version --json 2>/dev/null || true)" + EXISTING="${EXISTING#\"}"; EXISTING="${EXISTING%\"}" + if [ "$EXISTING" = "$VERSION" ]; then + CURRENT="$(npm view "$PACKAGE" "dist-tags.$DIST_TAG" --json 2>/dev/null || true)" + CURRENT="${CURRENT#\"}"; CURRENT="${CURRENT%\"}" + test "$CURRENT" = "$VERSION" || { echo "Existing version has conflicting dist-tag."; exit 1; } + echo "publish=false" >> "$GITHUB_OUTPUT" + else + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + - name: Publish to npm + if: steps.publish-check.outputs.publish == 'true' + working-directory: packages/tanstack + run: npm publish --provenance --tag "${{ steps.dist.outputs.tag }}" --access public diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc0b184 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +.turbo/ +.mf/ +@mf-types/ +*.log +pnpm-debug.log* +coverage/ +*.tgz +*.tsbuildinfo +.DS_Store diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..77c3854 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist +node_modules +pnpm-lock.yaml +.turbo +**/routeTree.gen.ts diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..bccee91 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3bad16e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ + + +# This is NOT the Turborepo you know + +Turborepo configuration, task behavior, and CLI commands can vary between installed versions and may differ from your training data. Resolve the `turbo` package from this file's directory or relevant workspace; in monorepos, it may not be visible from the repository root. For example, run `node -p "require.resolve('turbo/package.json')"` from a workspace that depends on `turbo`. + +Read `docs/README.md` inside that installed package first, then read the relevant pages from its `docs/` directory before changing Turborepo configuration or commands. Heed deprecation notices. These bundled docs match the installed package version and are available without network access. + +This block is written and re-added by `turbo` before repository-scoped commands when an AI agent is detected. In the Turborepo source repository, its template is defined in `crates/turborepo-cli/src/cli/agent_guidance.rs`. Removing the managed block while updates are enabled means a later qualifying invocation will add it again. Set `"agentGuidance": false` in the root `turbo.json` or `turbo.jsonc` to opt out; this does not remove an existing block. Keep the block committed with your work to avoid an uncommitted change on the next agent invocation. + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b0cebd8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Module Federation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5ad6652..ad5f4af 100644 --- a/README.md +++ b/README.md @@ -1 +1,147 @@ -# tanstack \ No newline at end of file +# @module-federation/tanstack + +Module Federation support for TanStack Start on Vite and Rsbuild. Rsbuild uses +Rspack under the hood. + +## Install + +```bash +pnpm add @module-federation/tanstack @tanstack/react-start +``` + +Install the adapter for the bundler you use. They are optional peers so an +Rsbuild-only application does not install or resolve the Vite adapter, and a +Vite-only application does not install or resolve the Rsbuild adapter. + +```bash +pnpm add @module-federation/vite vite # Vite +pnpm add @module-federation/rsbuild-plugin @rsbuild/core # Rsbuild +``` + +## Configure + +The federation wrapper must come before `tanstackStart()` so Module Federation +can normalize TanStack's client and SSR entrypoints: + +```ts +import { tanstackStartModuleFederation } from "@module-federation/tanstack/vite"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + tanstackStartModuleFederation({ + name: "host", + remotes: { + remote: { + type: "module", + name: "remote", + entry: "https://example.test/remoteEntry.js", + }, + }, + }), + tanstackStart(), + react(), + ], +}); +``` + +Defaults are `remoteEntry.js`, `manifest: true`, and +`hostInitInjectLocation: "entry"`. React and `react-dom` are shared +singletons; when the config declares `remotes`, they are also eager so the +host's own React stays in the share scope. Explicit entries in `shared` take +precedence. + +Do not set a global Module Federation `target`. TanStack Start builds client +and SSR environments from one Vite configuration, so the wrapper leaves target +selection to `@module-federation/vite` for each environment. + +The package supports Node 22.18+, 24.11+, and 26+, plus TanStack Start +1.167.43+. The Vite adapter requires Vite 8 and `@module-federation/vite` +1.23.3+. + +The package root remains a Vite compatibility export. New Vite projects can +import `/vite` explicitly. Rsbuild projects must import `/rsbuild`: + +```ts +import { defineConfig } from "@rsbuild/core"; +import { pluginReact } from "@rsbuild/plugin-react"; +import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild"; +import { tanstackStartModuleFederation } from "@module-federation/tanstack/rsbuild"; + +export default defineConfig({ + plugins: [ + pluginReact(), + tanstackStart(), + ...tanstackStartModuleFederation({ + federation: { + name: "host", + shareStrategy: "loaded-first", + remotes: { remote: "remote@https://example.test/mf-manifest.json" }, + }, + }), + ], +}); +``` + +The Rsbuild adapter emits script-compatible browser output, so its manifest can +be consumed by Vite and Rsbuild hosts. It shares singleton `react` and +`react-dom` (eager for hosts, lazy for remotes), enables async startup, and +gives remotes `publicPath: "auto"` so other origins load their chunks from the +remote. Hosts should use `shareStrategy: "loaded-first"` so one offline remote +cannot fail startup. + +Rsbuild SSR federation is opt-in with `server: true`. A remote builds a Node +container, `remoteEntry.ssr.cjs`, next to its browser entry and advertises it in +its manifest, so Vite and Rsbuild hosts can render it on the server. A host +loads remotes on the server from an async-node CommonJS server build, and +renders Vite remotes too when `@module-federation/vite` is installed. See the +[package README](packages/tanstack/README.md#ssr-federation). + +`@module-federation/tanstack/runtime` has two helpers for route code: +`lazyRemote`, a `React.lazy` that loads a failed remote again instead of +failing until the server restarts, and `getRemoteStylesheets`, which reads a +remote's stylesheets from its manifest so a route can put them in the server +response's ``. See +[rendering remotes on the server](packages/tanstack/README.md#rendering-remotes-on-the-server). + +## Support status + +| Host | Remote | Browser federation | Federated SSR | +| ------- | ------- | ------------------ | ---------------------------------- | +| Vite | Vite | Supported | Supported | +| Vite | Rsbuild | Supported | Supported (remote: `server: true`) | +| Rsbuild | Vite | Supported | Supported (host: `server: true`) | +| Rsbuild | Rsbuild | Supported | Supported (both: `server: true`) | + +Known limitations are listed in the +[package README](packages/tanstack/README.md#known-limitations), and +[`TODO.md`](TODO.md) tracks the remaining work. + +## Examples + +The workspace has six TanStack Start apps; see [`apps/README.md`](apps/README.md). + +The repository uses pnpm 12 (pinned in `packageManager`). pnpm 12 ships as a +native binary, so an older global pnpm cannot switch to it automatically; +install pnpm 12 itself (see https://pnpm.io/installation) before running these +commands. + +```bash +pnpm install +pnpm start # development servers +pnpm preview # production builds +``` + +Open the Vite host at http://127.0.0.1:3000, the Rsbuild host at +http://127.0.0.1:3003, or the Rsbuild SSR host at http://127.0.0.1:3005. + +`pnpm test` builds everything, then runs the six apps twice, as development +servers and as production builds, and drives every host in headless Chromium. +It checks server-rendered remote markup from both bundlers (including +concurrent first requests), remote stylesheets in the server response, +hydration, remote interactivity, shared React context, console errors, +manifest asset reachability, and the fallback and recovery when remotes go +offline. Run +`pnpm exec playwright install chromium` once before the first local test run. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..ef738f2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,178 @@ +# Federation TODO + +This file tracks what `@module-federation/tanstack` proves today and the work +left after 0.1.0. + +Assessment date: 2026-10-06. + +## Support boundary + +| Host | Remote | Browser federation | Federated SSR | Proof (development and production) | +| ------- | ------- | ------------------ | ------------- | --------------------------------------------------------------------- | +| Vite | Vite | Supported | Supported | `vite-host` renders `vite-remote` on the server | +| Vite | Rsbuild | Supported | Supported | `vite-host` renders `rsbuild-ssr-remote` on the server | +| Rsbuild | Vite | Supported | Supported | `rsbuild-ssr-host` renders `vite-remote` on the server | +| Rsbuild | Rsbuild | Supported | Supported | `rsbuild-ssr-host` renders `rsbuild-ssr-remote` on the server | +| Any | Any | Supported | — | `rsbuild-host` and `vite-host` load browser-only remotes on hydration | + +`test/example-runtime.test.mjs` (development servers) and +`test/example-production.test.mjs` (production builds) run the same scenarios +through `test/support/examples.mjs` on every supported Node line in CI: remote +markup in the initial HTML for eight concurrent first requests, remote +stylesheets in the response (builds), hydration without console errors, remote +interactivity, one React instance and shared context, fallback and recovery +while every remote is offline, and hosts that start during an outage rendering +the remotes once they return. + +## Done for 0.1.0 + +### Rsbuild remotes on the server + +- [x] Default `experiments.asyncStartup: true` for browser and server + federation, preserving an explicit `false`. +- [x] Share `react` and `react-dom` as singletons: eager for hosts, lazy for + remotes and server containers, so remotes cannot replace a host's React. +- [x] Build a remote's Node container in a dedicated `mf-server` environment: + `remoteEntry.ssr.cjs` next to `remoteEntry.js`, chunks in `ssr/`. It + contains only the exposed modules; the app's server bundle is never + published. +- [x] Advertise the container in the browser manifest as a `commonjs-module` + `ssrRemoteEntry`, which Module Federation otherwise only does for + Rslib's `dual` target. +- [x] Give remotes `publicPath: "auto"`. TanStack Start derives `/` from + `server.base`, which made production remotes request their chunks from + the host's origin, and made Node hosts fail to fetch them. +- [x] Keep the container next to the manifest, so its chunks resolve the same + way from the container URL (Module Federation's Node runtime) and from + the manifest URL (`@module-federation/vite`). + +### Rsbuild hosts on the server + +- [x] Load remotes in a host's server bundle as async-node CommonJS, keep + `dist/server/index.js` as an ES module that re-exports it, and write the + server output to disk in development so async-node chunks match the + in-memory entry. The dev bundle goes to + `node_modules/.cache/tanstack-start-federation/`, so it never replaces a + production build in `dist/server`. +- [x] `@module-federation/tanstack/node-entry-loader` compiles CommonJS + containers as CommonJS modules, so their `import()` calls work in + Rsbuild's development runner without experimental Node flags or + warnings. +- [x] Vite remotes: the loader hands ES module entries to + `@module-federation/vite`'s SSR loader, loaded with Node's `require` so + its imports bypass Rsbuild's runner. It registers the host's loaded + shared modules under their node_modules paths, so React imported from + node_modules by a Vite dev server's module runner is the host's React. +- [x] Failures throw `RemoteEntryError` with `phase` (`fetch`, `evaluate`, + `loader`), the remote, the URL, and the original `cause`, instead of + falling through to Module Federation's Node loader, which cannot load + ES modules without Node flags. + +### Vite hosts + +- [x] Support Vite 8 only, like TanStack Start's examples (Vite ^8.0.14 with + `@vitejs/plugin-react` 6, which requires Vite 8). The `vite` peer range + is `^8.0.0`, and the adapter stops with an error on older versions: on + Vite 7, `@module-federation/vite` hangs server-side remote loads in + development and emits an unresolved `virtual:` import in production. +- [x] Require `@module-federation/vite` 1.23.3, which ships the fixes for the + problems found here (see "Fixed upstream" below), so the adapter needs + no workarounds for it. + +### SSR quality and hardening + +- [x] `getRemoteStylesheets` (`@module-federation/tanstack/runtime`) reads an + expose's stylesheets from the remote manifest for the route `head`, so + server-rendered remote markup is styled before JavaScript loads. The + production suite checks it with JavaScript disabled. +- [x] Production builds (`vite preview`, `rsbuild preview`) run the same browser + scenarios as development servers. +- [x] Hosts render a fallback, not a 500 or a blank page, while remotes are + offline, and render remote markup again once they return, in development + and production. +- [x] Hosts recover from a remote that failed its first load, such as a host + started during an outage. Three layers kept the failure: + `@module-federation/vite`'s single load attempt (fixed in 1.23.3), + Rspack's module cache (a failed remote module kept empty exports; + Rspack's `strictModuleExceptionHandling` caches the error instead, so + the adapter removes the module from the cache), and `React.lazy` + (`lazyRemote`). +- [x] Every remote, including the Vite remote, works as a standalone TanStack + Start app (`@module-federation/vite` 1.23.3). +- [x] `useState`, `useEffect`, Suspense, and error boundaries across the + federation boundary (counter, hydration status, lazy remote components, + `RemoteBoundary`). +- [x] React context created by each host and read by every remote, including + across bundlers and in server-rendered HTML, through the singleton + `example-host-context` package. + +## Open + +### Upstream issues + +To report to `module-federation/core`: + +- [ ] `@module-federation/webpack-bundler-runtime`: a remote module that + failed stays in the module cache with empty exports, although the load + itself is retried. Worked around by `remoteRetryPlugin`. +- [ ] `@module-federation/node`: a container without an absolute public path + logs `Backup remote entry found` for every chunk it loads through a Vite + host. + +Fixed upstream in `@module-federation/vite` 1.23.3: + +- [x] An unawaited `__mf_remote_pending` turned a remote outage into an + unhandled rejection that exited a dev server + ([vite#1421](https://github.com/module-federation/vite/pull/1421)). +- [x] A server wrapper made one load attempt, so a remote that failed its + first load failed for the life of the process + ([vite#1424](https://github.com/module-federation/vite/issues/1424), + fixed by [vite#1426](https://github.com/module-federation/vite/pull/1426)). +- [x] The SSR loader could not load a CommonJS server container from a + manifest ([vite#1423](https://github.com/module-federation/vite/issues/1423), + fixed by [vite#1425](https://github.com/module-federation/vite/pull/1425)). +- [x] Rollup builds (Vite 5 to 7) kept an unresolved `virtual:mf-exposes-ssr:` + import in the SSR entry outside Nuxt + ([vite#1422](https://github.com/module-federation/vite/pull/1422)). +- [x] A Vite remote with `exposes` did not hydrate when opened directly + ([vite#1413](https://github.com/module-federation/vite/issues/1413), + fixed by [vite#1415](https://github.com/module-federation/vite/pull/1415) + and [vite#1420](https://github.com/module-federation/vite/pull/1420)). + +### SSR quality + +- [ ] Revalidate a changed remote manifest and server entry without restarting + the host, and prove no stale container or shared module is served. + `@module-federation/vite` has `revalidate()` and `maxAgeMs` for its own + loader; the Module Federation runtime caches containers for the life of + the process. +- [ ] Test cyclic imports across chunks in a server remote graph. +- [ ] In development, a Vite remote's server modules import React from the files + its dev server resolves, so host and remote must resolve React to the + same files (true in a monorepo). Production builds take React from the + share scope. + +### Shared dependencies + +- [x] React range: React 19. TanStack Start 1.168 accepts React 18, but its + Rsbuild server build fails with it: TanStack Router imports React's + `use`, which React 18 lacks. Vite apps build with React 18; revisit when + TanStack Start supports it on both bundlers. +- [ ] Validate version-mismatch warnings for incompatible React majors, and + that `import: false` fails at startup when the host cannot provide a + package. With mismatched React majors, the federation runtime already + warns (`Version 18.3.1 ... does not satisfy the requirement ... 19.3.0`). + +### Error reporting + +- [ ] Distinguish manifest, chunk, and shared-dependency failures the way + `RemoteEntryError` does for server entries. +- [ ] Add optional observability reports for raw runtime codes that do not + contain enough context. + +## Release work for each new SSR combination + +- [ ] Update the support table only after the combination passes the + development and production scenarios on every supported Node line. +- [ ] Add a Changeset and list the tested bundler and Node versions in the + release notes. diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 0000000..3fde10c --- /dev/null +++ b/apps/README.md @@ -0,0 +1,65 @@ +# TanStack Start examples + +Six TanStack Start applications: a host and a remote for each bundler, plus an +Rsbuild pair that federates on the server. Both SSR hosts render a Vite remote +and an Rsbuild remote into their initial HTML. + +`host-context` is a small shared package: each host provides its name through a +React context, and every remote card shows "Host: …". It is shared as a +singleton, so the right name only appears when host and remote resolve one +module instance. + +| App | Port | Role | +| -------------------- | ---- | -------------------------------------------------------------------------------- | +| `vite-host` | 3000 | Server-renders the Vite and Rsbuild SSR remotes; loads `rsbuild-remote` later | +| `vite-remote` | 3001 | Exposes `./StatusCard`, rendered into both SSR hosts' initial HTML | +| `rsbuild-remote` | 3002 | Exposes `./StatusCard` through a browser manifest | +| `rsbuild-host` | 3003 | Loads both browser remotes with `loadRemote()` after hydration | +| `rsbuild-ssr-remote` | 3004 | Exposes `./StatusCard` with a Node container (`server: true`) for both SSR hosts | +| `rsbuild-ssr-host` | 3005 | Server-renders the Rsbuild SSR and Vite remotes (`server: true`) | + +From the repository root: + +```bash +pnpm start # development servers +pnpm preview # production builds (`vite preview` / `rsbuild preview`) +``` + +Then open http://127.0.0.1:3000, http://127.0.0.1:3003, or +http://127.0.0.1:3005. Every remote card has a counter button; clicking it +proves the remote shares the host's React instance. Cards rendered on the +server show "Rendered on the server" until they hydrate. + +## What each host shows + +- **Vite host.** The route loader imports the Vite remote and the Rsbuild SSR + remote, so both cards are in the server response and hydrate in place. The + loader also reads each remote's stylesheets with `getRemoteStylesheets`, and + the route's `head` links them, so the cards are styled before JavaScript + runs. The `rsbuild-remote` card mounts after hydration. The cards are + `lazyRemote` components, which load again after a failure. Every remote sits + behind `RemoteBoundary`, an error boundary with a fallback card, and the + loader ignores failed imports, so an offline remote never fails the route. +- **Rsbuild host.** `RemoteCardSlot` calls `loadRemote()` in an effect and + shows "Remote unavailable" when a remote fails. The host uses + `shareStrategy: "loaded-first"`, so it does not fetch every remote's + manifest at startup. +- **Rsbuild SSR host.** Same pattern as the Vite host, on Rsbuild. The + Module Federation Node runtime loads the Rsbuild remote's + `remoteEntry.ssr.cjs` container, and hands the Vite remote's ES module entry + to `@module-federation/vite`'s SSR loader, which is why this app depends on + `@module-federation/vite` and `vite`. + +Stop a remote while the hosts are open and reload them: each host keeps +rendering and shows its fallback, then renders the remote again once it is +back. The same holds for a host started before its remotes. + +## Notes + +- The federation wrapper must come before `tanstackStart()` in the Vite + configs, and after it in the Rsbuild configs. +- Each `start` script runs the bundler's development server. In Vite's CLI the + bare `vite` command starts the dev server; `vite start` would treat `start` + as a project directory. +- Every remote also works as a standalone TanStack Start app: open port 3001, + 3002, or 3004 directly, and its card shows "Host: standalone". diff --git a/apps/host-context/package.json b/apps/host-context/package.json new file mode 100644 index 0000000..b832dfa --- /dev/null +++ b/apps/host-context/package.json @@ -0,0 +1,17 @@ +{ + "name": "example-host-context", + "version": "0.0.0", + "private": true, + "sideEffects": false, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "peerDependencies": { + "react": "^19.0.0" + }, + "devDependencies": { + "@types/react": "19.3.0", + "react": "19.3.0" + } +} diff --git a/apps/host-context/src/index.ts b/apps/host-context/src/index.ts new file mode 100644 index 0000000..806c0f5 --- /dev/null +++ b/apps/host-context/src/index.ts @@ -0,0 +1,11 @@ +import { createContext, useContext } from "react"; + +/** + * Provided by each host and read by every remote card. Shared as a Module Federation + * singleton, so a remote only sees the host's value if both resolve one module instance. + */ +export const HostContext = createContext("standalone"); + +export function useHostName() { + return useContext(HostContext); +} diff --git a/apps/rsbuild-host/package.json b/apps/rsbuild-host/package.json new file mode 100644 index 0000000..fad3a00 --- /dev/null +++ b/apps/rsbuild-host/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-start-rsbuild-host", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "rsbuild build", + "dev": "rsbuild dev --port 3003 --strict-port", + "start": "rsbuild dev --host 127.0.0.1 --port 3003 --strict-port", + "preview": "rsbuild preview --host 127.0.0.1 --port 3003", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@module-federation/runtime": "2.9.2", + "@tanstack/react-router": "1.170.41", + "@tanstack/react-start": "1.168.60", + "example-host-context": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@module-federation/rsbuild-plugin": "2.9.2", + "@module-federation/tanstack": "workspace:*", + "@rsbuild/core": "2.2.11", + "@rsbuild/plugin-react": "2.1.1", + "@types/node": "26.6.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "typescript": "7.0.2" + } +} diff --git a/apps/rsbuild-host/rsbuild.config.ts b/apps/rsbuild-host/rsbuild.config.ts new file mode 100644 index 0000000..caec5e4 --- /dev/null +++ b/apps/rsbuild-host/rsbuild.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from "@rsbuild/core"; +import { pluginReact } from "@rsbuild/plugin-react"; +import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild"; +import { tanstackStartModuleFederation } from "@module-federation/tanstack/rsbuild"; + +export default defineConfig({ + dev: { lazyCompilation: false }, + server: { host: "127.0.0.1", port: 3003 }, + plugins: [ + pluginReact(), + tanstackStart(), + ...tanstackStartModuleFederation({ + federation: { + dts: false, + name: "tanstack_rsbuild_host", + // Workspace dependencies declare "workspace:*", which is not a semver range. + shared: { "example-host-context": { requiredVersion: "*", singleton: true } }, + // Resolve remotes on demand so one offline remote cannot block host startup. + shareStrategy: "loaded-first", + remotes: { + tanstack_vite_remote: "tanstack_vite_remote@http://127.0.0.1:3001/mf-manifest.json", + tanstack_rsbuild_remote: "tanstack_rsbuild_remote@http://127.0.0.1:3002/mf-manifest.json", + }, + }, + }), + ], +}); diff --git a/apps/rsbuild-host/src/assets.d.ts b/apps/rsbuild-host/src/assets.d.ts new file mode 100644 index 0000000..596a189 --- /dev/null +++ b/apps/rsbuild-host/src/assets.d.ts @@ -0,0 +1,4 @@ +declare module "*.css?url" { + const href: string; + export default href; +} diff --git a/apps/rsbuild-host/src/components/RemoteCardSlot.tsx b/apps/rsbuild-host/src/components/RemoteCardSlot.tsx new file mode 100644 index 0000000..c99449f --- /dev/null +++ b/apps/rsbuild-host/src/components/RemoteCardSlot.tsx @@ -0,0 +1,38 @@ +import { loadRemote } from "@module-federation/runtime"; +import { useEffect, useState, type ComponentType } from "react"; + +type RemoteModule = { + default: ComponentType; +}; + +export function RemoteCardSlot({ fallback, remote }: { fallback: string; remote: string }) { + const [RemoteCard, setRemoteCard] = useState(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + let active = true; + setRemoteCard(null); + setFailed(false); + + loadRemote(remote) + .then((remoteModule) => { + if (!active) return; + if (remoteModule?.default) { + setRemoteCard(() => remoteModule.default); + } else { + setFailed(true); + } + }) + .catch(() => { + if (active) setFailed(true); + }); + + return () => { + active = false; + }; + }, [remote]); + + if (failed) return
Remote unavailable
; + if (!RemoteCard) return
{fallback}
; + return ; +} diff --git a/apps/rsbuild-host/src/routeTree.gen.ts b/apps/rsbuild-host/src/routeTree.gen.ts new file mode 100644 index 0000000..dceedff --- /dev/null +++ b/apps/rsbuild-host/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/apps/rsbuild-host/src/router.tsx b/apps/rsbuild-host/src/router.tsx new file mode 100644 index 0000000..6982ebb --- /dev/null +++ b/apps/rsbuild-host/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +export function getRouter() { + return createRouter({ + defaultPreload: "intent", + routeTree, + scrollRestoration: true, + }); +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/apps/rsbuild-host/src/routes/__root.tsx b/apps/rsbuild-host/src/routes/__root.tsx new file mode 100644 index 0000000..c235e44 --- /dev/null +++ b/apps/rsbuild-host/src/routes/__root.tsx @@ -0,0 +1,39 @@ +import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router"; +import { HostContext } from "example-host-context"; +import type { ReactNode } from "react"; +import stylesHref from "../styles.css?url"; + +export const Route = createRootRoute({ + head: () => ({ + links: [{ href: stylesHref, rel: "stylesheet" }], + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + ], + }), + component: Root, +}); + +function Root() { + return ( + + + + + + ); +} + +function Document({ children }: { children: ReactNode }) { + return ( + + + + + + {children} + + + + ); +} diff --git a/apps/rsbuild-host/src/routes/index.tsx b/apps/rsbuild-host/src/routes/index.tsx new file mode 100644 index 0000000..41b643c --- /dev/null +++ b/apps/rsbuild-host/src/routes/index.tsx @@ -0,0 +1,24 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { RemoteCardSlot } from "../components/RemoteCardSlot"; + +export const Route = createFileRoute("/")({ component: Home }); + +function Home() { + return ( +
+

Rsbuild host

+

Rspack host, two remote formats.

+

This TanStack Start app loads both remotes through the browser runtime.

+
+ + +
+
+ ); +} diff --git a/apps/rsbuild-host/src/styles.css b/apps/rsbuild-host/src/styles.css new file mode 100644 index 0000000..e379e6a --- /dev/null +++ b/apps/rsbuild-host/src/styles.css @@ -0,0 +1,32 @@ +:root { + background: #111; + color: #edf6ef; + font-family: system-ui, sans-serif; +} +body { + margin: 0; +} +.page-shell { + width: min(760px, calc(100% - 40px)); + margin: 0 auto; + padding: 80px 0; +} +.eyebrow { + color: #91f1b0; + font-family: monospace; + text-transform: uppercase; +} +h1 { + font-size: clamp(3rem, 10vw, 6rem); + margin: 12px 0; +} +.remote-loading { + border: 1px solid #4b6653; + margin-top: 36px; + padding: 30px; +} +.remote-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; +} diff --git a/apps/rsbuild-host/tsconfig.json b/apps/rsbuild-host/tsconfig.json new file mode 100644 index 0000000..0416574 --- /dev/null +++ b/apps/rsbuild-host/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["src/**/*.ts", "src/**/*.tsx", "rsbuild.config.ts"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node"], + "verbatimModuleSyntax": true + } +} diff --git a/apps/rsbuild-remote/package.json b/apps/rsbuild-remote/package.json new file mode 100644 index 0000000..4aee25b --- /dev/null +++ b/apps/rsbuild-remote/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-start-rsbuild-remote", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "rsbuild build", + "dev": "rsbuild dev --port 3002 --strict-port", + "start": "rsbuild dev --host 127.0.0.1 --port 3002 --strict-port", + "preview": "rsbuild preview --host 127.0.0.1 --port 3002", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@module-federation/runtime": "2.9.2", + "@tanstack/react-router": "1.170.41", + "@tanstack/react-start": "1.168.60", + "example-host-context": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@module-federation/rsbuild-plugin": "2.9.2", + "@module-federation/tanstack": "workspace:*", + "@rsbuild/core": "2.2.11", + "@rsbuild/plugin-react": "2.1.1", + "@types/node": "26.6.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "typescript": "7.0.2" + } +} diff --git a/apps/rsbuild-remote/rsbuild.config.ts b/apps/rsbuild-remote/rsbuild.config.ts new file mode 100644 index 0000000..34d992e --- /dev/null +++ b/apps/rsbuild-remote/rsbuild.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from "@rsbuild/core"; +import { pluginReact } from "@rsbuild/plugin-react"; +import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild"; +import { tanstackStartModuleFederation } from "@module-federation/tanstack/rsbuild"; + +export default defineConfig({ + dev: { lazyCompilation: false }, + server: { + cors: { origin: ["http://127.0.0.1:3000", "http://127.0.0.1:3003"] }, + host: "127.0.0.1", + port: 3002, + }, + plugins: [ + pluginReact(), + tanstackStart(), + ...tanstackStartModuleFederation({ + federation: { + // The type-hints runtime plugin opens a WebSocket to the dev type server, which + // races host startup and logs errors in the browser. Types are still generated. + dev: { disableDynamicRemoteTypeHints: true }, + exposes: { "./StatusCard": "./src/components/StatusCard.tsx" }, + name: "tanstack_rsbuild_remote", + // Workspace dependencies declare "workspace:*", which is not a semver range. + shared: { "example-host-context": { requiredVersion: "*", singleton: true } }, + }, + }), + ], +}); diff --git a/apps/rsbuild-remote/src/assets.d.ts b/apps/rsbuild-remote/src/assets.d.ts new file mode 100644 index 0000000..18e9eca --- /dev/null +++ b/apps/rsbuild-remote/src/assets.d.ts @@ -0,0 +1,6 @@ +declare module "*.css"; + +declare module "*.css?url" { + const href: string; + export default href; +} diff --git a/apps/rsbuild-remote/src/components/StatusCard.css b/apps/rsbuild-remote/src/components/StatusCard.css new file mode 100644 index 0000000..42e1cc1 --- /dev/null +++ b/apps/rsbuild-remote/src/components/StatusCard.css @@ -0,0 +1,23 @@ +.rsbuild-card { + border: 1px solid #83502a; + background: #291c13; + color: #fff3e8; + padding: 30px; +} + +.rsbuild-card .eyebrow { + color: #ffbc7c; + font-family: monospace; + text-transform: uppercase; +} + +.rsbuild-card h2 { + font-size: 2rem; +} + +.rsbuild-card button { + background: #ffbc7c; + border: 0; + cursor: pointer; + padding: 12px; +} diff --git a/apps/rsbuild-remote/src/components/StatusCard.tsx b/apps/rsbuild-remote/src/components/StatusCard.tsx new file mode 100644 index 0000000..b5b37cc --- /dev/null +++ b/apps/rsbuild-remote/src/components/StatusCard.tsx @@ -0,0 +1,19 @@ +import { useHostName } from "example-host-context"; +import { useState } from "react"; +import "./StatusCard.css"; + +export default function StatusCard() { + const hostName = useHostName(); + const [count, setCount] = useState(0); + return ( +
+

Federated from Rsbuild

+

One component, either host.

+

This card is built by Rspack and shared with the Vite host.

+

Host: {hostName}

+ +
+ ); +} diff --git a/apps/rsbuild-remote/src/routeTree.gen.ts b/apps/rsbuild-remote/src/routeTree.gen.ts new file mode 100644 index 0000000..dceedff --- /dev/null +++ b/apps/rsbuild-remote/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/apps/rsbuild-remote/src/router.tsx b/apps/rsbuild-remote/src/router.tsx new file mode 100644 index 0000000..673839e --- /dev/null +++ b/apps/rsbuild-remote/src/router.tsx @@ -0,0 +1,12 @@ +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +export function getRouter() { + return createRouter({ defaultPreload: "intent", routeTree, scrollRestoration: true }); +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/apps/rsbuild-remote/src/routes/__root.tsx b/apps/rsbuild-remote/src/routes/__root.tsx new file mode 100644 index 0000000..2eb5fe8 --- /dev/null +++ b/apps/rsbuild-remote/src/routes/__root.tsx @@ -0,0 +1,36 @@ +import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router"; +import type { ReactNode } from "react"; +import stylesHref from "../styles.css?url"; + +export const Route = createRootRoute({ + head: () => ({ + links: [{ href: stylesHref, rel: "stylesheet" }], + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + ], + }), + component: Root, +}); + +function Root() { + return ( + + + + ); +} + +function Document({ children }: { children: ReactNode }) { + return ( + + + + + + {children} + + + + ); +} diff --git a/apps/rsbuild-remote/src/routes/index.tsx b/apps/rsbuild-remote/src/routes/index.tsx new file mode 100644 index 0000000..9cfe99e --- /dev/null +++ b/apps/rsbuild-remote/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import StatusCard from "../components/StatusCard"; + +export const Route = createFileRoute("/")({ component: Home }); + +function Home() { + return ( +
+

Rsbuild remote

+

Rspack-built remote.

+ +
+ ); +} diff --git a/apps/rsbuild-remote/src/styles.css b/apps/rsbuild-remote/src/styles.css new file mode 100644 index 0000000..fbab7d3 --- /dev/null +++ b/apps/rsbuild-remote/src/styles.css @@ -0,0 +1,22 @@ +:root { + background: #16110d; + color: #fff3e8; + font-family: system-ui, sans-serif; +} +body { + margin: 0; +} +.page-shell { + width: min(760px, calc(100% - 40px)); + margin: 0 auto; + padding: 80px 0; +} +.eyebrow { + color: #ffbc7c; + font-family: monospace; + text-transform: uppercase; +} +h1 { + font-size: clamp(3rem, 10vw, 6rem); + margin: 12px 0 48px; +} diff --git a/apps/rsbuild-remote/tsconfig.json b/apps/rsbuild-remote/tsconfig.json new file mode 100644 index 0000000..0416574 --- /dev/null +++ b/apps/rsbuild-remote/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["src/**/*.ts", "src/**/*.tsx", "rsbuild.config.ts"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node"], + "verbatimModuleSyntax": true + } +} diff --git a/apps/rsbuild-ssr-host/package.json b/apps/rsbuild-ssr-host/package.json new file mode 100644 index 0000000..c8e67d3 --- /dev/null +++ b/apps/rsbuild-ssr-host/package.json @@ -0,0 +1,33 @@ +{ + "name": "tanstack-start-rsbuild-ssr-host", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "rsbuild build", + "dev": "rsbuild dev --port 3005 --strict-port", + "start": "rsbuild dev --host 127.0.0.1 --port 3005 --strict-port", + "preview": "rsbuild preview --host 127.0.0.1 --port 3005", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@module-federation/runtime": "2.9.2", + "@module-federation/vite": "1.23.3", + "@tanstack/react-router": "1.170.41", + "@tanstack/react-start": "1.168.60", + "example-host-context": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@module-federation/rsbuild-plugin": "2.9.2", + "@module-federation/tanstack": "workspace:*", + "@rsbuild/core": "2.2.11", + "@rsbuild/plugin-react": "2.1.1", + "@types/node": "26.6.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "typescript": "7.0.2", + "vite": "8.3.2" + } +} diff --git a/apps/rsbuild-ssr-host/rsbuild.config.ts b/apps/rsbuild-ssr-host/rsbuild.config.ts new file mode 100644 index 0000000..f02e28e --- /dev/null +++ b/apps/rsbuild-ssr-host/rsbuild.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "@rsbuild/core"; +import { pluginReact } from "@rsbuild/plugin-react"; +import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild"; +import { tanstackStartModuleFederation } from "@module-federation/tanstack/rsbuild"; + +export default defineConfig({ + dev: { lazyCompilation: false }, + server: { host: "127.0.0.1", port: 3005 }, + plugins: [ + pluginReact(), + tanstackStart(), + ...tanstackStartModuleFederation({ + federation: { + dts: false, + name: "tanstack_rsbuild_ssr_host", + // Workspace dependencies declare "workspace:*", which is not a semver range. + shared: { "example-host-context": { requiredVersion: "*", singleton: true } }, + shareStrategy: "loaded-first", + remotes: { + tanstack_rsbuild_ssr_remote: + "tanstack_rsbuild_ssr_remote@http://127.0.0.1:3004/mf-manifest.json", + tanstack_vite_remote: "tanstack_vite_remote@http://127.0.0.1:3001/mf-manifest.json", + }, + }, + // Loads remotes on the server. Vite remotes also need @module-federation/vite installed. + server: true, + }), + ], +}); diff --git a/apps/rsbuild-ssr-host/src/assets.d.ts b/apps/rsbuild-ssr-host/src/assets.d.ts new file mode 100644 index 0000000..596a189 --- /dev/null +++ b/apps/rsbuild-ssr-host/src/assets.d.ts @@ -0,0 +1,4 @@ +declare module "*.css?url" { + const href: string; + export default href; +} diff --git a/apps/rsbuild-ssr-host/src/components/RemoteBoundary.tsx b/apps/rsbuild-ssr-host/src/components/RemoteBoundary.tsx new file mode 100644 index 0000000..7e953b5 --- /dev/null +++ b/apps/rsbuild-ssr-host/src/components/RemoteBoundary.tsx @@ -0,0 +1,28 @@ +import { Component, Suspense, type ReactNode } from "react"; + +type Props = { children: ReactNode; fallback: string; name: string }; + +/** Keeps a failed remote from taking down the host route. */ +export class RemoteBoundary extends Component { + state = { failed: false }; + + static getDerivedStateFromError() { + return { failed: true }; + } + + componentDidCatch(error: unknown) { + console.warn(`[${this.props.name}] remote failed to render`, error); + } + + render() { + if (this.state.failed) { + return
{this.props.name} is unavailable
; + } + + return ( + {this.props.fallback}}> + {this.props.children} + + ); + } +} diff --git a/apps/rsbuild-ssr-host/src/remote.d.ts b/apps/rsbuild-ssr-host/src/remote.d.ts new file mode 100644 index 0000000..c46c4d0 --- /dev/null +++ b/apps/rsbuild-ssr-host/src/remote.d.ts @@ -0,0 +1,13 @@ +declare module "tanstack_rsbuild_ssr_remote/StatusCard" { + import type { ComponentType } from "react"; + + const StatusCard: ComponentType; + export default StatusCard; +} + +declare module "tanstack_vite_remote/StatusCard" { + import type { ComponentType } from "react"; + + const StatusCard: ComponentType; + export default StatusCard; +} diff --git a/apps/rsbuild-ssr-host/src/routeTree.gen.ts b/apps/rsbuild-ssr-host/src/routeTree.gen.ts new file mode 100644 index 0000000..dceedff --- /dev/null +++ b/apps/rsbuild-ssr-host/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/apps/rsbuild-ssr-host/src/router.tsx b/apps/rsbuild-ssr-host/src/router.tsx new file mode 100644 index 0000000..6982ebb --- /dev/null +++ b/apps/rsbuild-ssr-host/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +export function getRouter() { + return createRouter({ + defaultPreload: "intent", + routeTree, + scrollRestoration: true, + }); +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/apps/rsbuild-ssr-host/src/routes/__root.tsx b/apps/rsbuild-ssr-host/src/routes/__root.tsx new file mode 100644 index 0000000..cc68b8a --- /dev/null +++ b/apps/rsbuild-ssr-host/src/routes/__root.tsx @@ -0,0 +1,39 @@ +import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router"; +import { HostContext } from "example-host-context"; +import type { ReactNode } from "react"; +import stylesHref from "../styles.css?url"; + +export const Route = createRootRoute({ + head: () => ({ + links: [{ href: stylesHref, rel: "stylesheet" }], + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + ], + }), + component: Root, +}); + +function Root() { + return ( + + + + + + ); +} + +function Document({ children }: { children: ReactNode }) { + return ( + + + + + + {children} + + + + ); +} diff --git a/apps/rsbuild-ssr-host/src/routes/index.tsx b/apps/rsbuild-ssr-host/src/routes/index.tsx new file mode 100644 index 0000000..fa5dc6e --- /dev/null +++ b/apps/rsbuild-ssr-host/src/routes/index.tsx @@ -0,0 +1,57 @@ +import { getRemoteStylesheets, lazyRemote } from "@module-federation/tanstack/runtime"; +import { createFileRoute } from "@tanstack/react-router"; +import { RemoteBoundary } from "../components/RemoteBoundary"; + +// Unlike React.lazy, lazyRemote loads again after a failure, so a server that hit a remote +// outage renders the remote once it is back. +const RsbuildStatusCard = lazyRemote(() => import("tanstack_rsbuild_ssr_remote/StatusCard")); +const ViteStatusCard = lazyRemote(() => import("tanstack_vite_remote/StatusCard")); + +// Manifests of the remotes this route renders on the server. +const serverRenderedManifests = [ + "http://127.0.0.1:3004/mf-manifest.json", + "http://127.0.0.1:3001/mf-manifest.json", +]; + +export const Route = createFileRoute("/")({ + // Loading the remotes before render puts their markup in the server response. + // An unavailable remote must not fail the route: its boundary renders a fallback. + loader: async () => { + const [stylesheets] = await Promise.all([ + Promise.all( + serverRenderedManifests.map((manifest) => + getRemoteStylesheets(manifest, "./StatusCard").catch(() => []), + ), + ).then((lists) => lists.flat()), + import("tanstack_rsbuild_ssr_remote/StatusCard").catch(() => null), + import("tanstack_vite_remote/StatusCard").catch(() => null), + ]); + return { stylesheets }; + }, + // The remote markup arrives with the HTML, so its stylesheets must too. Otherwise it stays + // unstyled until the remote's JavaScript loads. + head: ({ loaderData }) => ({ + links: loaderData?.stylesheets.map((href) => ({ href, rel: "stylesheet" })), + }), + component: Home, +}); + +function Home() { + return ( +
+

Rsbuild SSR host

+

Server-rendered from Rsbuild and Vite.

+

+ This TanStack Start server renders both remote cards before the page reaches the browser. +

+
+ + + + + + +
+
+ ); +} diff --git a/apps/rsbuild-ssr-host/src/styles.css b/apps/rsbuild-ssr-host/src/styles.css new file mode 100644 index 0000000..e379e6a --- /dev/null +++ b/apps/rsbuild-ssr-host/src/styles.css @@ -0,0 +1,32 @@ +:root { + background: #111; + color: #edf6ef; + font-family: system-ui, sans-serif; +} +body { + margin: 0; +} +.page-shell { + width: min(760px, calc(100% - 40px)); + margin: 0 auto; + padding: 80px 0; +} +.eyebrow { + color: #91f1b0; + font-family: monospace; + text-transform: uppercase; +} +h1 { + font-size: clamp(3rem, 10vw, 6rem); + margin: 12px 0; +} +.remote-loading { + border: 1px solid #4b6653; + margin-top: 36px; + padding: 30px; +} +.remote-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; +} diff --git a/apps/rsbuild-ssr-host/tsconfig.json b/apps/rsbuild-ssr-host/tsconfig.json new file mode 100644 index 0000000..0416574 --- /dev/null +++ b/apps/rsbuild-ssr-host/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["src/**/*.ts", "src/**/*.tsx", "rsbuild.config.ts"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node"], + "verbatimModuleSyntax": true + } +} diff --git a/apps/rsbuild-ssr-remote/package.json b/apps/rsbuild-ssr-remote/package.json new file mode 100644 index 0000000..db721b7 --- /dev/null +++ b/apps/rsbuild-ssr-remote/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-start-rsbuild-ssr-remote", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "rsbuild build", + "dev": "rsbuild dev --port 3004 --strict-port", + "start": "rsbuild dev --host 127.0.0.1 --port 3004 --strict-port", + "preview": "rsbuild preview --host 127.0.0.1 --port 3004", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@module-federation/runtime": "2.9.2", + "@tanstack/react-router": "1.170.41", + "@tanstack/react-start": "1.168.60", + "example-host-context": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@module-federation/rsbuild-plugin": "2.9.2", + "@module-federation/tanstack": "workspace:*", + "@rsbuild/core": "2.2.11", + "@rsbuild/plugin-react": "2.1.1", + "@types/node": "26.6.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "typescript": "7.0.2" + } +} diff --git a/apps/rsbuild-ssr-remote/rsbuild.config.ts b/apps/rsbuild-ssr-remote/rsbuild.config.ts new file mode 100644 index 0000000..8e302bf --- /dev/null +++ b/apps/rsbuild-ssr-remote/rsbuild.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "@rsbuild/core"; +import { pluginReact } from "@rsbuild/plugin-react"; +import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild"; +import { tanstackStartModuleFederation } from "@module-federation/tanstack/rsbuild"; + +export default defineConfig({ + dev: { lazyCompilation: false }, + server: { + cors: { origin: ["http://127.0.0.1:3000", "http://127.0.0.1:3005"] }, + host: "127.0.0.1", + port: 3004, + }, + plugins: [ + pluginReact(), + tanstackStart(), + ...tanstackStartModuleFederation({ + federation: { + dev: { disableDynamicRemoteTypeHints: true }, + dts: false, + exposes: { "./StatusCard": "./src/components/StatusCard.tsx" }, + name: "tanstack_rsbuild_ssr_remote", + // Workspace dependencies declare "workspace:*", which is not a semver range. + shared: { "example-host-context": { requiredVersion: "*", singleton: true } }, + }, + // Builds an async-node server container next to the browser container, so an + // Rsbuild host can render this card on the server. + server: true, + }), + ], +}); diff --git a/apps/rsbuild-ssr-remote/src/assets.d.ts b/apps/rsbuild-ssr-remote/src/assets.d.ts new file mode 100644 index 0000000..18e9eca --- /dev/null +++ b/apps/rsbuild-ssr-remote/src/assets.d.ts @@ -0,0 +1,6 @@ +declare module "*.css"; + +declare module "*.css?url" { + const href: string; + export default href; +} diff --git a/apps/rsbuild-ssr-remote/src/components/StatusCard.css b/apps/rsbuild-ssr-remote/src/components/StatusCard.css new file mode 100644 index 0000000..a0dfe22 --- /dev/null +++ b/apps/rsbuild-ssr-remote/src/components/StatusCard.css @@ -0,0 +1,23 @@ +.rsbuild-ssr-card { + border: 1px solid #83502a; + background: #291c13; + color: #fff3e8; + padding: 30px; +} + +.rsbuild-ssr-card .eyebrow { + color: #ffbc7c; + font-family: monospace; + text-transform: uppercase; +} + +.rsbuild-ssr-card h2 { + font-size: 2rem; +} + +.rsbuild-ssr-card button { + background: #ffbc7c; + border: 0; + cursor: pointer; + padding: 12px; +} diff --git a/apps/rsbuild-ssr-remote/src/components/StatusCard.tsx b/apps/rsbuild-ssr-remote/src/components/StatusCard.tsx new file mode 100644 index 0000000..68c0080 --- /dev/null +++ b/apps/rsbuild-ssr-remote/src/components/StatusCard.tsx @@ -0,0 +1,28 @@ +import { useHostName } from "example-host-context"; +import { useEffect, useState } from "react"; +import "./StatusCard.css"; + +export default function StatusCard() { + const hostName = useHostName(); + const [count, setCount] = useState(0); + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + setHydrated(true); + }, []); + + return ( +
+

Federated SSR from Rsbuild

+

+ {hydrated ? "Hydrated on the host" : "Rendered on the server"} +

+

Server-rendered by an Rsbuild remote.

+

The host's server loaded this card from the remote's async-node container.

+

Host: {hostName}

+ +
+ ); +} diff --git a/apps/rsbuild-ssr-remote/src/routeTree.gen.ts b/apps/rsbuild-ssr-remote/src/routeTree.gen.ts new file mode 100644 index 0000000..dceedff --- /dev/null +++ b/apps/rsbuild-ssr-remote/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/apps/rsbuild-ssr-remote/src/router.tsx b/apps/rsbuild-ssr-remote/src/router.tsx new file mode 100644 index 0000000..673839e --- /dev/null +++ b/apps/rsbuild-ssr-remote/src/router.tsx @@ -0,0 +1,12 @@ +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +export function getRouter() { + return createRouter({ defaultPreload: "intent", routeTree, scrollRestoration: true }); +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/apps/rsbuild-ssr-remote/src/routes/__root.tsx b/apps/rsbuild-ssr-remote/src/routes/__root.tsx new file mode 100644 index 0000000..2eb5fe8 --- /dev/null +++ b/apps/rsbuild-ssr-remote/src/routes/__root.tsx @@ -0,0 +1,36 @@ +import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router"; +import type { ReactNode } from "react"; +import stylesHref from "../styles.css?url"; + +export const Route = createRootRoute({ + head: () => ({ + links: [{ href: stylesHref, rel: "stylesheet" }], + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + ], + }), + component: Root, +}); + +function Root() { + return ( + + + + ); +} + +function Document({ children }: { children: ReactNode }) { + return ( + + + + + + {children} + + + + ); +} diff --git a/apps/rsbuild-ssr-remote/src/routes/index.tsx b/apps/rsbuild-ssr-remote/src/routes/index.tsx new file mode 100644 index 0000000..badc535 --- /dev/null +++ b/apps/rsbuild-ssr-remote/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import StatusCard from "../components/StatusCard"; + +export const Route = createFileRoute("/")({ component: Home }); + +function Home() { + return ( +
+

Rsbuild SSR remote

+

Rspack-built SSR remote.

+ +
+ ); +} diff --git a/apps/rsbuild-ssr-remote/src/styles.css b/apps/rsbuild-ssr-remote/src/styles.css new file mode 100644 index 0000000..fbab7d3 --- /dev/null +++ b/apps/rsbuild-ssr-remote/src/styles.css @@ -0,0 +1,22 @@ +:root { + background: #16110d; + color: #fff3e8; + font-family: system-ui, sans-serif; +} +body { + margin: 0; +} +.page-shell { + width: min(760px, calc(100% - 40px)); + margin: 0 auto; + padding: 80px 0; +} +.eyebrow { + color: #ffbc7c; + font-family: monospace; + text-transform: uppercase; +} +h1 { + font-size: clamp(3rem, 10vw, 6rem); + margin: 12px 0 48px; +} diff --git a/apps/rsbuild-ssr-remote/tsconfig.json b/apps/rsbuild-ssr-remote/tsconfig.json new file mode 100644 index 0000000..0416574 --- /dev/null +++ b/apps/rsbuild-ssr-remote/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["src/**/*.ts", "src/**/*.tsx", "rsbuild.config.ts"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node"], + "verbatimModuleSyntax": true + } +} diff --git a/apps/vite-host/package.json b/apps/vite-host/package.json new file mode 100644 index 0000000..c822690 --- /dev/null +++ b/apps/vite-host/package.json @@ -0,0 +1,30 @@ +{ + "name": "tanstack-start-vite-host", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite dev --port 3000 --strictPort", + "start": "vite --host 127.0.0.1 --port 3000 --strictPort", + "preview": "vite preview --host 127.0.0.1 --port 3000 --strictPort", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@module-federation/runtime": "2.9.2", + "@tanstack/react-router": "1.170.41", + "@tanstack/react-start": "1.168.60", + "example-host-context": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@module-federation/tanstack": "workspace:*", + "@types/node": "26.6.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.2", + "typescript": "7.0.2", + "vite": "8.3.2" + } +} diff --git a/apps/vite-host/src/components/RemoteBoundary.tsx b/apps/vite-host/src/components/RemoteBoundary.tsx new file mode 100644 index 0000000..7e953b5 --- /dev/null +++ b/apps/vite-host/src/components/RemoteBoundary.tsx @@ -0,0 +1,28 @@ +import { Component, Suspense, type ReactNode } from "react"; + +type Props = { children: ReactNode; fallback: string; name: string }; + +/** Keeps a failed remote from taking down the host route. */ +export class RemoteBoundary extends Component { + state = { failed: false }; + + static getDerivedStateFromError() { + return { failed: true }; + } + + componentDidCatch(error: unknown) { + console.warn(`[${this.props.name}] remote failed to render`, error); + } + + render() { + if (this.state.failed) { + return
{this.props.name} is unavailable
; + } + + return ( + {this.props.fallback}}> + {this.props.children} + + ); + } +} diff --git a/apps/vite-host/src/remote.d.ts b/apps/vite-host/src/remote.d.ts new file mode 100644 index 0000000..9342b12 --- /dev/null +++ b/apps/vite-host/src/remote.d.ts @@ -0,0 +1,20 @@ +declare module "tanstack_vite_remote/StatusCard" { + import type { ComponentType } from "react"; + + const StatusCard: ComponentType; + export default StatusCard; +} + +declare module "tanstack_rsbuild_remote/StatusCard" { + import type { ComponentType } from "react"; + + const StatusCard: ComponentType; + export default StatusCard; +} + +declare module "tanstack_rsbuild_ssr_remote/StatusCard" { + import type { ComponentType } from "react"; + + const StatusCard: ComponentType; + export default StatusCard; +} diff --git a/apps/vite-host/src/routeTree.gen.ts b/apps/vite-host/src/routeTree.gen.ts new file mode 100644 index 0000000..dceedff --- /dev/null +++ b/apps/vite-host/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/apps/vite-host/src/router.tsx b/apps/vite-host/src/router.tsx new file mode 100644 index 0000000..f8bf919 --- /dev/null +++ b/apps/vite-host/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: "intent", + scrollRestoration: true, + }); +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/apps/vite-host/src/routes/__root.tsx b/apps/vite-host/src/routes/__root.tsx new file mode 100644 index 0000000..44dbaf4 --- /dev/null +++ b/apps/vite-host/src/routes/__root.tsx @@ -0,0 +1,40 @@ +import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router"; +import { HostContext } from "example-host-context"; +import type { ReactNode } from "react"; +import stylesHref from "../styles.css?url"; + +export const Route = createRootRoute({ + head: () => ({ + links: [{ rel: "stylesheet", href: stylesHref }], + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + { title: "TanStack Start federation Vite host" }, + ], + }), + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + + + ); +} + +function RootDocument({ children }: { children: ReactNode }) { + return ( + + + + + + {children} + + + + ); +} diff --git a/apps/vite-host/src/routes/index.tsx b/apps/vite-host/src/routes/index.tsx new file mode 100644 index 0000000..d889520 --- /dev/null +++ b/apps/vite-host/src/routes/index.tsx @@ -0,0 +1,89 @@ +import { getRemoteStylesheets, lazyRemote } from "@module-federation/tanstack/runtime"; +import { createFileRoute } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { RemoteBoundary } from "../components/RemoteBoundary"; + +// Unlike React.lazy, lazyRemote loads again after a failure, so a server that hit a remote +// outage renders the remote once it is back. +const RemoteStatusCard = lazyRemote(() => import("tanstack_vite_remote/StatusCard")); +const RsbuildStatusCard = lazyRemote(() => import("tanstack_rsbuild_remote/StatusCard")); +const RsbuildSsrStatusCard = lazyRemote(() => import("tanstack_rsbuild_ssr_remote/StatusCard")); + +// Manifests of the remotes this route renders on the server. +const serverRenderedManifests = [ + "http://127.0.0.1:3001/mf-manifest.json", + "http://127.0.0.1:3004/mf-manifest.json", +]; + +export const Route = createFileRoute("/")({ + // Loading the server-rendered remotes before render puts their markup in the response. + // An unavailable remote must not fail the route: its boundary renders a fallback. + loader: async () => { + const [stylesheets] = await Promise.all([ + Promise.all( + serverRenderedManifests.map((manifest) => + getRemoteStylesheets(manifest, "./StatusCard").catch(() => []), + ), + ).then((lists) => lists.flat()), + import("tanstack_vite_remote/StatusCard").catch(() => null), + import("tanstack_rsbuild_ssr_remote/StatusCard").catch(() => null), + ]); + return { stylesheets }; + }, + // The remote markup arrives with the HTML, so its stylesheets must too. Otherwise it stays + // unstyled until the remote's JavaScript loads. + head: ({ loaderData }) => ({ + links: loaderData?.stylesheets.map((href) => ({ href, rel: "stylesheet" })), + }), + component: Home, +}); + +function Home() { + return ( +
+
+

TanStack Start × Module Federation

+

Two full-stack apps. One React tree.

+

+ This page belongs to the Vite host on port 3000. The Vite remote on port 3001 renders its + card on the server; the Rsbuild remote on port 3002 loads in the browser after hydration. +

+
+ +
+
+ 01 +

Host route

+

Rendered by TanStack Start

+

The route, document shell, and server response come from the host app.

+
+ + + + + + + + + + +
+
+ ); +} + +function RsbuildRemoteSlot() { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return
Rsbuild remote loads after hydration
; + + return ( + + + + ); +} diff --git a/apps/vite-host/src/styles.css b/apps/vite-host/src/styles.css new file mode 100644 index 0000000..47b0470 --- /dev/null +++ b/apps/vite-host/src/styles.css @@ -0,0 +1,139 @@ +:root { + color: #e9efe9; + background: #101411; + font-family: "Avenir Next", "Trebuchet MS", sans-serif; + font-synthesis: none; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: + linear-gradient(rgba(143, 255, 184, 0.045) 1px, transparent 1px), + linear-gradient(90deg, rgba(143, 255, 184, 0.045) 1px, transparent 1px), + radial-gradient(circle at 82% 8%, rgba(62, 133, 91, 0.24), transparent 35%), #101411; + background-size: + 40px 40px, + 40px 40px, + auto; +} + +button { + font: inherit; +} + +.page-shell { + width: min(1120px, calc(100% - 40px)); + margin: 0 auto; + padding: 88px 0 72px; +} + +.hero { + max-width: 820px; + animation: rise 520ms ease-out both; +} + +.eyebrow, +.card-label, +.card-index { + font-family: "SFMono-Regular", Consolas, monospace; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.eyebrow { + color: #8fffb8; + font-size: 0.78rem; +} + +h1 { + margin: 18px 0 20px; + max-width: 760px; + font-size: clamp(3rem, 8vw, 6.8rem); + line-height: 0.94; + letter-spacing: -0.065em; +} + +.lede { + max-width: 650px; + color: #aeb9b1; + font-size: 1.08rem; + line-height: 1.7; +} + +.proof-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; + margin-top: 64px; +} + +.host-card, +.remote-loading { + min-height: 300px; + border: 1px solid #344239; + padding: 30px; +} + +.host-card { + position: relative; + background: rgba(20, 28, 23, 0.82); +} + +.card-index { + position: absolute; + top: 24px; + right: 26px; + color: #617066; + font-size: 0.72rem; +} + +.card-label { + margin: 0 0 70px; + color: #8fffb8; + font-size: 0.72rem; +} + +.host-card h2 { + margin: 0 0 14px; + font-size: 1.8rem; +} + +.host-card p:last-child { + max-width: 390px; + color: #96a39a; + line-height: 1.65; +} + +.remote-loading { + display: grid; + place-items: center; + color: #8fffb8; + font-family: "SFMono-Regular", Consolas, monospace; +} + +@keyframes rise { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 760px) { + .page-shell { + padding-top: 56px; + } + + .proof-grid { + grid-template-columns: 1fr; + } +} diff --git a/apps/vite-host/tsconfig.json b/apps/vite-host/tsconfig.json new file mode 100644 index 0000000..189cae2 --- /dev/null +++ b/apps/vite-host/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node", "vite/client"], + "verbatimModuleSyntax": true + } +} diff --git a/apps/vite-host/vite.config.ts b/apps/vite-host/vite.config.ts new file mode 100644 index 0000000..805fee3 --- /dev/null +++ b/apps/vite-host/vite.config.ts @@ -0,0 +1,45 @@ +import { tanstackStartModuleFederation } from "@module-federation/tanstack"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + tanstackStartModuleFederation({ + name: "tanstack_vite_host", + dts: false, + shared: { + // The root route renders the provider synchronously, so a Vite host must provide + // the package eagerly. "workspace:*" is not a semver range, hence requiredVersion. + "example-host-context": { eager: true, requiredVersion: "*", singleton: true }, + }, + remotes: { + tanstack_vite_remote: { + type: "module", + name: "tanstack_vite_remote", + entry: "http://127.0.0.1:3001/remoteEntry.js", + }, + tanstack_rsbuild_remote: { + type: "global", + name: "tanstack_rsbuild_remote", + entry: "http://127.0.0.1:3002/mf-manifest.json", + }, + tanstack_rsbuild_ssr_remote: { + type: "global", + name: "tanstack_rsbuild_ssr_remote", + entry: "http://127.0.0.1:3004/mf-manifest.json", + }, + }, + }), + tanstackStart(), + react(), + ], + server: { + port: 3000, + }, + ssr: { + optimizeDeps: { + include: ["react", "react-dom"], + }, + }, +}); diff --git a/apps/vite-remote/package.json b/apps/vite-remote/package.json new file mode 100644 index 0000000..87f7d6a --- /dev/null +++ b/apps/vite-remote/package.json @@ -0,0 +1,30 @@ +{ + "name": "tanstack-start-vite-remote", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite dev --port 3001 --strictPort", + "start": "vite --host 127.0.0.1 --port 3001 --strictPort", + "preview": "vite preview --host 127.0.0.1 --port 3001 --strictPort", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@module-federation/runtime": "2.9.2", + "@tanstack/react-router": "1.170.41", + "@tanstack/react-start": "1.168.60", + "example-host-context": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@module-federation/tanstack": "workspace:*", + "@types/node": "26.6.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.2", + "typescript": "7.0.2", + "vite": "8.3.2" + } +} diff --git a/apps/vite-remote/src/components/StatusCard.css b/apps/vite-remote/src/components/StatusCard.css new file mode 100644 index 0000000..c8ab085 --- /dev/null +++ b/apps/vite-remote/src/components/StatusCard.css @@ -0,0 +1,77 @@ +.remote-card { + position: relative; + min-height: 300px; + border: 1px solid #7b4d26; + background: #241b14; + color: #e9efe9; + padding: 30px; + box-shadow: 14px 14px 0 #0a0d0b; + font-family: "Avenir Next", "Trebuchet MS", sans-serif; +} + +.remote-card .card-index, +.remote-card .card-label, +.remote-card .remote-status, +.remote-card button { + font-family: "SFMono-Regular", Consolas, monospace; +} + +.remote-card .card-index, +.remote-card .card-label, +.remote-card .remote-status { + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.remote-card .card-index { + position: absolute; + top: 24px; + right: 26px; + color: #8f6d4e; + font-size: 0.72rem; +} + +.remote-card .card-label { + margin: 0 0 58px; + color: #f4a95f; + font-size: 0.75rem; +} + +.remote-card .remote-status { + display: flex; + align-items: center; + gap: 8px; + color: #c4cfca; + font-size: 0.66rem; +} + +.remote-card .status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: #8fffb8; + box-shadow: 0 0 10px rgba(143, 255, 184, 0.72); +} + +.remote-card h2 { + margin: 22px 0 12px; + font-size: 1.8rem; +} + +.remote-card > p:not(.card-label) { + color: #aeb9b1; + line-height: 1.65; +} + +.remote-card button { + margin-top: 24px; + border: 0; + background: #f4a95f; + color: #17120e; + cursor: pointer; + padding: 12px 16px; +} + +.remote-card button:hover { + background: #ffc27f; +} diff --git a/apps/vite-remote/src/components/StatusCard.tsx b/apps/vite-remote/src/components/StatusCard.tsx new file mode 100644 index 0000000..d60e898 --- /dev/null +++ b/apps/vite-remote/src/components/StatusCard.tsx @@ -0,0 +1,30 @@ +import { useHostName } from "example-host-context"; +import { useEffect, useState } from "react"; +import "./StatusCard.css"; + +export default function StatusCard() { + const hostName = useHostName(); + const [count, setCount] = useState(0); + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + setHydrated(true); + }, []); + + return ( +
+ 02 +

Federated route module

+
+ + {hydrated ? "Hydrated on the host" : "Rendered on the server"} +
+

Owned by the remote app

+

This component crossed an application boundary while React stayed a shared singleton.

+

Host: {hostName}

+ +
+ ); +} diff --git a/apps/vite-remote/src/routeTree.gen.ts b/apps/vite-remote/src/routeTree.gen.ts new file mode 100644 index 0000000..dceedff --- /dev/null +++ b/apps/vite-remote/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/apps/vite-remote/src/router.tsx b/apps/vite-remote/src/router.tsx new file mode 100644 index 0000000..f8bf919 --- /dev/null +++ b/apps/vite-remote/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: "intent", + scrollRestoration: true, + }); +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/apps/vite-remote/src/routes/__root.tsx b/apps/vite-remote/src/routes/__root.tsx new file mode 100644 index 0000000..987b3fc --- /dev/null +++ b/apps/vite-remote/src/routes/__root.tsx @@ -0,0 +1,37 @@ +import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router"; +import type { ReactNode } from "react"; +import stylesHref from "../styles.css?url"; + +export const Route = createRootRoute({ + head: () => ({ + links: [{ rel: "stylesheet", href: stylesHref }], + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + { title: "TanStack Start federation Vite remote" }, + ], + }), + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + ); +} + +function RootDocument({ children }: { children: ReactNode }) { + return ( + + + + + + {children} + + + + ); +} diff --git a/apps/vite-remote/src/routes/index.tsx b/apps/vite-remote/src/routes/index.tsx new file mode 100644 index 0000000..b1a59cd --- /dev/null +++ b/apps/vite-remote/src/routes/index.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from "@tanstack/react-router"; +import StatusCard from "../components/StatusCard"; + +export const Route = createFileRoute("/")({ + component: Home, +}); + +function Home() { + return ( +
+
+

Standalone remote

+

This is a complete TanStack Start app.

+

+ It owns a route and a server build, then exposes the same component to the host through + Module Federation. +

+
+ +
+ ); +} diff --git a/apps/vite-remote/src/styles.css b/apps/vite-remote/src/styles.css new file mode 100644 index 0000000..2db3a3d --- /dev/null +++ b/apps/vite-remote/src/styles.css @@ -0,0 +1,59 @@ +:root { + color: #e9efe9; + background: #101411; + font-family: "Avenir Next", "Trebuchet MS", sans-serif; + font-synthesis: none; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: radial-gradient(circle at 75% 12%, rgba(231, 155, 76, 0.2), transparent 32%), #101411; +} + +button { + font: inherit; +} + +.remote-page { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr); + align-items: center; + gap: 70px; + width: min(1050px, calc(100% - 40px)); + min-height: 100vh; + margin: 0 auto; +} + +.eyebrow { + font-family: "SFMono-Regular", Consolas, monospace; + color: #f4a95f; + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +h1 { + margin: 18px 0 20px; + font-size: clamp(3rem, 6vw, 5.8rem); + line-height: 0.96; + letter-spacing: -0.06em; +} + +.lede { + color: #aeb9b1; + line-height: 1.65; +} + +@media (max-width: 780px) { + .remote-page { + grid-template-columns: 1fr; + gap: 40px; + padding: 56px 0; + } +} diff --git a/apps/vite-remote/tsconfig.json b/apps/vite-remote/tsconfig.json new file mode 100644 index 0000000..189cae2 --- /dev/null +++ b/apps/vite-remote/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node", "vite/client"], + "verbatimModuleSyntax": true + } +} diff --git a/apps/vite-remote/vite.config.ts b/apps/vite-remote/vite.config.ts new file mode 100644 index 0000000..ca36c2a --- /dev/null +++ b/apps/vite-remote/vite.config.ts @@ -0,0 +1,34 @@ +import { tanstackStartModuleFederation } from "@module-federation/tanstack"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + tanstackStartModuleFederation({ + name: "tanstack_vite_remote", + dts: false, + // Workspace dependencies declare "workspace:*", which is not a semver range. + shared: { "example-host-context": { requiredVersion: "*", singleton: true } }, + exposes: { + "./StatusCard": "./src/components/StatusCard.tsx", + }, + }), + tanstackStart(), + react(), + ], + server: { + port: 3001, + cors: true, + origin: "http://127.0.0.1:3001", + }, + preview: { + port: 3001, + cors: true, + }, + ssr: { + optimizeDeps: { + include: ["react", "react-dom"], + }, + }, +}); diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..1909bc4 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,77 @@ +--- +title: Releasing +summary: Release and trusted npm publishing for @module-federation/tanstack. +read_when: + - Preparing a release + - Updating release automation +updated_at: 2026-10-07 +--- + +# Releasing + +Add a Changeset to every user-visible package change: + +```bash +pnpm changeset +``` + +Run the complete local gate before merging: + +```bash +pnpm install --frozen-lockfile +pnpm format:check +pnpm typecheck +pnpm build +pnpm test +pnpm pack:tanstack +``` + +The `Release Pull Request` workflow versions the package and updates its +changelog. After merging that PR, publish from `main` with a GitHub Release +whose tag exactly matches `packages/tanstack/package.json` (without a `v`), or +dispatch `Release` with `version=latest`. Preview dispatches use `version=next` +and publish to npm's `next` dist-tag. + +GitHub prerelease tags must match the prerelease version already committed in +`packages/tanstack/package.json`. The workflow publishes that exact version to +the `next` dist-tag. + +The publish workflow reruns the gate, checks that the source is reachable from +`main`, uses npm provenance, and refuses to republish an existing version under +a different dist-tag. It publishes through npm trusted publishing for +repository `module-federation/tanstack`, workflow `release.yml`, environment +`Publish`, so the repository needs a `Publish` environment. + +## First publish + +npm only lets a package that already exists name a trusted publisher, so the +workflow cannot publish `@module-federation/tanstack` the first time. Bootstrap +it once, as a maintainer with publish rights in the `@module-federation` npm +organization: + +1. Publish a placeholder from an empty directory: a `package.json` with + `"name": "@module-federation/tanstack"` and `"version": "0.0.0"`, then + `npm publish --access public`. +2. Add the trusted publisher on npmjs.com (package settings, "Trusted + Publisher", GitHub Actions) or with npm 11.10+: + `npm trust github @module-federation/tanstack --file release.yml --repo module-federation/tanstack --env Publish --allow-publish`. +3. Release `0.1.0` through the workflow within two days: npm discards a trusted + publisher configuration that has not published by then. +4. Deprecate the placeholder: + `npm deprecate @module-federation/tanstack@0.0.0 "Placeholder; use 0.1.0 or later."` + +Trusted publishing needs npm 11.5.1+ on Node 22.14+; the workflow's Node 24 +includes it. Releases after `0.1.0` need no manual step. + +The release gate builds the six example apps and checks their manifests and +server bundles. It then runs the apps twice, as development servers and as +production builds, and drives every host in headless Chromium: server-rendered +remote markup for every Vite and Rsbuild host and remote pair (including +concurrent first requests), remote stylesheets in the server response, +hydration, remote interactivity, one shared React instance and context, no +console errors, reachable manifest assets, the fallback and recovery when +remotes go offline, and hosts that start during an outage rendering the remotes +once they return. + +The test suite needs Chromium. Locally, run `pnpm exec playwright install +chromium` once; the workflows install it before `pnpm test`. diff --git a/package.json b/package.json new file mode 100644 index 0000000..6ad1669 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "mf-tanstack", + "private": true, + "packageManager": "pnpm@12.9.1", + "engines": { + "node": "^22.18.0 || ^24.11.0 || >=26.0.0" + }, + "scripts": { + "build": "turbo run build", + "build:package": "pnpm --filter @module-federation/tanstack build", + "dev": "pnpm build:package && turbo run dev", + "test": "pnpm build && node --test --test-concurrency=1 test/*.test.mjs", + "typecheck": "turbo run typecheck", + "format": "prettier --write .", + "format:check": "prettier --check .", + "pack:tanstack": "pnpm --filter @module-federation/tanstack pack:dry", + "start": "pnpm build:package && turbo run start", + "preview": "pnpm build && turbo run preview", + "changeset": "changeset", + "changeset:status": "changeset status" + }, + "devDependencies": { + "@changesets/cli": "3.0.3", + "playwright": "1.63.0", + "prettier": "3.9.9", + "turbo": "2.11.7", + "typescript": "7.0.2" + } +} diff --git a/packages/tanstack/CHANGELOG.md b/packages/tanstack/CHANGELOG.md new file mode 100644 index 0000000..8486c6e --- /dev/null +++ b/packages/tanstack/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +## 0.1.0 + +Initial release: Module Federation for TanStack Start on Vite and Rsbuild. + +- `@module-federation/tanstack/vite` (also the package root) wraps + `@module-federation/vite` with TanStack Start defaults: `remoteEntry.js`, a + manifest, entry-level host init, and environment-specific build targets. + Hosts share eager singleton `react` and `react-dom`, so a remote built by + another bundler cannot replace the host's React instance. It requires Vite 8 + and stops with an error on older versions. +- `@module-federation/tanstack/rsbuild` wraps + `@module-federation/rsbuild-plugin` with script-compatible browser output, + async startup, role-aware React sharing, and `publicPath: "auto"` for + remotes, so production remotes load from their own origin. +- Requires `@module-federation/vite` 1.23.3, which fixes federated SSR of + CommonJS server containers and retries failed server-side remote loads. +- Rsbuild SSR federation with `server: true`: remotes ship a Node container, + `remoteEntry.ssr.cjs`, with their browser assets and advertise it in their + manifest; hosts load remotes on the server from an async-node CommonJS build + that keeps `dist/server/index.js` as its entry. +- Federated SSR for every host and remote pair: Vite hosts render Rsbuild + remotes built with `server: true`, and Rsbuild hosts render Vite remotes + through `@module-federation/vite`'s SSR loader. +- `@module-federation/tanstack/node-entry-loader`: the runtime plugin Rsbuild + hosts use to load remote server entries. Failures throw `RemoteEntryError` + with the failing phase and the original cause. +- `@module-federation/tanstack/runtime`: `lazyRemote`, a `React.lazy` that + loads a failed remote again instead of failing until the server restarts, and + `getRemoteStylesheets`, which reads a remote's stylesheets from its manifest + so server-rendered remote markup is styled before JavaScript loads. +- Hosts that hit a remote outage, including at startup, render the remote again + once it returns, and every remote also runs as a standalone app. +- Tested in development and production builds on Node 22.18, 24, and 26, with + Vite 8.3, Rsbuild 2.2, TanStack Start 1.168, and React 19.3. diff --git a/packages/tanstack/LICENSE b/packages/tanstack/LICENSE new file mode 100644 index 0000000..b0cebd8 --- /dev/null +++ b/packages/tanstack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Module Federation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/tanstack/README.md b/packages/tanstack/README.md new file mode 100644 index 0000000..f54143b --- /dev/null +++ b/packages/tanstack/README.md @@ -0,0 +1,224 @@ +# @module-federation/tanstack + +TanStack Start integration for Module Federation on Vite and Rsbuild. The +adapter packages are optional peers, so importing one adapter does not resolve +the other. + +## Support status + +| Host | Remote | Browser federation | Federated SSR | +| ------- | ------- | ------------------ | ---------------------------------- | +| Vite | Vite | Supported | Supported | +| Vite | Rsbuild | Supported | Supported (remote: `server: true`) | +| Rsbuild | Vite | Supported | Supported (host: `server: true`) | +| Rsbuild | Rsbuild | Supported | Supported (both: `server: true`) | + +"Federated SSR" means the remote component is rendered into the host's initial +HTML. Every row is covered by browser tests against both development servers +and production builds: the remote hydrates, shares the host's React and +context, responds to clicks, logs no errors, and the host falls back and +recovers when the remote goes offline. + +## Vite + +```ts +import { tanstackStartModuleFederation } from "@module-federation/tanstack/vite"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + tanstackStartModuleFederation({ + name: "host", + remotes: { + remote: { + type: "module", + name: "remote", + entry: "https://example.test/remoteEntry.js", + }, + }, + }), + tanstackStart(), + react(), + ], +}); +``` + +Place the federation wrapper before `tanstackStart()`, followed by the React +plugin. The root import (`@module-federation/tanstack`) is the same Vite +adapter. + +Defaults: + +- `filename: "remoteEntry.js"`, `manifest: true`, and + `hostInitInjectLocation: "entry"`. +- `react` and `react-dom` are shared singletons. When the config declares + `remotes`, they are also `eager`, so the host's own React stays in the share + scope and a remote built by another bundler cannot replace it. Remote-only + builds stay lazy. +- Any entry you pass in `shared` replaces the default for that package. + +A Vite host renders before lazy shared modules resolve, so mark any other shared +package the host imports from its entry or root route as `eager: true`, as the +wrapper does for React. Remotes can keep their shares lazy. + +The wrapper omits the global Module Federation `target` option. TanStack +Start's client and SSR environments need `@module-federation/vite` to select +`web` and `node` independently. + +The Vite adapter requires Vite 8, like TanStack Start's own examples and +`@vitejs/plugin-react` 6. It stops with an error on older versions, where +`@module-federation/vite` cannot load remotes on the server. + +A Vite host renders Rsbuild remotes on the server when they are built with +`server: true` (see below). Declare such a remote by its manifest, so the +server can find the Node container: + +```ts +remotes: { + rsbuild_remote: { + type: "global", + name: "rsbuild_remote", + entry: "https://example.test/mf-manifest.json", + }, +}, +``` + +## Rsbuild + +```ts +import { defineConfig } from "@rsbuild/core"; +import { pluginReact } from "@rsbuild/plugin-react"; +import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild"; +import { tanstackStartModuleFederation } from "@module-federation/tanstack/rsbuild"; + +export default defineConfig({ + plugins: [ + pluginReact(), + tanstackStart(), + ...tanstackStartModuleFederation({ + federation: { + name: "remote", + exposes: { "./StatusCard": "./src/StatusCard.tsx" }, + }, + }), + ], +}); +``` + +The adapter returns an array of Rsbuild plugins; spread it into `plugins`, after +`tanstackStart()`. It emits script-compatible browser output, so the manifest +can be consumed by Vite and Rsbuild hosts. + +Defaults: + +- `react` and `react-dom` are shared singletons, `eager` for hosts (configs with + `remotes`) and lazy for remote-only builds. +- `experiments.asyncStartup: true`, so entries wait for shared modules before + running. Pass `experiments: { asyncStartup: false }` to opt out. +- A remote's browser build uses `publicPath: "auto"`, so hosts on other origins + load its chunks from the remote. TanStack Start otherwise derives `/` from + `server.base`. An absolute asset prefix, such as a CDN URL, is kept. + +For hosts, set `shareStrategy: "loaded-first"`. With the default +`version-first` strategy, the host fetches every remote's manifest during +startup, and a single offline remote fails the whole page. + +### SSR federation + +Pass `server: true` (or an options object) to federate on the server: + +- **Remotes** (`exposes`) build a Node container, `remoteEntry.ssr.cjs`, next to + `remoteEntry.js` in `dist/client` (its chunks go in `dist/client/ssr/`), and + advertise it in `mf-manifest.json` as `ssrRemoteEntry`. Deploy `dist/client` + as usual; the container holds only the exposed modules, never the app's + server code. The remote's own server output is unchanged. Vite and Rsbuild + hosts can both render it. +- **Hosts** (`remotes`) load remotes on the server through the Module + Federation Node runtime. TanStack Start's server output becomes async-node + CommonJS (`dist/server/index.cjs`), with `dist/server/index.js` re-exporting + it, so `rsbuild preview` and deployments that import `index.js` keep working. + The dev server writes its server bundle to + `node_modules/.cache/tanstack-start-federation/` instead, so a production + build in `dist/server` survives development sessions. Rsbuild remotes need + `server: true` too. Vite remotes need nothing extra on + their side, but the host must have `@module-federation/vite` (and its `vite` + peer) installed: their server entries are ES modules, loaded by that + package's SSR loader. + +When a remote's server entry cannot load, the host's runtime plugin throws a +`RemoteEntryError` whose `phase` is `fetch`, `evaluate`, or `loader` and whose +`cause` is the original error. + +## Rendering remotes on the server + +Hosts on either bundler render remotes the same way: load the remote in the +route `loader`, render it with `lazyRemote` inside a Suspense and error +boundary, and link its stylesheets from the route's `head`: + +```tsx +import { getRemoteStylesheets, lazyRemote } from "@module-federation/tanstack/runtime"; +import { createFileRoute } from "@tanstack/react-router"; + +const StatusCard = lazyRemote(() => import("remote/StatusCard")); + +export const Route = createFileRoute("/")({ + // Loading the remote before rendering puts its markup in the server response. + // A failed load must not fail the route: the boundary renders a fallback. + loader: async () => { + const [stylesheets] = await Promise.all([ + getRemoteStylesheets("https://remote.test/mf-manifest.json", "./StatusCard").catch(() => []), + import("remote/StatusCard").catch(() => null), + ]); + return { stylesheets }; + }, + head: ({ loaderData }) => ({ + links: loaderData?.stylesheets.map((href) => ({ href, rel: "stylesheet" })), + }), + component: () => ( + + + + ), +}); +``` + +`RemoteBoundary` stands for any error boundary around a `Suspense`; the +examples in `apps/` include one. + +- **`lazyRemote(load, { retryAfterMs })`** is `React.lazy` for remotes. A + `React.lazy` component that fails once stays failed, so a server that hit a + remote outage would render the fallback until it restarts. `lazyRemote` loads + again on the first render after `retryAfterMs` (5 seconds by default). The + import itself loads again after a failure too: `@module-federation/vite` + 1.23.3+ retries failed server loads, and the Rsbuild adapter removes a failed + remote module from Rspack's module cache. +- **`getRemoteStylesheets(manifestUrl, expose, { maxAgeMs })`** lists the + stylesheets the remote's manifest declares for an expose, as absolute URLs. A + remote's CSS otherwise loads with its JavaScript, after the HTML, so the + server-rendered markup would be unstyled until then. It works in the browser + and on the server, reuses a fetched manifest for 30 seconds (`maxAgeMs`), and + rejects when the manifest is unreachable or does not list the expose. + +## Known limitations + +- In development, a Vite remote's server modules import React from the files + its dev server resolves, so host and remote must resolve React to the same + files, as they do in a monorepo. Production builds take React from the + host's share scope. +- A Vite dev server injects CSS from JavaScript, so its manifest lists no + stylesheets and `getRemoteStylesheets` returns none for it in development. + +## Requirements + +- Node 22.18+, 24.11+, or 26+. +- TanStack Start 1.167.43+ (the first release with the Rsbuild integration). +- React 19, which the tests use. TanStack Start also accepts React 18, but as + of 1.168 its Rsbuild server build fails with it: TanStack Router imports + React's `use`, which React 18 lacks. React 18 with Vite builds but is + untested. +- Vite: `@module-federation/vite` 1.23.3+ and `vite` 8. +- Rsbuild: `@module-federation/rsbuild-plugin` and `@rsbuild/core`. An Rsbuild + host that renders Vite remotes on the server also needs + `@module-federation/vite` 1.23.3+ and `vite`. diff --git a/packages/tanstack/package.json b/packages/tanstack/package.json new file mode 100644 index 0000000..a9734d1 --- /dev/null +++ b/packages/tanstack/package.json @@ -0,0 +1,142 @@ +{ + "name": "@module-federation/tanstack", + "version": "0.1.0", + "type": "module", + "description": "TanStack Start integration for Module Federation", + "license": "MIT", + "sideEffects": false, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./vite": { + "import": { + "types": "./dist/vite.d.ts", + "default": "./dist/vite.js" + }, + "require": { + "types": "./dist/vite.d.cts", + "default": "./dist/vite.cjs" + } + }, + "./rsbuild": { + "import": { + "types": "./dist/rsbuild.d.ts", + "default": "./dist/rsbuild.js" + }, + "require": { + "types": "./dist/rsbuild.d.cts", + "default": "./dist/rsbuild.cjs" + } + }, + "./node-entry-loader": { + "import": { + "types": "./dist/node-entry-loader.d.ts", + "default": "./dist/node-entry-loader.js" + }, + "require": { + "types": "./dist/node-entry-loader.d.cts", + "default": "./dist/node-entry-loader.cjs" + } + }, + "./runtime": { + "import": { + "types": "./dist/runtime.d.ts", + "default": "./dist/runtime.js" + }, + "require": { + "types": "./dist/runtime.d.cts", + "default": "./dist/runtime.cjs" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/**/*", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], + "publishConfig": { + "access": "public", + "provenance": true + }, + "repository": { + "type": "git", + "url": "git+https://github.com/module-federation/tanstack.git", + "directory": "packages/tanstack" + }, + "bugs": { + "url": "https://github.com/module-federation/tanstack/issues" + }, + "homepage": "https://github.com/module-federation/tanstack#readme", + "keywords": [ + "tanstack", + "tanstack-start", + "module-federation", + "microfrontend" + ], + "engines": { + "node": "^22.18.0 || ^24.11.0 || >=26.0.0" + }, + "scripts": { + "build": "rslib build --lib cjs && rslib build --lib esm --no-clean", + "dev": "rslib build --watch", + "pack:dry": "npm pack --dry-run", + "prepack": "pnpm run build", + "prepublishOnly": "pnpm run typecheck && pnpm run build", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "peerDependencies": { + "@module-federation/rsbuild-plugin": "^2.9.0", + "@module-federation/vite": "^1.23.3", + "@rsbuild/core": "^2.0.0", + "@tanstack/react-start": "^1.167.43", + "react": ">=18.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/react-start": { + "optional": true + }, + "@module-federation/rsbuild-plugin": { + "optional": true + }, + "@module-federation/vite": { + "optional": true + }, + "@rsbuild/core": { + "optional": true + }, + "vite": { + "optional": true + }, + "react": { + "optional": true + } + }, + "devDependencies": { + "@module-federation/rsbuild-plugin": "2.9.2", + "@module-federation/runtime": "2.9.2", + "@module-federation/vite": "1.23.3", + "@rsbuild/core": "2.2.11", + "@rslib/core": "1.0.3", + "@tanstack/react-start": "1.168.60", + "@types/node": "26.6.4", + "@types/react": "19.3.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "typescript": "7.0.2", + "vite": "8.3.2" + } +} diff --git a/packages/tanstack/rslib.config.ts b/packages/tanstack/rslib.config.ts new file mode 100644 index 0000000..6ab4262 --- /dev/null +++ b/packages/tanstack/rslib.config.ts @@ -0,0 +1,42 @@ +import { defineConfig } from "@rslib/core"; + +const entry = { + index: "./src/index.ts", + "node-entry-loader": "./src/node-entry-loader.ts", + rsbuild: "./src/rsbuild.ts", + runtime: "./src/runtime.ts", + shared: "./src/shared.ts", + vite: "./src/vite.ts", + "vite-compat": "./src/vite-compat.ts", +}; + +const output = { + distPath: { root: "./dist" }, + sourceMap: { js: "source-map" }, + target: "node", +} as const; + +// The `build` script runs the CJS library first. With TypeScript 7, rsbuild-plugin-dts +// renames every `.d.ts` in `dist` to `.d.cts` during the CJS pass, which would also +// consume the ESM declarations if they were emitted first. +export default defineConfig({ + lib: [ + { + bundle: false, + dts: { autoExtension: true }, + format: "esm", + output, + redirect: { dts: { extension: true } }, + source: { entry }, + syntax: "es2022", + }, + { + bundle: false, + dts: { autoExtension: true }, + format: "cjs", + output, + source: { entry }, + syntax: "es2022", + }, + ], +}); diff --git a/packages/tanstack/src/index.ts b/packages/tanstack/src/index.ts new file mode 100644 index 0000000..24f205b --- /dev/null +++ b/packages/tanstack/src/index.ts @@ -0,0 +1,4 @@ +// The root entrypoint remains Vite-compatible. Rsbuild users should import +// `@module-federation/tanstack/rsbuild` so their installation never resolves +// the Vite adapter. +export { tanstackStartModuleFederation, type TanStackStartModuleFederationOptions } from "./vite"; diff --git a/packages/tanstack/src/node-entry-loader.ts b/packages/tanstack/src/node-entry-loader.ts new file mode 100644 index 0000000..8d48f45 --- /dev/null +++ b/packages/tanstack/src/node-entry-loader.ts @@ -0,0 +1,269 @@ +import type { ModuleFederationRuntimePlugin } from "@module-federation/runtime"; + +type LoadEntryArgs = Parameters>[0]; +type RemoteEntryExports = NonNullable; +type RemoteInfo = LoadEntryArgs["remoteInfo"]; +type FederationHost = LoadEntryArgs["origin"]; +type NodeModule = typeof import("node:module"); + +/** A remote's server entry failed to load. `phase` says which step failed; `cause` keeps the original error. */ +export class RemoteEntryError extends Error { + readonly phase: "fetch" | "evaluate" | "loader"; + readonly remote: string; + readonly url: string; + + constructor( + message: string, + options: { cause?: unknown; phase: RemoteEntryError["phase"]; remote: string; url: string }, + ) { + super(message, { cause: options.cause }); + this.name = "RemoteEntryError"; + this.phase = options.phase; + this.remote = options.remote; + this.url = options.url; + } +} + +/** + * Loads remote server entries for an Rsbuild host's server bundle. + * + * - CommonJS containers (Rsbuild remotes) are fetched and compiled here as CommonJS + * modules, so their own `import()` calls work. Module Federation's default Node loader + * evaluates them with `vm`, which Rsbuild's development runner cannot support. + * - ES module entries (Vite remotes) go to `@module-federation/vite`'s SSR loader, which + * handles both production builds and Vite dev servers. It is loaded with Node's + * `require`, not the bundle's, so its `import()` calls work in Rsbuild's runner too. + */ +export default function nodeEntryLoaderPlugin(): ModuleFederationRuntimePlugin { + return { + name: "tanstack-start-node-entry-loader", + async loadEntry(args) { + const { remoteInfo } = args; + if (!isNodeServer() || !/^https?:\/\//.test(remoteInfo.entry)) return undefined; + if (remoteInfo.type === "commonjs-module") return loadCommonJsEntry(remoteInfo); + if (remoteInfo.type === "module" || remoteInfo.type === "esm") return loadModuleEntry(args); + return undefined; + }, + }; +} + +// Matches @module-federation/vite's SSR fetch timeout. +const FETCH_TIMEOUT_MS = 10_000; + +function isNodeServer() { + return typeof process !== "undefined" && typeof process.getBuiltinModule === "function"; +} + +function nodeModule() { + return process.getBuiltinModule("node:module") as NodeModule; +} + +/** Requires from the app, because the host's server bundle has no node_modules of its own. */ +function appRequires() { + const { createRequire } = nodeModule(); + const path = process.getBuiltinModule("node:path") as typeof import("node:path"); + // The app root first, then the entry script for servers started from elsewhere. + return [path.join(process.cwd(), "package.json"), process.argv[1]] + .filter((base): base is string => Boolean(base)) + .map((base) => createRequire(base)); +} + +async function loadCommonJsEntry({ entry, entryGlobalName, name }: RemoteInfo) { + let source: string; + try { + const response = await fetch(entry, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`.trim()); + source = await response.text(); + } catch (cause) { + throw new RemoteEntryError( + `Could not fetch the server entry of remote "${name}" from ${entry}.`, + { + cause, + phase: "fetch", + remote: name, + url: entry, + }, + ); + } + + let exports: Record; + try { + const Module = nodeModule() as unknown as new (id: string) => CompilableModule; + const container = new Module(entry); + container.filename = entry; + container.paths = appRequires()[0]?.resolve.paths("react") ?? []; + // What `require()` does with a file's source: Node compiles it as a CommonJS module + // whose `import()` calls use Node's own loader. + container._compile(source, entry, "commonjs"); + exports = container.exports as Record; + } catch (cause) { + throw new RemoteEntryError(`The server entry of remote "${name}" threw while loading.`, { + cause, + phase: "evaluate", + remote: name, + url: entry, + }); + } + + const remoteEntry = (entryGlobalName && exports[entryGlobalName]) || exports; + if (!isRemoteEntry(remoteEntry)) { + throw new RemoteEntryError( + `The server entry of remote "${name}" did not export a federation container.`, + { phase: "evaluate", remote: name, url: entry }, + ); + } + return remoteEntry; +} + +interface CompilableModule { + exports: unknown; + filename: string; + paths: string[]; + _compile(source: string, filename: string, format?: "commonjs"): void; +} + +function isRemoteEntry(value: unknown): value is RemoteEntryExports { + const candidate = value as Partial | undefined; + return typeof candidate?.get === "function" && typeof candidate.init === "function"; +} + +// Packages a Vite remote's server code may import directly, besides the host's shared +// packages. Resolving them from the host keeps one copy of each in the process. +const VITE_SHARED_PACKAGES = [ + "@module-federation/runtime", + "@module-federation/runtime-core", + "@module-federation/sdk", + "react", + "react-dom", + "react/compiler-runtime", + "react/jsx-dev-runtime", + "react/jsx-runtime", +]; + +type ViteSsrEntryLoaderModule = { + default: (options: { resolvedShared: Record }) => ModuleFederationRuntimePlugin; + SsrEntryHttpError?: new (...args: never[]) => Error; +}; + +type ViteLoader = { + httpError?: ViteSsrEntryLoaderModule["SsrEntryHttpError"]; + plugin: ModuleFederationRuntimePlugin; + require: NodeJS.Require; + resolvedShared: Record; +}; + +let viteLoader: ViteLoader | undefined; + +async function loadModuleEntry(args: LoadEntryArgs) { + const { entry, name } = args.remoteInfo; + viteLoader ??= createViteLoader(args); + // Shared modules the host loaded since the last remote are aligned too. + provideHostModules(args.origin, viteLoader.resolvedShared, viteLoader.require); + + let remoteEntry: RemoteEntryExports | void; + try { + remoteEntry = await viteLoader.plugin.loadEntry?.(args); + } catch (cause) { + const { httpError } = viteLoader; + throw new RemoteEntryError( + `Could not load the server entry of remote "${name}" from ${entry}.`, + { + cause, + phase: httpError && cause instanceof httpError ? "fetch" : "evaluate", + remote: name, + url: entry, + }, + ); + } + // Without an entry the runtime would try its own Node loader, which cannot load ES + // modules without Node flags. Report the failure instead. The Vite loader returns + // nothing for an unreachable remote, so tell that case apart. + if (!remoteEntry) { + const reachable = await fetch(entry, { + method: "HEAD", + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + }).then( + (response) => response.ok, + () => false, + ); + throw new RemoteEntryError( + reachable + ? `@module-federation/vite could not load the server entry of remote "${name}" ` + + `from ${entry}; its warnings have the cause.` + : `Could not reach remote "${name}" at ${entry}.`, + { phase: reachable ? "loader" : "fetch", remote: name, url: entry }, + ); + } + return remoteEntry; +} + +function createViteLoader({ origin, remoteInfo }: LoadEntryArgs): ViteLoader { + const requires = appRequires(); + const resolve = (specifier: string) => { + for (const require of requires) { + try { + return { path: require.resolve(specifier), require }; + } catch { + // Try the next base. + } + } + return undefined; + }; + + const loader = resolve("@module-federation/vite/ssrEntryLoader"); + if (!loader) { + throw new RemoteEntryError( + `Remote "${remoteInfo.name}" is a Vite remote. Add @module-federation/vite to the ` + + "host's dependencies to render it on the server.", + { phase: "loader", remote: remoteInfo.name, url: remoteInfo.entry }, + ); + } + + const resolvedShared: Record = {}; + for (const specifier of [...VITE_SHARED_PACKAGES, ...Object.keys(origin.options.shared)]) { + const resolved = resolve(specifier); + if (resolved) resolvedShared[specifier] = resolved.path; + } + + // The loader is an ES module; Node 22.12+ requires it synchronously. + const loaderModule = loader.require(loader.path) as ViteSsrEntryLoaderModule; + return { + httpError: loaderModule.SsrEntryHttpError, + plugin: loaderModule.default({ resolvedShared }), + require: loader.require, + resolvedShared, + }; +} + +/** + * Vite remotes can import shared packages from node_modules instead of the share scope: + * a Vite dev server's module runner externalizes them, and resolved imports in production + * builds point there. The host bundles its own copies, so this registers them in Node's + * module cache under their node_modules paths. The process then keeps one React. + */ +function provideHostModules( + origin: FederationHost, + resolvedShared: Record, + require: NodeJS.Require, +) { + const Module = nodeModule() as unknown as new (id: string) => CompilableModule & { + loaded: boolean; + }; + for (const scope of Object.values(origin.shareScopeMap)) { + for (const [packageName, versions] of Object.entries(scope)) { + const filename = resolvedShared[packageName]; + // Something already loaded this package from node_modules; nothing to align. + if (!filename || require.cache[filename]) continue; + const provided = Object.values(versions).find( + (shared) => shared.from === origin.name && shared.loaded && shared.lib, + ); + if (!provided?.lib) continue; + + const cached = new Module(filename); + cached.filename = filename; + cached.exports = provided.lib(); + cached.loaded = true; + require.cache[filename] = cached as unknown as NodeJS.Module; + } + } +} diff --git a/packages/tanstack/src/rsbuild.ts b/packages/tanstack/src/rsbuild.ts new file mode 100644 index 0000000..4852b64 --- /dev/null +++ b/packages/tanstack/src/rsbuild.ts @@ -0,0 +1,400 @@ +import { join } from "node:path"; +import { + pluginModuleFederation, + type ModuleFederationOptions, +} from "@module-federation/rsbuild-plugin"; +import type { RsbuildPlugin } from "@rsbuild/core"; +import { defaultShared, eagerShared, resolveShared } from "./shared"; + +type ModuleFederationOverride = Partial; + +type ClientFederationOptions = ModuleFederationOverride & { + chunkLoadingGlobal?: string; + environment?: string; + forceScriptOutput?: boolean; +}; + +type ServerFederationOptions = + | false + | (ModuleFederationOverride & { + chunkFilename?: string; + entryFilename?: string; + environment?: string; + forceCommonJsOutput?: boolean; + }); + +export type TanStackStartRsbuildModuleFederationOptions = { + client?: ClientFederationOptions; + federation: ModuleFederationOptions; + /** + * Server-side federation. Pass `true` or an options object to enable it. Defaults to + * `false`: browser-only federation. + * + * - With `exposes`, the build adds a Node container, `remoteEntry.ssr.cjs`, next to the + * browser entry (its chunks go in `ssr/`) and advertises it in the browser manifest + * as `ssrRemoteEntry`. + * - With `remotes`, TanStack Start's server environment loads remotes through the + * Module Federation Node runtime, as async-node CommonJS (`dist/server/index.cjs`). + */ + server?: ServerFederationOptions | true; +}; + +// The server container sits next to the manifest, like a Vite remote's SSR entry. Hosts +// resolve its chunks from either the container URL (Module Federation's Node runtime) or +// the manifest URL (`@module-federation/vite`), so both must share a directory. The +// extension says the container is CommonJS. +const SERVER_CONTAINER_FILENAME = "remoteEntry.ssr.cjs"; +const SERVER_CHUNK_DIR = "ssr"; +const SERVER_CONTAINER_ENVIRONMENT = "mf-server"; +const NODE_ENTRY_LOADER = "@module-federation/tanstack/node-entry-loader"; + +/** Creates browser and optional SSR federation adapters for TanStack Start's Rsbuild integration. */ +export function tanstackStartModuleFederation({ + client = {}, + federation, + server = false, +}: TanStackStartRsbuildModuleFederationOptions): RsbuildPlugin[] { + const { + chunkLoadingGlobal, + environment: clientEnvironment = "client", + forceScriptOutput = true, + ...clientOverrides + } = client; + const federationName = federation.name; + if (!federationName) throw new Error("federation.name is required"); + + const isHost = hasEntries(federation.remotes); + const isRemote = hasEntries(federation.exposes); + const serverEnabled = server !== false; + const exposesServerContainer = serverEnabled && isRemote; + + const clientFederationOptions = withDefaults( + { ...federation, ...clientOverrides }, + isHost ? eagerShared : defaultShared, + ); + if (exposesServerContainer) advertiseServerContainer(clientFederationOptions); + + const plugins: RsbuildPlugin[] = [ + pluginModuleFederation(clientFederationOptions, { + environment: clientEnvironment, + target: "web", + }), + ...(isHost ? [remoteRetryPlugin()] : []), + clientCompatibilityPlugin({ + chunkLoadingGlobal: chunkLoadingGlobal ?? `chunk_${federationName}`, + environment: clientEnvironment, + forceScriptOutput, + isRemote, + name: federationName, + }), + ]; + + if (!serverEnabled) return plugins; + + const { + chunkFilename, + entryFilename, + environment: serverEnvironment = "ssr", + forceCommonJsOutput = true, + ...serverOverrides + } = server === true ? {} : server; + + if (exposesServerContainer) { + const { remotes: _remotes, ...containerOptions } = { ...federation, ...serverOverrides }; + plugins.push( + serverContainerEnvironmentPlugin({ clientEnvironment }), + pluginModuleFederation( + withDefaults( + { + ...containerOptions, + // The browser build owns the manifest and the federated types. + dev: false, + dts: false, + filename: SERVER_CONTAINER_FILENAME, + manifest: false, + }, + defaultShared, + ), + { environment: SERVER_CONTAINER_ENVIRONMENT, target: "node" }, + ), + ); + } + + if (isHost) { + // The host's own server bundle consumes remotes. Its container, if any, is built by the + // dedicated environment above, so TanStack's server output never contains one. + const { exposes: _exposes, ...hostOptions } = { ...federation, ...serverOverrides }; + plugins.push( + pluginModuleFederation( + { + ...withDefaults(hostOptions, eagerShared), + manifest: false, + runtimePlugins: [...(hostOptions.runtimePlugins ?? []), NODE_ENTRY_LOADER], + }, + { environment: serverEnvironment, target: "node" }, + ), + serverCompatibilityPlugin({ + chunkFilename: + chunkFilename ?? (forceCommonJsOutput ? `[id].${federationName}.cjs` : undefined), + entryFilename: entryFilename ?? (forceCommonJsOutput ? "[name].cjs" : undefined), + environment: serverEnvironment, + forceCommonJsOutput, + }), + ); + } + + return plugins; +} + +export const pluginTanStackStartModuleFederation = tanstackStartModuleFederation; + +function isAbsoluteUrl(value: unknown): boolean { + return typeof value === "string" && /^[a-z][a-z\d+.-]*:\/\//i.test(value); +} + +function hasEntries(value: unknown): boolean { + if (Array.isArray(value)) return value.length > 0; + return Boolean(value) && typeof value === "object" && Object.keys(value as object).length > 0; +} + +function withDefaults( + options: ModuleFederationOptions, + sharedDefaults: Record, +): ModuleFederationOptions { + return { + ...options, + filename: options.filename ?? "remoteEntry.js", + manifest: options.manifest ?? true, + shared: resolveShared(options.shared, sharedDefaults) as ModuleFederationOptions["shared"], + // TanStack Start imports React synchronously from its entries. Async startup waits for + // shared modules before running them, so lazy shares cannot fail with RUNTIME-006. + experiments: { asyncStartup: true, ...options.experiments }, + }; +} + +/** + * Adds the Node container to the browser manifest, as Module Federation's dual-target + * build does. A Node consumer reads `ssrRemoteEntry` and fetches the container from the + * manifest's public path. + */ +function advertiseServerContainer(options: ModuleFederationOptions) { + const manifest = options.manifest === true ? {} : options.manifest; + if (!manifest) return; + + const userAdditionalData = manifest.additionalData; + options.manifest = { + ...manifest, + async additionalData(context) { + const stats = (await userAdditionalData?.(context)) ?? context.stats; + Object.assign(stats.metaData, { + ssrRemoteEntry: { name: SERVER_CONTAINER_FILENAME, path: "", type: "commonjs-module" }, + }); + return stats; + }, + }; +} + +/** Builds the remote's Node container in its own environment, into the browser output. */ +function serverContainerEnvironmentPlugin({ + clientEnvironment, +}: { + clientEnvironment: string; +}): RsbuildPlugin { + return { + name: "tanstack-start-federation-server-container", + setup(api) { + // Module Federation validates its node environment in a `pre` hook, before TanStack + // Start defines its own environments, so this one is created bare and placed next to + // the browser output once the bundler configs exist. + api.modifyRsbuildConfig({ + order: "pre", + handler(config) { + config.environments ??= {}; + config.environments[SERVER_CONTAINER_ENVIRONMENT] = { + // Module Federation adds the container entry; the placeholder keeps the + // environment from bundling the application. + source: { entry: { [SERVER_CONTAINER_ENVIRONMENT]: "data:text/javascript," } }, + // The output directory is shared with the browser build, which cleans it and + // emits the stylesheets. + output: { cleanDistPath: false, emitAssets: false, target: "node" }, + }; + }, + }); + + api.onBeforeCreateCompiler(({ bundlerConfigs }) => { + const clientConfig = bundlerConfigs?.find(({ name }) => name === clientEnvironment); + const containerConfig = bundlerConfigs?.find( + ({ name }) => name === SERVER_CONTAINER_ENVIRONMENT, + ); + if (!containerConfig) return; + if (!clientConfig?.output?.path) { + throw new Error( + `TanStack Start's "${clientEnvironment}" environment was not found. ` + + "Register tanstackStart() before tanstackStartModuleFederation().", + ); + } + + // The container ships with the browser assets and loads its chunks over HTTP from + // the same public path. + const publicPath = clientConfig.output.publicPath; + containerConfig.output ||= {}; + containerConfig.output.path = clientConfig.output.path; + containerConfig.output.filename = `${SERVER_CHUNK_DIR}/[name].js`; + containerConfig.output.chunkFilename = `${SERVER_CHUNK_DIR}/[id].[contenthash:8].js`; + containerConfig.output.publicPath = + typeof publicPath === "string" && publicPath !== "auto" ? publicPath : "auto"; + }); + }, + }; +} + +const REMOTE_FAILURE_FACTORY = /(webpackRequire\.m\[id\]\s*=\s*\(\)\s*=>\s*\{)(\s*throw error;)/; + +/** + * Lets a host load a remote again after a failed load. Module Federation's bundler runtime + * replaces a remote module that failed with one that throws, and retries the load on the + * next import, but the module cache keeps the first failure's empty exports. A server + * that hit a remote outage, or started during one, would then never render that remote. + * Rspack's `strictModuleExceptionHandling` caches the error instead, so this removes the + * failed module from the cache. + */ +function remoteRetryPlugin(): RsbuildPlugin { + return { + name: "tanstack-start-federation-remote-retry", + setup(api) { + api.transform( + { test: /[\\/]@module-federation[\\/]webpack-bundler-runtime[\\/]dist[\\/]remotes\.c?js$/ }, + ({ code }) => + // One line, so the rest of the file keeps its positions. + code.replace( + REMOTE_FAILURE_FACTORY, + "$1 if (webpackRequire.c) delete webpackRequire.c[id];$2", + ), + ); + }, + }; +} + +function clientCompatibilityPlugin({ + chunkLoadingGlobal, + environment, + forceScriptOutput, + isRemote, + name, +}: { + chunkLoadingGlobal: string; + environment: string; + forceScriptOutput: boolean; + isRemote: boolean; + name: string; +}): RsbuildPlugin { + return { + name: "tanstack-start-federation-client-compat", + setup(api) { + api.onBeforeCreateCompiler(({ bundlerConfigs }) => { + for (const config of bundlerConfigs ?? []) { + if (config.name !== environment) continue; + + config.output ||= {}; + config.output.chunkFormat = "array-push"; + config.output.chunkLoading = "jsonp"; + config.output.chunkLoadingGlobal = chunkLoadingGlobal; + config.output.uniqueName ??= name; + + // TanStack Start derives the public path from `server.base`, which Rsbuild always + // sets, so a remote ends up with a host-relative "/" in production. Hosts on other + // origins would then request its chunks from themselves. "auto" resolves them from + // the remote's own script and manifest URLs; an absolute URL (such as a CDN) is kept. + if (isRemote && !isAbsoluteUrl(config.output.publicPath)) { + config.output.publicPath = "auto"; + } + + if (forceScriptOutput) { + config.output.module = false; + const experiments = (config.experiments ??= {}) as { outputModule?: boolean }; + experiments.outputModule = false; + } + } + }); + }, + }; +} + +function serverCompatibilityPlugin({ + chunkFilename, + entryFilename, + environment, + forceCommonJsOutput, +}: { + chunkFilename?: string; + entryFilename?: string; + environment: string; + forceCommonJsOutput: boolean; +}): RsbuildPlugin { + return { + name: "tanstack-start-federation-ssr-compat", + setup(api) { + api.onBeforeCreateCompiler(({ bundlerConfigs }) => { + for (const config of bundlerConfigs ?? []) { + if (config.name !== environment) continue; + + config.output ||= {}; + if (forceCommonJsOutput) { + config.output.filename = entryFilename ?? "[name].cjs"; + config.output.chunkFilename = + chunkFilename ?? `[id].${config.output.uniqueName ?? "server"}.cjs`; + config.target = "async-node"; + config.output.module = false; + config.output.chunkFormat = "commonjs"; + config.output.chunkLoading = "async-node"; + config.output.library = { type: "commonjs2" }; + delete config.output.chunkLoadingGlobal; + } else { + if (entryFilename) config.output.filename = entryFilename; + if (chunkFilename) config.output.chunkFilename = chunkFilename; + } + } + }); + + if (!forceCommonJsOutput) return; + + // async-node chunk loading reads chunks from disk, while the dev server runs the entry + // from memory. Writing the server output keeps both from the same compilation. The + // dev output goes to a cache directory: in `dist/server` it would replace a production + // build that `rsbuild preview` serves, and dev chunks share its chunk names. + api.modifyEnvironmentConfig((config, { name }) => { + if (name !== environment) return; + config.dev.writeToDisk = true; + if (api.context.action === "dev") { + config.output.distPath.root = join( + api.context.rootPath, + "node_modules", + ".cache", + "tanstack-start-federation", + environment, + ); + } + }); + + // TanStack Start's preview server, and deployments built against it, import + // `dist/server/index.js`. Keep that entry as an ES module that re-exports the CommonJS + // bundle. With async startup, the bundle exports a promise that settles once + // federation has initialized. + api.processAssets( + { stage: "additional", environments: [environment] }, + ({ assets, compilation, sources }) => { + if (!assets["index.cjs"] || assets["index.js"]) return; + compilation.emitAsset("index.js", new sources.RawSource(SERVER_ENTRY_SHIM)); + }, + ); + }, + }; +} + +const SERVER_ENTRY_SHIM = `import { createRequire } from "node:module"; + +const server = await createRequire(import.meta.url)("./index.cjs"); + +export const createServerEntry = server.createServerEntry; +export default server.default; +`; diff --git a/packages/tanstack/src/runtime.ts b/packages/tanstack/src/runtime.ts new file mode 100644 index 0000000..1c9ba22 --- /dev/null +++ b/packages/tanstack/src/runtime.ts @@ -0,0 +1,96 @@ +import { createElement, lazy, type ComponentProps, type ComponentType } from "react"; + +export type LazyRemoteOptions = { + /** How long a failed load stays failed before a render tries again, in milliseconds. Defaults to 5 seconds. */ + retryAfterMs?: number; +}; + +/** + * `React.lazy` for remote modules. A component from `React.lazy` that fails to load stays + * failed: on a server, every later request renders the same error, even once the remote + * is back. This one loads again on the first render after `retryAfterMs`. + * + * ```tsx + * const StatusCard = lazyRemote(() => import("remote/StatusCard")); + * ``` + */ +export function lazyRemote>( + load: () => Promise<{ default: T }>, + { retryAfterMs = 5_000 }: LazyRemoteOptions = {}, +): ComponentType> { + let failedAt: number | undefined; + const create = () => + lazy(() => + load().catch((error: unknown) => { + failedAt = Date.now(); + throw error; + }), + ); + let current = create(); + + return function RemoteComponent(props: ComponentProps) { + // React renders again right after a load fails, to report the error. Waiting before a + // new attempt keeps that render from starting another load. + if (failedAt !== undefined && Date.now() - failedAt >= retryAfterMs) { + failedAt = undefined; + current = create(); + } + return createElement(current as ComponentType>, props); + }; +} + +export type RemoteStylesheetOptions = { + /** How long a fetched manifest is reused, in milliseconds. Defaults to 30 seconds. */ + maxAgeMs?: number; +}; + +type CssAssets = { async?: string[]; sync?: string[] }; + +type RemoteManifest = { + exposes?: Array<{ assets?: { css?: CssAssets }; name?: string; path?: string }>; + metaData?: { publicPath?: string }; +}; + +const manifests = new Map }>(); + +/** + * Lists the stylesheets a remote's manifest declares for an exposed module, as absolute + * URLs. Return them from a route loader and render them in the route's `head`, so markup + * a remote rendered on the server is styled before the remote's JavaScript loads. + * + * Rejects when the manifest cannot be fetched or does not list `expose`. + */ +export async function getRemoteStylesheets( + manifestUrl: string, + expose: string, + { maxAgeMs = 30_000 }: RemoteStylesheetOptions = {}, +): Promise { + const manifest = await fetchManifest(manifestUrl, maxAgeMs); + const name = expose.replace(/^\.\//, ""); + const exposed = manifest.exposes?.find( + (candidate) => candidate.path === expose || candidate.name === name, + ); + if (!exposed) throw new Error(`${manifestUrl} does not expose "${expose}".`); + + // "auto" means the assets sit next to the manifest. + const publicPath = manifest.metaData?.publicPath; + const base = new URL(publicPath && publicPath !== "auto" ? publicPath : ".", manifestUrl); + const { async = [], sync = [] } = exposed.assets?.css ?? {}; + return [...new Set([...sync, ...async])].map((asset) => new URL(asset, base).href); +} + +function fetchManifest(url: string, maxAgeMs: number) { + const cached = manifests.get(url); + if (cached && Date.now() - cached.fetchedAt < maxAgeMs) return cached.manifest; + + const manifest = fetch(url).then(async (response) => { + if (!response.ok) throw new Error(`${url} responded with HTTP ${response.status}.`); + return (await response.json()) as RemoteManifest; + }); + manifests.set(url, { fetchedAt: Date.now(), manifest }); + // A failed fetch is not reused: the next call retries. + manifest.catch(() => { + if (manifests.get(url)?.manifest === manifest) manifests.delete(url); + }); + return manifest; +} diff --git a/packages/tanstack/src/shared.ts b/packages/tanstack/src/shared.ts new file mode 100644 index 0000000..20c04ec --- /dev/null +++ b/packages/tanstack/src/shared.ts @@ -0,0 +1,31 @@ +export const defaultShared = { + react: { singleton: true }, + "react-dom": { singleton: true }, +} as const; + +export const eagerShared = { + react: { eager: true, singleton: true }, + "react-dom": { eager: true, singleton: true }, +} as const; + +export function resolveShared( + shared: unknown, + defaults: Record = defaultShared, +): Record { + if (Array.isArray(shared)) { + return { + ...defaults, + ...Object.fromEntries( + shared + .filter((packageName): packageName is string => typeof packageName === "string") + .filter((packageName) => !Object.hasOwn(defaults, packageName)) + .map((packageName) => [packageName, {}]), + ), + }; + } + + return { + ...defaults, + ...(shared && typeof shared === "object" ? shared : {}), + }; +} diff --git a/packages/tanstack/src/vite-compat.ts b/packages/tanstack/src/vite-compat.ts new file mode 100644 index 0000000..ec82983 --- /dev/null +++ b/packages/tanstack/src/vite-compat.ts @@ -0,0 +1,22 @@ +import type { Plugin } from "vite"; + +/** + * Fails fast on Vite 7 and older. `@module-federation/vite` loads remotes on the server + * only through Vite 8's module runner and Rolldown output, so federated SSR breaks + * silently on older versions. + */ +export function viteVersionPlugin(): Plugin { + return { + name: "tanstack-start-federation:vite-version", + enforce: "pre", + config() { + const version = this.meta.viteVersion; + const major = Number(version?.split(".")[0]); + if (major < 8) { + throw new Error( + `@module-federation/tanstack requires Vite 8; this project runs Vite ${version}.`, + ); + } + }, + }; +} diff --git a/packages/tanstack/src/vite.ts b/packages/tanstack/src/vite.ts new file mode 100644 index 0000000..ed974e1 --- /dev/null +++ b/packages/tanstack/src/vite.ts @@ -0,0 +1,36 @@ +import { federation, type ModuleFederationOptions } from "@module-federation/vite"; +import type { Plugin } from "vite"; +import { defaultShared, eagerShared, resolveShared } from "./shared"; +import { viteVersionPlugin } from "./vite-compat"; + +export type TanStackStartModuleFederationOptions = Omit; + +/** Creates one Vite federation plugin set for TanStack Start's client and SSR environments. */ +export function tanstackStartModuleFederation( + options: TanStackStartModuleFederationOptions, +): Plugin[] { + const { + shared, + target: _environmentSpecificTarget, + ...configured + } = options as ModuleFederationOptions; + + // A host renders with its own React before any remote registers a provider. Eager host + // shares keep that instance in the share scope, so remotes from other bundlers cannot + // replace it. Remote-only builds stay lazy and defer to the host. + const isHost = Object.keys(configured.remotes ?? {}).length > 0; + + return [ + viteVersionPlugin(), + ...federation({ + ...configured, + filename: configured.filename ?? "remoteEntry.js", + manifest: configured.manifest ?? true, + hostInitInjectLocation: configured.hostInitInjectLocation ?? "entry", + shared: resolveShared( + shared, + isHost ? eagerShared : defaultShared, + ) as ModuleFederationOptions["shared"], + }), + ]; +} diff --git a/packages/tanstack/tsconfig.json b/packages/tanstack/tsconfig.json new file mode 100644 index 0000000..058e070 --- /dev/null +++ b/packages/tanstack/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noUncheckedIndexedAccess": true, + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "verbatimModuleSyntax": true + }, + "include": ["src/**/*.ts"], + "exclude": ["dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..b0c1c85 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3425 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + '@pnpm/exe': + specifier: 12.9.1 + version: 12.9.1 + pnpm: + specifier: 12.9.1 + version: 12.9.1 + +packages: + + '@pnpm/exe.android-arm64@12.9.1': + resolution: {integrity: sha512-Hgm1XdqMTfBC8PdGLL+NjZWv4xyi7D6XNrN9fb24XGYlyTBmUtdCefKnCLmHujShRD8BEO1zqM30SNcKBDLysw==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.9.1': + resolution: {integrity: sha512-WDGCe6jZRVZP3RC/q6hOkZylkPkIQIi1/GcrR/d/YzbihhAHXCczrk+UYnV5TgbsEnAYO7Cx+2rX3nfhHvy24w==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.9.1': + resolution: {integrity: sha512-+7iiEEBgYN9szPWUnAuN6eOVMTI6Hrki5bcuC9n9xl9tTnT53Tpxpo45Hml+W4IrAU0d+Tmd0sPnVIbX59PD6w==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.9.1': + resolution: {integrity: sha512-SK7TxJiR12GbJZGffRFHItkc+Mx+99v6xMMJ9/X//8NwWV1wo17a2bwfGI8qZpl17YwsrmPEFOOqhzE1GIlrtQ==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.9.1': + resolution: {integrity: sha512-4AjLk6uvTs2iO6wMit2hcxJ2M32xb7OV5SNFk+w6G8/NzcjcMB7ENEpyeXyLiP0kcnp8uaCGUct2NfrLCJ35eQ==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.9.1': + resolution: {integrity: sha512-dZL2ES1ckDWM9v+EtxoOklZ0WNdtayooi9iLiDUubopudX3zXPNR2mlrxBRgjbDgECOndQen2SmyxL5eZiBuKw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.9.1': + resolution: {integrity: sha512-cx+uF6xqwEtm7B8NV+1ve0xBg9gcuzQZ5R+0WgOWvKG/h978UljS6BgWs1ipIGuGgUdeVhm3PKmBltXIcczLXA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.9.1': + resolution: {integrity: sha512-MpjPsy4XR17zAtLziQZfsN+wveWa/bUJggZtwfo3+SuyRmXnSvVkFD4+v8/3RTWyYaM6mwLUTjZDJ+g3urL+ew==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.9.1': + resolution: {integrity: sha512-Oo67TKqqH+Bno+L6X7Xyorwp1QG82zKIkL0vbo1x2kcPTDiyPbdM+d6lIs4+hzeB1KHT5FDoGzZj2dad7Ft+eg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.9.1': + resolution: {integrity: sha512-QAD5FRCdW36HfD58BV7DoDm6z8yv+GgSqcUV1+ERwYjpVQuG6ZTpf5o0x3KDas8eTLIW7MLYVdDdlAurWeArEg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.9.1': + resolution: {integrity: sha512-mQDhBsGqcTkrLQM4q5igmRFcr6EAl4UM/FGawY+0CWGcqPpUmYUvDy9LHy80xxkHIWVJ886gvvso2XOjfav1Jw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.9.1': + resolution: {integrity: sha512-+M5N74hM4fc87AAckxx/UIW+dUnWf2eko+HchJs/NZUxehLhekNyMI/GNaNXlvd+kWR+97XXBD51XRkkNiUtrA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.9.1': + resolution: {integrity: sha512-mI28C2wxobrDi6x1OYhBRyG10hnDq/4sEIyy5RzoX6LB4kLuv6K/d25HOpMwlW20cuM4M72k9NZpOdcmUxMbEA==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.9.1': + resolution: {integrity: sha512-SWoDMT0oD7NrDSIoPpkKD09D9fAQKNjwFZ3lezlCUhAfXxMv70zsfrNT2/P+GoCK2kefXF9OysUZkH1l5vvvfA==} + cpu: [x64] + os: [win32] + + '@pnpm/exe@12.9.1': + resolution: {integrity: sha512-TZOVvYq8nwzmw8IR3P1GKZk87tV7vDPCT3oJlL7IhgYNZfLGov4jX0J8RJxuAXxIT3uxg/uiconrLLBv+X5KyA==} + engines: {node: '>=18.*'} + hasBin: true + + pnpm@12.9.1: + resolution: {integrity: sha512-BrBV//XNINwSeB3hc87TMQzglRvy7GICEaFgRdVETVyTpmMhB2TvKaZTphBFm6A2jw50+E2AxUcjz5Wq57wNbQ==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.9.1': + optional: true + + '@pnpm/exe.android-x64@12.9.1': + optional: true + + '@pnpm/exe.darwin-arm64@12.9.1': + optional: true + + '@pnpm/exe.darwin-x64@12.9.1': + optional: true + + '@pnpm/exe.freebsd-x64@12.9.1': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.9.1': + optional: true + + '@pnpm/exe.linux-arm64@12.9.1': + optional: true + + '@pnpm/exe.linux-ppc64@12.9.1': + optional: true + + '@pnpm/exe.linux-riscv64@12.9.1': + optional: true + + '@pnpm/exe.linux-s390x@12.9.1': + optional: true + + '@pnpm/exe.linux-x64-musl@12.9.1': + optional: true + + '@pnpm/exe.linux-x64@12.9.1': + optional: true + + '@pnpm/exe.win32-arm64@12.9.1': + optional: true + + '@pnpm/exe.win32-x64@12.9.1': + optional: true + + '@pnpm/exe@12.9.1': + optionalDependencies: + '@pnpm/exe.android-arm64': 12.9.1 + '@pnpm/exe.android-x64': 12.9.1 + '@pnpm/exe.darwin-arm64': 12.9.1 + '@pnpm/exe.darwin-x64': 12.9.1 + '@pnpm/exe.freebsd-x64': 12.9.1 + '@pnpm/exe.linux-arm64': 12.9.1 + '@pnpm/exe.linux-arm64-musl': 12.9.1 + '@pnpm/exe.linux-ppc64': 12.9.1 + '@pnpm/exe.linux-riscv64': 12.9.1 + '@pnpm/exe.linux-s390x': 12.9.1 + '@pnpm/exe.linux-x64': 12.9.1 + '@pnpm/exe.linux-x64-musl': 12.9.1 + '@pnpm/exe.win32-arm64': 12.9.1 + '@pnpm/exe.win32-x64': 12.9.1 + + pnpm@12.9.1: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.9.1 + '@pnpm/exe.android-x64': 12.9.1 + '@pnpm/exe.darwin-arm64': 12.9.1 + '@pnpm/exe.darwin-x64': 12.9.1 + '@pnpm/exe.freebsd-x64': 12.9.1 + '@pnpm/exe.linux-arm64': 12.9.1 + '@pnpm/exe.linux-arm64-musl': 12.9.1 + '@pnpm/exe.linux-ppc64': 12.9.1 + '@pnpm/exe.linux-riscv64': 12.9.1 + '@pnpm/exe.linux-s390x': 12.9.1 + '@pnpm/exe.linux-x64': 12.9.1 + '@pnpm/exe.linux-x64-musl': 12.9.1 + '@pnpm/exe.win32-arm64': 12.9.1 + '@pnpm/exe.win32-x64': 12.9.1 + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: 3.0.3 + version: 3.0.3 + playwright: + specifier: 1.63.0 + version: 1.63.0 + prettier: + specifier: 3.9.9 + version: 3.9.9 + turbo: + specifier: 2.11.7 + version: 2.11.7 + typescript: + specifier: 7.0.2 + version: 7.0.2 + + apps/host-context: + devDependencies: + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + react: + specifier: 19.3.0 + version: 19.3.0 + + apps/rsbuild-host: + dependencies: + '@module-federation/runtime': + specifier: 2.9.2 + version: 2.9.2 + '@tanstack/react-router': + specifier: 1.170.41 + version: 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/react-start': + specifier: 1.168.60 + version: 1.168.60(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + example-host-context: + specifier: workspace:* + version: link:../host-context + react: + specifier: 19.3.0 + version: 19.3.0 + react-dom: + specifier: 19.3.0 + version: 19.3.0(react@19.3.0) + devDependencies: + '@module-federation/rsbuild-plugin': + specifier: 2.9.2 + version: 2.9.2(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/tanstack': + specifier: workspace:* + version: link:../../packages/tanstack + '@rsbuild/core': + specifier: 2.2.11 + version: 2.2.11(@module-federation/runtime-tools@2.9.2) + '@rsbuild/plugin-react': + specifier: 2.1.1 + version: 2.1.1(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23)) + '@types/node': + specifier: 26.6.4 + version: 26.6.4 + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) + typescript: + specifier: 7.0.2 + version: 7.0.2 + + apps/rsbuild-remote: + dependencies: + '@module-federation/runtime': + specifier: 2.9.2 + version: 2.9.2 + '@tanstack/react-router': + specifier: 1.170.41 + version: 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/react-start': + specifier: 1.168.60 + version: 1.168.60(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + example-host-context: + specifier: workspace:* + version: link:../host-context + react: + specifier: 19.3.0 + version: 19.3.0 + react-dom: + specifier: 19.3.0 + version: 19.3.0(react@19.3.0) + devDependencies: + '@module-federation/rsbuild-plugin': + specifier: 2.9.2 + version: 2.9.2(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/tanstack': + specifier: workspace:* + version: link:../../packages/tanstack + '@rsbuild/core': + specifier: 2.2.11 + version: 2.2.11(@module-federation/runtime-tools@2.9.2) + '@rsbuild/plugin-react': + specifier: 2.1.1 + version: 2.1.1(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23)) + '@types/node': + specifier: 26.6.4 + version: 26.6.4 + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) + typescript: + specifier: 7.0.2 + version: 7.0.2 + + apps/rsbuild-ssr-host: + dependencies: + '@module-federation/runtime': + specifier: 2.9.2 + version: 2.9.2 + '@module-federation/vite': + specifier: 1.23.3 + version: 1.23.3(typescript@7.0.2)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + '@tanstack/react-router': + specifier: 1.170.41 + version: 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/react-start': + specifier: 1.168.60 + version: 1.168.60(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + example-host-context: + specifier: workspace:* + version: link:../host-context + react: + specifier: 19.3.0 + version: 19.3.0 + react-dom: + specifier: 19.3.0 + version: 19.3.0(react@19.3.0) + devDependencies: + '@module-federation/rsbuild-plugin': + specifier: 2.9.2 + version: 2.9.2(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/tanstack': + specifier: workspace:* + version: link:../../packages/tanstack + '@rsbuild/core': + specifier: 2.2.11 + version: 2.2.11(@module-federation/runtime-tools@2.9.2) + '@rsbuild/plugin-react': + specifier: 2.1.1 + version: 2.1.1(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23)) + '@types/node': + specifier: 26.6.4 + version: 26.6.4 + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) + typescript: + specifier: 7.0.2 + version: 7.0.2 + vite: + specifier: 8.3.2 + version: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + + apps/rsbuild-ssr-remote: + dependencies: + '@module-federation/runtime': + specifier: 2.9.2 + version: 2.9.2 + '@tanstack/react-router': + specifier: 1.170.41 + version: 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/react-start': + specifier: 1.168.60 + version: 1.168.60(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + example-host-context: + specifier: workspace:* + version: link:../host-context + react: + specifier: 19.3.0 + version: 19.3.0 + react-dom: + specifier: 19.3.0 + version: 19.3.0(react@19.3.0) + devDependencies: + '@module-federation/rsbuild-plugin': + specifier: 2.9.2 + version: 2.9.2(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/tanstack': + specifier: workspace:* + version: link:../../packages/tanstack + '@rsbuild/core': + specifier: 2.2.11 + version: 2.2.11(@module-federation/runtime-tools@2.9.2) + '@rsbuild/plugin-react': + specifier: 2.1.1 + version: 2.1.1(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23)) + '@types/node': + specifier: 26.6.4 + version: 26.6.4 + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) + typescript: + specifier: 7.0.2 + version: 7.0.2 + + apps/vite-host: + dependencies: + '@module-federation/runtime': + specifier: 2.9.2 + version: 2.9.2 + '@tanstack/react-router': + specifier: 1.170.41 + version: 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/react-start': + specifier: 1.168.60 + version: 1.168.60(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + example-host-context: + specifier: workspace:* + version: link:../host-context + react: + specifier: 19.3.0 + version: 19.3.0 + react-dom: + specifier: 19.3.0 + version: 19.3.0(react@19.3.0) + devDependencies: + '@module-federation/tanstack': + specifier: workspace:* + version: link:../../packages/tanstack + '@types/node': + specifier: 26.6.4 + version: 26.6.4 + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) + '@vitejs/plugin-react': + specifier: 6.1.2 + version: 6.1.2(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + typescript: + specifier: 7.0.2 + version: 7.0.2 + vite: + specifier: 8.3.2 + version: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + + apps/vite-remote: + dependencies: + '@module-federation/runtime': + specifier: 2.9.2 + version: 2.9.2 + '@tanstack/react-router': + specifier: 1.170.41 + version: 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/react-start': + specifier: 1.168.60 + version: 1.168.60(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + example-host-context: + specifier: workspace:* + version: link:../host-context + react: + specifier: 19.3.0 + version: 19.3.0 + react-dom: + specifier: 19.3.0 + version: 19.3.0(react@19.3.0) + devDependencies: + '@module-federation/tanstack': + specifier: workspace:* + version: link:../../packages/tanstack + '@types/node': + specifier: 26.6.4 + version: 26.6.4 + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) + '@vitejs/plugin-react': + specifier: 6.1.2 + version: 6.1.2(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + typescript: + specifier: 7.0.2 + version: 7.0.2 + vite: + specifier: 8.3.2 + version: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + + packages/tanstack: + devDependencies: + '@module-federation/rsbuild-plugin': + specifier: 2.9.2 + version: 2.9.2(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/runtime': + specifier: 2.9.2 + version: 2.9.2 + '@module-federation/vite': + specifier: 1.23.3 + version: 1.23.3(typescript@7.0.2)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + '@rsbuild/core': + specifier: 2.2.11 + version: 2.2.11(@module-federation/runtime-tools@2.9.2) + '@rslib/core': + specifier: 1.0.3 + version: 1.0.3(@module-federation/runtime-tools@2.9.2)(typescript@7.0.2) + '@tanstack/react-start': + specifier: 1.168.60 + version: 1.168.60(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + '@types/node': + specifier: 26.6.4 + version: 26.6.4 + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + react: + specifier: 19.3.0 + version: 19.3.0 + react-dom: + specifier: 19.3.0 + version: 19.3.0(react@19.3.0) + typescript: + specifier: 7.0.2 + version: 7.0.2 + vite: + specifier: 8.3.2 + version: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + +packages: + + '@ast-grep/napi-darwin-arm64@0.45.3': + resolution: {integrity: sha512-rMZJ6frSpR/xSbeV4hiA9WR5hHqk3rVYX8lOqEoUCaUnjEuFXvHdGN4WawJ8pVDq/IqM1UnTtsDSYgUdVmO5DA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@ast-grep/napi-darwin-x64@0.45.3': + resolution: {integrity: sha512-tIclZ5pM+YSgk8NNTBoTkaclbS39cU5lbVfSrFWX4LUnxXfUPG+F+ppxyFjKwJTBELsGHEO4LdX3/Lc+FDwPNA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@ast-grep/napi-linux-arm64-gnu@0.45.3': + resolution: {integrity: sha512-Bm8F7/oRHQkDXbjH+gBi9oB5huJ0q12r/AIOMIypf5PWlAVvh206Tsw468phw4Oh6kd4Ad1Z+gflNW1MutgK2g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@ast-grep/napi-linux-arm64-musl@0.45.3': + resolution: {integrity: sha512-yr5xcRerV41GeRxmZ8SKbfGeyZRhQePoNNCgTAb4OrWMelWwbMI80aY7/IlF2bo1sN40BSW4hZdGIO/akTd1RQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@ast-grep/napi-linux-x64-gnu@0.45.3': + resolution: {integrity: sha512-lluRK3hxfHMQdFuVID6seAKVx4FH0kEwF6rCh5/I40Sf3EtNR2UCBXAqd6RBA6hqlSckktvBxCfw+h4aescDmg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@ast-grep/napi-linux-x64-musl@0.45.3': + resolution: {integrity: sha512-09wJQKq86LqILqQtw0+X09SzhcPwUnMVFKyia7S3XLbLqSeQpAMF55Y+zll53JRQ1dCzFEZJyvTT/7j9vwfE3w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@ast-grep/napi-win32-arm64-msvc@0.45.3': + resolution: {integrity: sha512-/HiVgw2cdtzXLDLoG4eBBcauekNXewfagy7qiVbjkVu/KcCH8dJ9lpj8DzeWwuBNtZxuD8jpiwP0M1pz3Bc3uw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@ast-grep/napi-win32-ia32-msvc@0.45.3': + resolution: {integrity: sha512-/gwJrJQUw65uNtAFcG2rtSTr117Gqqy3bfjaljCPKsa65dTeRcMMnjKLlZ6V9/g8z+4Xf78kdhp5ECbir4yfSw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@ast-grep/napi-win32-x64-msvc@0.45.3': + resolution: {integrity: sha512-CEZAMAsaDoQoVE3MT2c/ry3/ydfyaqlTjxv8DK5PG355SrdAYPgevGC2Me6tz4dhphJin89Y20VsnrS5UrYV5Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@ast-grep/napi@0.45.3': + resolution: {integrity: sha512-bjJsUt0g3UCwbn9bjv7MnoR15rszSm79yPCXn4DWWK0NuYUUqBU/srtNMR4WnpfR7TnfBRdYFoqSNx0pGCNWJg==} + engines: {node: '>= 10'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@changesets/apply-release-plan@8.1.1': + resolution: {integrity: sha512-nk2Hbq3M9NeHMdaIRBDvawKrQJzXB9sGyBUE5yWnIKtvdcNCPj75zBekJYr0gTT2ecJH3eFeTrc8eisPImcv0Q==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/assemble-release-plan@7.0.0': + resolution: {integrity: sha512-oEW8BxdA604kGGtDSCiHr5w9Tv4UWe9I2k61IBNZzCOE1kbYaJj4v+lFQNgcEZFkUc2pV/+hASErGDvpJOZCTg==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/changelog-git@1.0.0': + resolution: {integrity: sha512-3Dst2Ime2Op5nd4XmWJLPIgp11ZFqJqSkVug9izK6TDcIV4YlhPS4ECbEVR+eGI0bk0r1ItogD4j2Oli87bJrA==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/cli@3.0.3': + resolution: {integrity: sha512-L1i67bMfkK6iC/kDejuNO4pFgTRcWIObsD6Qof2woP1I4M8tj17DGlcpRaTzTv1DYSP5EMwWdEtR8X0Pj5Jg3Q==} + engines: {node: ^22.11 || ^24 || >=26, npm: '>=10.9.0', pnpm: '>=10.0.0', yarn: '>=4.5.2'} + hasBin: true + + '@changesets/config@4.0.1': + resolution: {integrity: sha512-PxpxSQLQSLDjceAgRq+4FWlU+HflDIw03mmPVipxoQx9xrXFUjmqTy3SE+n/2t+DqPdpPiLWVr5NRiLZvjyLwg==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/errors@1.0.0': + resolution: {integrity: sha512-ElN/mEzn6zmETgjwf5MclCMa9ef59sAR0lfO8VSYIsiRvbC2FbLB/92EoYw10Sl0kGixxHFiJZUSv7dA+YpR8g==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/format@0.1.2': + resolution: {integrity: sha512-Caez5XtNXCFS/G5bwyav3wuXL0tMxVd2ZGbaumWbzN08tyzO21asCw7JZhNtVsAZDCvDRUzZN+Iit9SyRITSYA==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/get-dependents-graph@3.0.0': + resolution: {integrity: sha512-ji/t5wFA1zREKXRUePE6Qi+Qu2UgxCeSSGQrphezwvQZrp49B7sJ+8+wvM0tA7zPeSxYKCojDy3WWgrl+s+awg==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/git@4.0.1': + resolution: {integrity: sha512-6vWpIAC4LkpmlqaIu37ViT5enyd9+uBwAiGqCGhASvkSHBgx4PrtTGXWTMFYpDmZbjgyonyVgMciPrW4MqtJsA==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/parse@1.0.0': + resolution: {integrity: sha512-P0iaMb9p9CRYZiTgAllEIF9AUMQHIy1G72tKlcIqJp61icZSsKQNiOPdxAMZG8m/DvZwt/oz5xEbTpike//dWg==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/pre@3.0.0': + resolution: {integrity: sha512-Zm/6YliV/a2oeWTqHJf6KxLrQwgcK1i/BRDl2m0EKZvbnxV5fG9QRhwJJGshjsZTUTS6dkfURQ2K6aAvgNw/3Q==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/read@1.0.1': + resolution: {integrity: sha512-nzvSC6RcTiWf/dsuwZFXpLzGGsRHYCL68U3uHV7srsulU93aVWY7u2GEJiDZ+4GIIElfaW5EqtKC0UHroY+LTQ==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/should-skip-package@1.0.0': + resolution: {integrity: sha512-pwqoJmbONn1XgXmZXPEExgAaT+HdZLjALFTDgIm+PnS5KeO2nLtzA2/Q+4aMFY14kFMuXKG30MObhvDzWgzDgg==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/types@7.0.0': + resolution: {integrity: sha512-c5GoiQyt3pxiXjrWSNoP8/GRf4kG+VnKzovx1OQM8dYYALlSwgedmkPmJ+ZqGxqwg9D3Bkj85Uo4KLd5BN3A0w==} + engines: {node: ^22.11 || ^24 || >=26} + + '@changesets/write@1.0.1': + resolution: {integrity: sha512-q/ThtP9gcnEP6xlv7LrY26C2mHqdGBti/MmOVngt/M/oIGYkssmQGxPK9WzBNt2juVcH/vml2WQ+ra8LXYOTaA==} + engines: {node: ^22.11 || ^24 || >=26} + + '@clack/core@1.5.1': + resolution: {integrity: sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.8.1': + resolution: {integrity: sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==} + engines: {node: '>= 20.12.0'} + + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@manypkg/find-root@3.1.0': + resolution: {integrity: sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA==} + engines: {node: '>=20.0.0'} + + '@manypkg/get-packages@3.1.0': + resolution: {integrity: sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg==} + engines: {node: '>=20.0.0'} + + '@manypkg/tools@2.1.2': + resolution: {integrity: sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==} + engines: {node: '>=20.0.0'} + + '@module-federation/bridge-react-webpack-plugin@2.9.2': + resolution: {integrity: sha512-6ikuvP1639g8jzSxveDLz/BOk0VTIkO7aSuvrZgq0waB0h553zFtkTJAXQETezzO+s1taHGhEvFoCtl6DJWu6A==} + + '@module-federation/cli@2.9.2': + resolution: {integrity: sha512-1+rNHFGC07lQq7CtgcF9ibtgMcAau4TzdOcLfCv13+o5XWWdM8X1yhPbqQWvj3MO/C/Js7Xwon1sTKjF69Do9g==} + engines: {node: '>=16.0.0'} + hasBin: true + + '@module-federation/dts-plugin@2.9.2': + resolution: {integrity: sha512-J84cRrewtUFnFdMeD/91mHtW+Toh3mAWvGnhsFAc9oEbGwqfzJEfgPJri65xdtzWcNhs1d6FImPMTvnjzOnYvA==} + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + vue-tsc: '>=1.0.24' + peerDependenciesMeta: + vue-tsc: + optional: true + + '@module-federation/enhanced@2.9.2': + resolution: {integrity: sha512-m1/kzb97Eu1v7FUenvwMpJiF6Vc8RNDjFxHS+YNgxy3WjI8s/mQrLJBrGK66qpK3wuP9kx0qB65kRiurB9IjUA==} + hasBin: true + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + vue-tsc: '>=1.0.24' + webpack: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + webpack: + optional: true + + '@module-federation/error-codes@2.9.2': + resolution: {integrity: sha512-MTpKubqsJSBgnuw+2Pli1kHL4INeHD5cGnHL84oRANwQAWK7b6lN3ZZTmL3FL6lYn65ImineqZNqGWBv5GQVLQ==} + + '@module-federation/inject-external-runtime-core-plugin@2.9.2': + resolution: {integrity: sha512-e8F/+fNwVgwhJVhLURAa7orYe1WFojamf4myf/G4jszGZ6Yx5DoIUjFBMrkLQgY2nuM/cw2/itIY4ZMGL1CJbA==} + peerDependencies: + '@module-federation/runtime-tools': 2.9.2 + + '@module-federation/managers@2.9.2': + resolution: {integrity: sha512-VtSKLkT2EA1wqCMFXNiklC+XF+5bZPYY3sUQ9LCFHqKS7v7NOJDrwMV6Qtw3fxH4Y6Pe6P28MiU8D9dlzknCKQ==} + + '@module-federation/manifest@2.9.2': + resolution: {integrity: sha512-l9X1g2nHqjdumz9U9jawKo8rKoWXVbSlzLOqs+GYiHfzqSB2/ZQ+4AX0CaJ7orLz2varpGK4qdnQ0saptGuEVw==} + + '@module-federation/node@2.7.52': + resolution: {integrity: sha512-K6R3+UoLK7SCxL0bxRs3J17AZeOEOY8ytyqxP3Y1jtO/drVqu1GaB0t+N1hvmle2zfAe9AmSG49cUiO03T5aEQ==} + peerDependencies: + webpack: ^5.40.0 + peerDependenciesMeta: + webpack: + optional: true + + '@module-federation/rsbuild-plugin@2.9.2': + resolution: {integrity: sha512-E6VekVYmFdKWhL/ip75ukhAm/9gbK7f/cRmzS1BxTmw+8EFrrnvScU7qoj1MTSnMvViTtmXS42D49JWZ3SFw3g==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@rsbuild/core': ^1.3.21 || ^2.0.0-0 + peerDependenciesMeta: + '@rsbuild/core': + optional: true + + '@module-federation/rspack@2.9.2': + resolution: {integrity: sha512-RobMJ/msRzJ94L9k1CAsPuhed2N2rvHBRBJhtk7t3pK/UoQM95Ks6KAcmVKBrxTVHWMhsKTCatxC7u7X19LYdA==} + peerDependencies: + '@rspack/core': ^0.7.0 || ^1.0.0 || ^2.0.0-0 + typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + vue-tsc: '>=1.0.24' + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + + '@module-federation/runtime-core@2.9.2': + resolution: {integrity: sha512-S9roTDqLtAWaaDBuug69ZnXXGEx0pUq/yG06J759bjRgfAg2P9+dWEv1q7rB2eMIp7HH4L6s3ColDRYdL6tJ9w==} + + '@module-federation/runtime-tools@2.9.2': + resolution: {integrity: sha512-rhOvimSLLnnocfOVUfAzH2xDq1pHMw4f/nSC+JQhSztEgP/1UslPPvcBcenx3B8/6CoYmBbGa2OIBTJ225ZHAg==} + + '@module-federation/runtime@2.9.2': + resolution: {integrity: sha512-1lPZVy7TkMTTBMkkjgQt7jA+9FAx226yQqEz+X0lFAIbpxIWfpzNQLD+7IydEe83ZhDJpK3cv/9bn3jjQBZeqQ==} + + '@module-federation/sdk@2.9.2': + resolution: {integrity: sha512-ILtBFPuYpVcRA/Qay1lJ2hMtZI8fDE/v1n7xiN17e5qx5rSBR3FaamTsmRpN1hIt+gVnoa76CzOP/Y73d8hHNg==} + + '@module-federation/third-party-dts-extractor@2.9.2': + resolution: {integrity: sha512-FKJVGx+/jIe16RfmwoyIDVMPAttIFJv6AJCLkKf05IRNvI3RJFqQ1tLP1YjcfGuumCbYuWGa4QKnMla7YjVpqg==} + + '@module-federation/vite@1.23.3': + resolution: {integrity: sha512-XYW7ePFmrSIIvQR4PBHBrujRlFI7TBP+g0QZkw9AsqW/A/x/tT97ypWe+osX4YCkCy57/48AQ0B/3bkl/wZXXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@module-federation/webpack-bundler-runtime@2.9.2': + resolution: {integrity: sha512-dDtWiYcCWWgm/mCS7L4KfDHsyr7HwDRnHHS2bgl1H7RQfH4WAi706gT2hhzmV272PI33V7qJHN7Go1d/BamN0Q==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oozcitak/dom@2.0.2': + resolution: {integrity: sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==} + engines: {node: '>=20.0'} + + '@oozcitak/infra@2.0.2': + resolution: {integrity: sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==} + engines: {node: '>=20.0'} + + '@oozcitak/url@3.0.0': + resolution: {integrity: sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==} + engines: {node: '>=20.0'} + + '@oozcitak/util@10.0.0': + resolution: {integrity: sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==} + engines: {node: '>=20.0'} + + '@oxc-project/types@0.152.0': + resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} + + '@pnpm/deps.graph-sequencer@1100.0.1': + resolution: {integrity: sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==} + engines: {node: '>=22.13'} + + '@rolldown/binding-android-arm-eabi@1.2.12': + resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.12': + resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.12': + resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.12': + resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.12': + resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': + resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.12': + resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.12': + resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.12': + resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.12': + resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.12': + resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.12': + resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.12': + resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.12': + resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.12': + resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rsbuild/core@2.2.11': + resolution: {integrity: sha512-avPW0j6RaEbtV2mfl0AzTAJEimPX4Xj0pCcZyxNCrE9vkS91HmweM5V7XQj3kI5iTYfHRp9Y6To8RRHYLEvG7A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + core-js: '>= 3.0.0' + peerDependenciesMeta: + core-js: + optional: true + + '@rsbuild/plugin-react@2.1.1': + resolution: {integrity: sha512-tLtUedsRtTLS6M4VtwzI6aakFLOFdgl9Sh1ZNBWvxb0Te3EnHrppaE7UiYEmDUXQDJ50gfr6yyirtxY5N6moyg==} + peerDependencies: + '@rsbuild/core': ^2.0.0 + peerDependenciesMeta: + '@rsbuild/core': + optional: true + + '@rslib/core@1.0.3': + resolution: {integrity: sha512-a5IZiyf9sNeeE9jcTsTmz16vxcThpbOSJg0BZXzJAxrUFOZX5xG25PJua9hr0LDWGryBX2UQTruVsLQ8BCpqng==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7 + typescript: ^5 || ^6 || ^7 + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + typescript: + optional: true + + '@rspack/binding-darwin-arm64@2.2.8': + resolution: {integrity: sha512-d6GuY/k7kJk7SOXmruIz0m+2fIXaLv2SnBHEYsbvBzE7ZO6QXq7fSABWFWNnSnNu+brYHA5Dr/skV81bXgTTCw==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@2.2.8': + resolution: {integrity: sha512-xCVou/jJASjEAjwy1T3hPJcJqA5QZASJfUwHqEk+nA0v44MagwH8tM8lsOvRfhzR7N2+YFac1rfZOUT8uM3awg==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@2.2.8': + resolution: {integrity: sha512-Zy/ss6YAQTjOWKDkcWrbKtu+pySltqT22wn68nlrWKygPWLNmMdU5anlsnwRTIuQ3MF0YJRS42ARauVtlqvRQw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-arm64-musl@2.2.8': + resolution: {integrity: sha512-lxgp5O8PhL3St9p82llHoNoeWzp9jltW5S9F00p00XPJZHUIuxbwfLSzn/6Wg86USEmafLoBkyD9CM8rBd3SNQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rspack/binding-linux-ppc64-gnu@2.2.8': + resolution: {integrity: sha512-MY+YqpT02tcDCMgHy+IewQsaz7ql4mbJaAqqMlTIqvQwAUl9dXEP7BL352OgCxm9VRebTldE5lK99A3wVd3zqA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-riscv64-gnu@2.2.8': + resolution: {integrity: sha512-wmN9OigSGNGCKY+AOYaWI8LJcIi8qvs5KjueBueCKOnTV6MPIEujA75QZH1pehTTYKvdGv7r2EtUXUmWpn7/SQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-riscv64-musl@2.2.8': + resolution: {integrity: sha512-ZcQMYR10hLh581zs9MFnFFn0F7wyt5fkDffyukeMHJEoPJrbkfl6cQtsUdpQnZ+mYGYS5KydD1DT9/eMH65jjw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rspack/binding-linux-s390x-gnu@2.2.8': + resolution: {integrity: sha512-ahBiE+Ir1FMYvStYicihhSJlS2YRagxtry0jWH8zW287o7C0eiHJ+pteJHHC15T5RJqm0j6w5TuwYe+F9AYxxg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-x64-gnu@2.2.8': + resolution: {integrity: sha512-dncEI515NoPXh3zzWk06Rb3mhSV76myiFKqXiJ8K4fVAe5x/RlweMhvPhHhm7RnIMeBld7BeStzOqj38XFsWGA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-x64-musl@2.2.8': + resolution: {integrity: sha512-pfFyU03PjD0GaQG8zOQOHwq51rrJZetXjTGpMmz7iA1hq/f9lChJsDWrC8/7VXeQLwBofLZlOODqe8OM8OBzgA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rspack/binding-wasm32-wasi@2.2.8': + resolution: {integrity: sha512-3mOJuZTrF56jOh/GD9chLMJKbzPZ6Ty2OIML7+lLz0Yk2XS/xWRfLn85Dmnkb/O6UF65x7+p8VPAIZlBtLTclw==} + cpu: [wasm32] + + '@rspack/binding-win32-arm64-msvc@2.2.8': + resolution: {integrity: sha512-Cu/RMZT/16s+67xhl6X1pswetL3GLSQl+LnugKRh08Ul21A/tR7vxC4g4aih4u9k5SXCAngxFUt8zAjKnHIBqA==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@2.2.8': + resolution: {integrity: sha512-H/erd6di8OapZhhM0raA/7XduPn9jusfLMeO1XF+AmF9WF3HzHgMjha1j+KwtPgQnaH69w6AhY+G2472VIwQmw==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@2.2.8': + resolution: {integrity: sha512-BziHSUlIQM0MliCqdcHuBXSkQGtPoHQTXJce7fsgnTAzQFgPeog92qTNezYI4IrQ7pQouyiVJ2HPieUbVPRKQQ==} + cpu: [x64] + os: [win32] + + '@rspack/binding@2.2.8': + resolution: {integrity: sha512-c9flkSua3khHnOKBCRk8BkHGOwU9YB7niv+5JfQXiLZIvJmgKsZ7PD8QrFklMhsJT3oVAc6c9GN7/wy1YkACjA==} + + '@rspack/core@2.2.8': + resolution: {integrity: sha512-JugsVrnbMGNYzMH++7gR9enygpP/lAo9PbVRq3XmJg0ryIvCpST0XJjBcccaSjYaaPamsjrddihmgVLMFp+yXw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 + '@swc/helpers': ^0.5.23 + peerDependenciesMeta: + '@module-federation/runtime-tools': + optional: true + '@swc/helpers': + optional: true + + '@rspack/plugin-react-refresh@2.0.2': + resolution: {integrity: sha512-dGNZiCxQxgAUI9sah7gd8u+O7OJZRCmqtEJNDOd8xW5RqcieC86F7p5qcShyw6onH5pKf57evpr2VjGbaFGkZg==} + peerDependencies: + '@rspack/core': ^2.0.0 + react-refresh: '>=0.10.0 <1.0.0' + peerDependenciesMeta: + '@rspack/core': + optional: true + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@tanstack/history@1.162.4': + resolution: {integrity: sha512-utTS5L2OkeYUzXGohL1Z8sefu1GLNOJcxe8Hd6iIdc/Xo1K1nDB2JEp4iSFhvYh33xKC9V91TxrS8qfrpoKobQ==} + engines: {node: '>=20.19'} + + '@tanstack/react-router@1.170.41': + resolution: {integrity: sha512-Mpvw8Wm5MGDbTqI8Yc9flLKxbOHwoWocv0H2aGKcgw2a017oK5nZWqa954P+DrUx/8fI98YNywnHP5XcD3tQ9w==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-start-client@1.168.39': + resolution: {integrity: sha512-Uby0uo37JlQgUKHU0snDwH9nYiEeAjm9+7pUOFgycU/UszEILC2SO2DSCoNnCMuHKj9z+4URKzBYIP2exT9+wg==} + engines: {node: '>=22.12.0'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-start-rsc@0.1.59': + resolution: {integrity: sha512-M2Lqxkk5C1zQncQs5paqX+/OLdOht51pg6D84YfLMrNsjhU/nzb6wd92MIja0mi5i23+L+fxLLjQYR2DObjBlw==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@rspack/core': '>=2.0.0-0' + '@vitejs/plugin-rsc': '>=0.5.30' + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + react-server-dom-rspack: '>=0.0.2' + peerDependenciesMeta: + '@rspack/core': + optional: true + '@vitejs/plugin-rsc': + optional: true + react-server-dom-rspack: + optional: true + + '@tanstack/react-start-server@1.167.46': + resolution: {integrity: sha512-K7z8xBOEdp8u5CZCxWrbS0tFtk5XZGMdhssLeZlRZKlLDbX7BKLdMtJ7th5sR6QVt9MTUAg3LcoqvJMPSFF+lg==} + engines: {node: '>=22.12.0'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-start@1.168.60': + resolution: {integrity: sha512-WMOM0Xlh2lPbVlbjYM6uOxOpiaWRPvODm/dwkLsGKq5tjyHOHZmKckJgwlrZl7OQUqFlOZHYj8bKUTgoeg8cUQ==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@rsbuild/core': ^2.0.0 + '@vitejs/plugin-rsc': '*' + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + vite: '>=7.0.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@vitejs/plugin-rsc': + optional: true + vite: + optional: true + + '@tanstack/react-store@0.11.2': + resolution: {integrity: sha512-oj5O5DmiMWE+ih+SioX2ayC1kbK7d3fpLx0B75fPJr9nY3zU+hoW010yr64qXsY/9fyiLVE2bn5mGNV3owm8RQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.171.34': + resolution: {integrity: sha512-X+19pGuuLVPvvAvPYw9v/+ktRk5cCll8AtfpBpOhDRmVgUMrfqze0467xtq8YRVVs/mmo5rVtWn/7Tp32RDBKw==} + engines: {node: '>=20.19'} + + '@tanstack/router-generator@1.167.40': + resolution: {integrity: sha512-WkEueqEvsBBudzO0fx1nWfOmRNW8OAvBoVABTGls9bAWzjdC+KDGHvjCNrvn88ofs1F6gqL8aWnzKCbxW3dCyQ==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.168.42': + resolution: {integrity: sha512-vAR5WXJh/ErseEWQftDhsJcr2c++ucRQ6FbqN3ygMO44+ZxxRfW3sIpZmddHROVSmBk8jc6MrwjTPNjVhd62Vw==} + engines: {node: '>=20.19'} + peerDependencies: + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.170.41 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.162.3': + resolution: {integrity: sha512-Icb0xGuG1+54IV0WMLRcc3ErTx2HeJWyPG661FkQ8UT6guoBq1FJjFRqlG/JS0xi4mFFkBhvwO7tbLa32f3yxA==} + engines: {node: '>=20.19'} + + '@tanstack/start-client-core@1.170.34': + resolution: {integrity: sha512-LfwZSbau2MLLZ/uPAoO+NC9fK3J66c0wLhnNQqIa+Goqw7bXPjkHwlsjY7evlUz1AnyhhQ1Qab95v+VmP0TZ9A==} + engines: {node: '>=22.12.0'} + + '@tanstack/start-fn-stubs@1.162.0': + resolution: {integrity: sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ==} + engines: {node: '>=22.12.0'} + + '@tanstack/start-plugin-core@1.171.49': + resolution: {integrity: sha512-HPOb7LnMc71k38cd5uvMybPPUU/Q/56KvSqQzSWz7cps1ZDFXaxUM6EFaFHLCdgTWqGexNGDdRE+sUVPVBM4Xg==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@rsbuild/core': ^2.0.0 + vite: '>=7.0.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + vite: + optional: true + + '@tanstack/start-server-core@1.169.39': + resolution: {integrity: sha512-pqqkxhXY46nuFSYt6TsMGyABiplUzHFHndKAlRRGC0fWDrJCH1koWxVMoxySa51ZETV79f/z73b5UZ1jfIkXwQ==} + engines: {node: '>=22.12.0'} + + '@tanstack/start-storage-context@1.167.36': + resolution: {integrity: sha512-x+xCXK2GtFzY6NbXqAg8YpU6cwAjDfMeYSNzxfGzntpjbwBRHHrXeC/kYUnBBQCV7wK4kJ8vBBQGP0OyYTasUg==} + engines: {node: '>=22.12.0'} + + '@tanstack/store@0.11.2': + resolution: {integrity: sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q==} + + '@tanstack/virtual-file-routes@1.162.0': + resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} + engines: {node: '>=20.19'} + + '@turbo/darwin-64@2.11.7': + resolution: {integrity: sha512-Z2LFOxwbiJueD1miByXJS/BrW21vv6YaQXZustty5jEX4vQYw6zyh101Sz1QO7WG0mqBzKagZ/DoAeeujI0/Sw==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.11.7': + resolution: {integrity: sha512-Vhv8ybgIflosNDSSRD9fNXHlCFA60dsGyTqQmc7l28SEX5jOaweCCD4BBYNd8/p0WGkw3e7tZbfA9w6FsujGvw==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.11.7': + resolution: {integrity: sha512-rAqRp3blQKk1+ylMGlZHAW0eJjVNuuGUm4ZrC6RU1Im8tLryvaO4a2zLQ/FQi1TRlmKqKV9YXBki2YeytlLbXA==} + cpu: [x64] + os: [android, linux] + + '@turbo/linux-arm64@2.11.7': + resolution: {integrity: sha512-thVYGov7S+llXAxrl3kBb/efalav0xhrjl5NHCR6AfKx14JkbqaetvbVwyss4FvZ9rt+Pv3xsTb+8SIYix8DTg==} + cpu: [arm64] + os: [android, linux] + + '@turbo/windows-64@2.11.7': + resolution: {integrity: sha512-ZIoKT7J7KutgYl4bGxmmgeUQjG+7E0nY8BF9dxUOiMH2vdTsM4JQ2uSwTYFkTR2Llbg6ghGjSmuZ8LQtkLEYjg==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.11.7': + resolution: {integrity: sha512-88Xv2f7SLGVJ68lRRx8o375dKmnZYNdDtvlKcPA0kZpcF93eAQd8l96BKQ0/AMHG0bMJA0Z5QqoqtetxzNsC+g==} + cpu: [arm64] + os: [win32] + + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@26.6.4': + resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@vitejs/plugin-react@6.1.2': + resolution: {integrity: sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.152.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true + + adm-zip@0.6.1: + resolution: {integrity: sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==} + engines: {node: '>=14.0'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansis@4.4.0: + resolution: {integrity: sha512-9k3v7xcHwgdO/DruxGIg4HtjvlAZlcnsX/mzqUb1t3NkYnl9kK2UJ+Gq0io+vQf7iT//BD/HB/NBkUR1LWxoeA==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + + baseline-browser-mapping@2.11.27: + resolution: {integrity: sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.29.3: + resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + caniuse-lite@1.0.30001814: + resolution: {integrity: sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + electron-to-chromium@1.5.444: + resolution: {integrity: sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + exsolve@1.1.3: + resolution: {integrity: sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetchdts@0.1.7: + resolution: {integrity: sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + h3@2.0.1-rc.20: + resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} + engines: {node: '>=20.11.1'} + hasBin: true + peerDependencies: + crossws: ^0.4.1 + peerDependenciesMeta: + crossws: + optional: true + + human-id@4.2.2: + resolution: {integrity: sha512-Z+8unD6em/MJwyrkWQ2M3wygRGI1+MztbutiNwk1DCuEPXOZRWGjtcMPZYP1exY9iNZMhd7ZOSMQSo9WYtiQRw==} + hasBin: true + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + isbot@5.2.2: + resolution: {integrity: sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==} + engines: {node: '>=18'} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + launch-editor@2.14.1: + resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.57: + resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} + engines: {node: '>=18'} + + package-manager-detector@1.9.0: + resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} + engines: {node: '>=20'} + hasBin: true + + postcss@8.5.29: + resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==} + engines: {node: ^10 || ^12 || >=14} + + prettier@3.9.9: + resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} + engines: {node: '>=14'} + hasBin: true + + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + peerDependencies: + react: ^19.3.0 + + react-refresh@0.19.0: + resolution: {integrity: sha512-5HdstYgoJBbBofjapO5qCPcovECJCBfvjLb+iTcsmqDFVMRdqy0qs7iiInBJTUXmIFc2bjoc5b2HVLp2zRtYhQ==} + engines: {node: '>=0.10.0'} + + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + engines: {node: '>=0.10.0'} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rolldown@1.2.12: + resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rou3@0.8.1: + resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==} + + rsbuild-plugin-dts@1.0.3: + resolution: {integrity: sha512-wVxMYvWmVPam6SeZsz8OixHm+4JPfLJAAMTD0Q2te2SxksvjuHC3wuvU12teSmloQ3nFX1YC6guNzuYqDYw+vQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@microsoft/api-extractor': ^7 + '@rsbuild/core': ^2.0.0 + typescript: ^5 || ^6 || ^7 + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + typescript: + optional: true + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + schema-utils@4.3.0: + resolution: {integrity: sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + seroval-plugins@1.6.8: + resolution: {integrity: sha512-N7mWAMydj89EnYTHtRpS3LBrAz3J9O6oVrqpuXuvlAHfAUG8WEKqtbAd7SBiJNY5NfCAFhSdyAh7wI4Shjqy4g==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.6.8: + resolution: {integrity: sha512-HlSgSAkTk4EqHcje1ptJjfZi1YDv5KbhVJ/d3P7T/nAXua2VmDu+AKDX5VTdFfZf48nDkWB2TKYt0DrCSa+3wg==} + engines: {node: '>=10'} + + shell-quote@1.12.0: + resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==} + engines: {node: '>= 0.4'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + srvx@0.11.22: + resolution: {integrity: sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==} + engines: {node: '>=20.16.0'} + hasBin: true + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + turbo@2.11.7: + resolution: {integrity: sha512-RhXHbqfvY6o3xt/zSHCwSNwLrZlXiqHzewi4eAj9PtZ1T1CqgMJ2yIDb7SKPTG9IikTm925mfzL6hldQVdyNWw==} + hasBin: true + + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + undici@7.29.1: + resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} + engines: {node: '>=20.18.1'} + + unplugin@3.4.0: + resolution: {integrity: sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@farmfe/core': '*' + '@rsbuild/core': '*' + '@rspack/core': '*' + bun-types-no-globals: '*' + esbuild: '*' + rolldown: '*' + rollup: '*' + unloader: '*' + vite: '*' + webpack: '*' + peerDependenciesMeta: + '@farmfe/core': + optional: true + '@rsbuild/core': + optional: true + '@rspack/core': + optional: true + bun-types-no-globals: + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + unloader: + optional: true + vite: + optional: true + webpack: + optional: true + + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.7.0: + resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite@8.3.2: + resolution: {integrity: sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xmlbuilder2@4.0.3: + resolution: {integrity: sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==} + engines: {node: '>=20.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + +snapshots: + + '@ast-grep/napi-darwin-arm64@0.45.3': + optional: true + + '@ast-grep/napi-darwin-x64@0.45.3': + optional: true + + '@ast-grep/napi-linux-arm64-gnu@0.45.3': + optional: true + + '@ast-grep/napi-linux-arm64-musl@0.45.3': + optional: true + + '@ast-grep/napi-linux-x64-gnu@0.45.3': + optional: true + + '@ast-grep/napi-linux-x64-musl@0.45.3': + optional: true + + '@ast-grep/napi-win32-arm64-msvc@0.45.3': + optional: true + + '@ast-grep/napi-win32-ia32-msvc@0.45.3': + optional: true + + '@ast-grep/napi-win32-x64-msvc@0.45.3': + optional: true + + '@ast-grep/napi@0.45.3': + optionalDependencies: + '@ast-grep/napi-darwin-arm64': 0.45.3 + '@ast-grep/napi-darwin-x64': 0.45.3 + '@ast-grep/napi-linux-arm64-gnu': 0.45.3 + '@ast-grep/napi-linux-arm64-musl': 0.45.3 + '@ast-grep/napi-linux-x64-gnu': 0.45.3 + '@ast-grep/napi-linux-x64-musl': 0.45.3 + '@ast-grep/napi-win32-arm64-msvc': 0.45.3 + '@ast-grep/napi-win32-ia32-msvc': 0.45.3 + '@ast-grep/napi-win32-x64-msvc': 0.45.3 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@changesets/apply-release-plan@8.1.1': + dependencies: + '@changesets/config': 4.0.1 + '@changesets/format': 0.1.2 + '@changesets/git': 4.0.1 + '@changesets/should-skip-package': 1.0.0 + '@changesets/types': 7.0.0 + import-meta-resolve: 4.2.0 + jsonc-parser: 3.3.1 + semver: 7.8.5 + + '@changesets/assemble-release-plan@7.0.0': + dependencies: + '@changesets/errors': 1.0.0 + '@changesets/get-dependents-graph': 3.0.0 + '@changesets/should-skip-package': 1.0.0 + '@changesets/types': 7.0.0 + semver: 7.8.5 + + '@changesets/changelog-git@1.0.0': + dependencies: + '@changesets/types': 7.0.0 + + '@changesets/cli@3.0.3': + dependencies: + '@changesets/apply-release-plan': 8.1.1 + '@changesets/assemble-release-plan': 7.0.0 + '@changesets/changelog-git': 1.0.0 + '@changesets/config': 4.0.1 + '@changesets/errors': 1.0.0 + '@changesets/get-dependents-graph': 3.0.0 + '@changesets/git': 4.0.1 + '@changesets/pre': 3.0.0 + '@changesets/read': 1.0.1 + '@changesets/should-skip-package': 1.0.0 + '@changesets/types': 7.0.0 + '@changesets/write': 1.0.1 + '@clack/prompts': 1.8.1 + '@manypkg/get-packages': 3.1.0 + '@pnpm/deps.graph-sequencer': 1100.0.1 + cac: 7.0.0 + import-meta-resolve: 4.2.0 + launch-editor: 2.14.1 + package-manager-detector: 1.9.0 + semver: 7.8.5 + tinyexec: 1.3.1 + + '@changesets/config@4.0.1': + dependencies: + '@changesets/get-dependents-graph': 3.0.0 + '@changesets/should-skip-package': 1.0.0 + '@changesets/types': 7.0.0 + '@manypkg/get-packages': 3.1.0 + picomatch: 4.0.7 + + '@changesets/errors@1.0.0': {} + + '@changesets/format@0.1.2': + dependencies: + package-manager-detector: 1.9.0 + tinyexec: 1.3.1 + + '@changesets/get-dependents-graph@3.0.0': + dependencies: + '@changesets/types': 7.0.0 + semver: 7.8.5 + + '@changesets/git@4.0.1': + dependencies: + '@changesets/errors': 1.0.0 + '@changesets/types': 7.0.0 + '@manypkg/get-packages': 3.1.0 + picomatch: 4.0.7 + tinyexec: 1.3.1 + + '@changesets/parse@1.0.0': + dependencies: + '@changesets/types': 7.0.0 + yaml: 2.9.1 + + '@changesets/pre@3.0.0': + dependencies: + '@changesets/errors': 1.0.0 + '@changesets/types': 7.0.0 + '@manypkg/get-packages': 3.1.0 + + '@changesets/read@1.0.1': + dependencies: + '@changesets/git': 4.0.1 + '@changesets/parse': 1.0.0 + '@changesets/types': 7.0.0 + + '@changesets/should-skip-package@1.0.0': + dependencies: + '@changesets/types': 7.0.0 + + '@changesets/types@7.0.0': {} + + '@changesets/write@1.0.1': + dependencies: + '@changesets/format': 0.1.2 + '@changesets/types': 7.0.0 + human-id: 4.2.2 + + '@clack/core@1.5.1': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.8.1': + dependencies: + '@clack/core': 1.5.1 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@emnapi/core@1.11.3': + dependencies: + '@emnapi/wasi-threads': 1.2.3 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@manypkg/find-root@3.1.0': + dependencies: + '@manypkg/tools': 2.1.2 + + '@manypkg/get-packages@3.1.0': + dependencies: + '@manypkg/find-root': 3.1.0 + '@manypkg/tools': 2.1.2 + + '@manypkg/tools@2.1.2': + dependencies: + jju: 1.4.0 + tinyglobby: 0.2.17 + yaml: 2.9.1 + + '@module-federation/bridge-react-webpack-plugin@2.9.2': + dependencies: + '@module-federation/sdk': 2.9.2 + + '@module-federation/cli@2.9.2(typescript@7.0.2)': + dependencies: + '@module-federation/dts-plugin': 2.9.2(typescript@7.0.2) + '@module-federation/sdk': 2.9.2 + commander: 11.1.0 + jiti: 2.4.2 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/dts-plugin@2.9.2(typescript@7.0.2)': + dependencies: + '@module-federation/error-codes': 2.9.2 + '@module-federation/managers': 2.9.2 + '@module-federation/sdk': 2.9.2 + '@module-federation/third-party-dts-extractor': 2.9.2 + adm-zip: 0.6.1 + isomorphic-ws: 5.0.0(ws@8.21.3) + typescript: 7.0.2 + undici: 7.29.1 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@module-federation/enhanced@2.9.2(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2)': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 2.9.2 + '@module-federation/cli': 2.9.2(typescript@7.0.2) + '@module-federation/dts-plugin': 2.9.2(typescript@7.0.2) + '@module-federation/error-codes': 2.9.2 + '@module-federation/inject-external-runtime-core-plugin': 2.9.2(@module-federation/runtime-tools@2.9.2) + '@module-federation/managers': 2.9.2 + '@module-federation/manifest': 2.9.2(typescript@7.0.2) + '@module-federation/rspack': 2.9.2(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/runtime-tools': 2.9.2 + '@module-federation/sdk': 2.9.2 + '@module-federation/webpack-bundler-runtime': 2.9.2 + schema-utils: 4.3.0 + tapable: 2.3.0 + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - utf-8-validate + + '@module-federation/error-codes@2.9.2': {} + + '@module-federation/inject-external-runtime-core-plugin@2.9.2(@module-federation/runtime-tools@2.9.2)': + dependencies: + '@module-federation/runtime-tools': 2.9.2 + + '@module-federation/managers@2.9.2': + dependencies: + '@module-federation/sdk': 2.9.2 + + '@module-federation/manifest@2.9.2(typescript@7.0.2)': + dependencies: + '@module-federation/dts-plugin': 2.9.2(typescript@7.0.2) + '@module-federation/managers': 2.9.2 + '@module-federation/sdk': 2.9.2 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/node@2.7.52(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2)': + dependencies: + '@module-federation/enhanced': 2.9.2(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/runtime': 2.9.2 + '@module-federation/sdk': 2.9.2 + encoding: 0.1.13 + node-fetch: 2.7.0(encoding@0.1.13) + tapable: 2.3.0 + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/rsbuild-plugin@2.9.2(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2)': + dependencies: + '@module-federation/enhanced': 2.9.2(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/node': 2.7.52(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/sdk': 2.9.2 + optionalDependencies: + '@rsbuild/core': 2.2.11(@module-federation/runtime-tools@2.9.2) + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - typescript + - utf-8-validate + - vue-tsc + - webpack + + '@module-federation/rspack@2.9.2(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(typescript@7.0.2)': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 2.9.2 + '@module-federation/dts-plugin': 2.9.2(typescript@7.0.2) + '@module-federation/inject-external-runtime-core-plugin': 2.9.2(@module-federation/runtime-tools@2.9.2) + '@module-federation/managers': 2.9.2 + '@module-federation/manifest': 2.9.2(typescript@7.0.2) + '@module-federation/runtime-tools': 2.9.2 + '@module-federation/sdk': 2.9.2 + '@rspack/core': 2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23) + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@module-federation/runtime-core@2.9.2': + dependencies: + '@module-federation/error-codes': 2.9.2 + '@module-federation/sdk': 2.9.2 + + '@module-federation/runtime-tools@2.9.2': + dependencies: + '@module-federation/runtime': 2.9.2 + '@module-federation/webpack-bundler-runtime': 2.9.2 + + '@module-federation/runtime@2.9.2': + dependencies: + '@module-federation/error-codes': 2.9.2 + '@module-federation/runtime-core': 2.9.2 + '@module-federation/sdk': 2.9.2 + + '@module-federation/sdk@2.9.2': {} + + '@module-federation/third-party-dts-extractor@2.9.2': {} + + '@module-federation/vite@1.23.3(typescript@7.0.2)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))': + dependencies: + '@module-federation/dts-plugin': 2.9.2(typescript@7.0.2) + '@module-federation/runtime': 2.9.2 + '@module-federation/sdk': 2.9.2 + vite: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/webpack-bundler-runtime@2.9.2': + dependencies: + '@module-federation/error-codes': 2.9.2 + '@module-federation/runtime': 2.9.2 + '@module-federation/sdk': 2.9.2 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@oozcitak/dom@2.0.2': + dependencies: + '@oozcitak/infra': 2.0.2 + '@oozcitak/url': 3.0.0 + '@oozcitak/util': 10.0.0 + + '@oozcitak/infra@2.0.2': + dependencies: + '@oozcitak/util': 10.0.0 + + '@oozcitak/url@3.0.0': + dependencies: + '@oozcitak/infra': 2.0.2 + '@oozcitak/util': 10.0.0 + + '@oozcitak/util@10.0.0': {} + + '@oxc-project/types@0.152.0': {} + + '@pnpm/deps.graph-sequencer@1100.0.1': {} + + '@rolldown/binding-android-arm-eabi@1.2.12': + optional: true + + '@rolldown/binding-android-arm64@1.2.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.12': + optional: true + + '@rolldown/binding-darwin-x64@1.2.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.12': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.12': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2)': + dependencies: + '@rspack/core': 2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23) + '@swc/helpers': 0.5.23 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + + '@rsbuild/plugin-react@2.1.1(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))': + dependencies: + '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-refresh@0.19.0) + react-refresh: 0.19.0 + optionalDependencies: + '@rsbuild/core': 2.2.11(@module-federation/runtime-tools@2.9.2) + transitivePeerDependencies: + - '@rspack/core' + + '@rslib/core@1.0.3(@module-federation/runtime-tools@2.9.2)(typescript@7.0.2)': + dependencies: + '@rsbuild/core': 2.2.11(@module-federation/runtime-tools@2.9.2) + rsbuild-plugin-dts: 1.0.3(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(typescript@7.0.2) + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - core-js + + '@rspack/binding-darwin-arm64@2.2.8': + optional: true + + '@rspack/binding-darwin-x64@2.2.8': + optional: true + + '@rspack/binding-linux-arm64-gnu@2.2.8': + optional: true + + '@rspack/binding-linux-arm64-musl@2.2.8': + optional: true + + '@rspack/binding-linux-ppc64-gnu@2.2.8': + optional: true + + '@rspack/binding-linux-riscv64-gnu@2.2.8': + optional: true + + '@rspack/binding-linux-riscv64-musl@2.2.8': + optional: true + + '@rspack/binding-linux-s390x-gnu@2.2.8': + optional: true + + '@rspack/binding-linux-x64-gnu@2.2.8': + optional: true + + '@rspack/binding-linux-x64-musl@2.2.8': + optional: true + + '@rspack/binding-wasm32-wasi@2.2.8': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + optional: true + + '@rspack/binding-win32-arm64-msvc@2.2.8': + optional: true + + '@rspack/binding-win32-ia32-msvc@2.2.8': + optional: true + + '@rspack/binding-win32-x64-msvc@2.2.8': + optional: true + + '@rspack/binding@2.2.8': + optionalDependencies: + '@rspack/binding-darwin-arm64': 2.2.8 + '@rspack/binding-darwin-x64': 2.2.8 + '@rspack/binding-linux-arm64-gnu': 2.2.8 + '@rspack/binding-linux-arm64-musl': 2.2.8 + '@rspack/binding-linux-ppc64-gnu': 2.2.8 + '@rspack/binding-linux-riscv64-gnu': 2.2.8 + '@rspack/binding-linux-riscv64-musl': 2.2.8 + '@rspack/binding-linux-s390x-gnu': 2.2.8 + '@rspack/binding-linux-x64-gnu': 2.2.8 + '@rspack/binding-linux-x64-musl': 2.2.8 + '@rspack/binding-wasm32-wasi': 2.2.8 + '@rspack/binding-win32-arm64-msvc': 2.2.8 + '@rspack/binding-win32-ia32-msvc': 2.2.8 + '@rspack/binding-win32-x64-msvc': 2.2.8 + + '@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23)': + dependencies: + '@rspack/binding': 2.2.8 + optionalDependencies: + '@module-federation/runtime-tools': 2.9.2 + '@swc/helpers': 0.5.23 + + '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-refresh@0.19.0)': + dependencies: + react-refresh: 0.19.0 + optionalDependencies: + '@rspack/core': 2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23) + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@tanstack/history@1.162.4': {} + + '@tanstack/react-router@1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@tanstack/history': 1.162.4 + '@tanstack/react-store': 0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/router-core': 1.171.34 + isbot: 5.2.2 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + + '@tanstack/react-start-client@1.168.39(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@tanstack/react-router': 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/router-core': 1.171.34 + '@tanstack/start-client-core': 1.170.34 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + + '@tanstack/react-start-rsc@0.1.59(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))': + dependencies: + '@tanstack/react-router': 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/router-core': 1.171.34 + '@tanstack/router-utils': 1.162.3 + '@tanstack/start-client-core': 1.170.34 + '@tanstack/start-fn-stubs': 1.162.0 + '@tanstack/start-plugin-core': 1.171.49(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(@tanstack/react-router@1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + '@tanstack/start-storage-context': 1.167.36 + pathe: 2.0.3 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@rspack/core': 2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23) + transitivePeerDependencies: + - '@farmfe/core' + - '@rsbuild/core' + - bun-types-no-globals + - crossws + - esbuild + - rolldown + - rollup + - supports-color + - unloader + - vite + - vite-plugin-solid + - webpack + + '@tanstack/react-start-server@1.167.46(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@tanstack/react-router': 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/router-core': 1.171.34 + '@tanstack/start-server-core': 1.169.39 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + transitivePeerDependencies: + - crossws + + '@tanstack/react-start@1.168.60(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))': + dependencies: + '@tanstack/react-router': 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/react-start-client': 1.168.39(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/react-start-rsc': 0.1.59(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + '@tanstack/react-start-server': 1.167.46(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/router-utils': 1.162.3 + '@tanstack/start-client-core': 1.170.34 + '@tanstack/start-plugin-core': 1.171.49(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(@tanstack/react-router@1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + '@tanstack/start-server-core': 1.169.39 + pathe: 2.0.3 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@rsbuild/core': 2.2.11(@module-federation/runtime-tools@2.9.2) + vite: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - crossws + - esbuild + - react-server-dom-rspack + - rolldown + - rollup + - supports-color + - unloader + - vite-plugin-solid + - webpack + + '@tanstack/react-store@0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@tanstack/store': 0.11.2 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + use-sync-external-store: 1.7.0(react@19.3.0) + + '@tanstack/router-core@1.171.34': + dependencies: + '@tanstack/history': 1.162.4 + cookie-es: 3.1.1 + seroval: 1.6.8 + seroval-plugins: 1.6.8(seroval@1.6.8) + + '@tanstack/router-generator@1.167.40': + dependencies: + '@babel/types': 7.29.8 + '@tanstack/router-core': 1.171.34 + '@tanstack/router-utils': 1.162.3 + '@tanstack/virtual-file-routes': 1.162.0 + jiti: 2.7.0 + magic-string: 0.30.21 + prettier: 3.9.9 + zod: 4.6.5 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.168.42(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(@tanstack/react-router@1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))': + dependencies: + '@babel/core': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + '@tanstack/router-core': 1.171.34 + '@tanstack/router-generator': 1.167.40 + '@tanstack/router-utils': 1.162.3 + chokidar: 5.0.0 + unplugin: 3.4.0(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + zod: 4.6.5 + optionalDependencies: + '@rsbuild/core': 2.2.11(@module-federation/runtime-tools@2.9.2) + '@tanstack/react-router': 1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + vite: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - supports-color + - unloader + + '@tanstack/router-utils@1.162.3': + dependencies: + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + ansis: 4.4.0 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.4 + pathe: 2.0.3 + tinyglobby: 0.2.17 + transitivePeerDependencies: + - supports-color + + '@tanstack/start-client-core@1.170.34': + dependencies: + '@tanstack/router-core': 1.171.34 + '@tanstack/start-fn-stubs': 1.162.0 + '@tanstack/start-storage-context': 1.167.36 + seroval: 1.6.8 + + '@tanstack/start-fn-stubs@1.162.0': {} + + '@tanstack/start-plugin-core@1.171.49(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(@tanstack/react-router@1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + '@tanstack/router-core': 1.171.34 + '@tanstack/router-generator': 1.167.40 + '@tanstack/router-plugin': 1.168.42(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(@tanstack/react-router@1.170.41(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + '@tanstack/router-utils': 1.162.3 + '@tanstack/start-server-core': 1.169.39 + exsolve: 1.1.3 + lightningcss: 1.33.0 + pathe: 2.0.3 + picomatch: 4.0.7 + seroval: 1.6.8 + source-map: 0.7.6 + srvx: 0.11.22 + tinyglobby: 0.2.17 + ufo: 1.6.4 + vitefu: 1.1.3(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)) + xmlbuilder2: 4.0.3 + zod: 4.6.5 + optionalDependencies: + '@rsbuild/core': 2.2.11(@module-federation/runtime-tools@2.9.2) + vite: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - '@tanstack/react-router' + - bun-types-no-globals + - crossws + - esbuild + - rolldown + - rollup + - supports-color + - unloader + - vite-plugin-solid + - webpack + + '@tanstack/start-server-core@1.169.39': + dependencies: + '@tanstack/history': 1.162.4 + '@tanstack/router-core': 1.171.34 + '@tanstack/start-client-core': 1.170.34 + '@tanstack/start-storage-context': 1.167.36 + fetchdts: 0.1.7 + h3-v2: h3@2.0.1-rc.20 + seroval: 1.6.8 + transitivePeerDependencies: + - crossws + + '@tanstack/start-storage-context@1.167.36': + dependencies: + '@tanstack/router-core': 1.171.34 + + '@tanstack/store@0.11.2': {} + + '@tanstack/virtual-file-routes@1.162.0': {} + + '@turbo/darwin-64@2.11.7': + optional: true + + '@turbo/darwin-arm64@2.11.7': + optional: true + + '@turbo/linux-64@2.11.7': + optional: true + + '@turbo/linux-arm64@2.11.7': + optional: true + + '@turbo/windows-64@2.11.7': + optional: true + + '@turbo/windows-arm64@2.11.7': + optional: true + + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/json-schema@7.0.15': {} + + '@types/node@26.6.4': + dependencies: + undici-types: 8.9.0 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@vitejs/plugin-react@6.1.2(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + + adm-zip@0.6.1: {} + + ajv-formats@2.1.1: + dependencies: + ajv: 8.20.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.8 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansis@4.4.0: {} + + argparse@2.0.1: {} + + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + baseline-browser-mapping@2.11.27: {} + + browserslist@4.29.3: + dependencies: + baseline-browser-mapping: 2.11.27 + caniuse-lite: 1.0.30001814 + electron-to-chromium: 1.5.444 + node-releases: 2.0.57 + update-browserslist-db: 1.3.3(browserslist@4.29.3) + + cac@7.0.0: {} + + caniuse-lite@1.0.30001814: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + commander@11.1.0: {} + + convert-source-map@2.0.0: {} + + cookie-es@3.1.1: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + diff@8.0.4: {} + + electron-to-chromium@1.5.444: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + + escalade@3.2.0: {} + + exsolve@1.1.3: {} + + fast-deep-equal@3.1.3: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-uri@3.1.8: {} + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fetchdts@0.1.7: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + h3@2.0.1-rc.20: + dependencies: + rou3: 0.8.1 + srvx: 0.11.22 + + human-id@4.2.2: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + import-meta-resolve@4.2.0: {} + + isbot@5.2.2: {} + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + jiti@2.4.2: {} + + jiti@2.7.0: {} + + jju@1.4.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + launch-editor@2.14.1: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.12.0 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + ms@2.1.3: {} + + nanoid@3.3.20: {} + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-releases@2.0.57: {} + + package-manager-detector@1.9.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + playwright-core@1.63.0: {} + + playwright@1.63.0: + dependencies: + playwright-core: 1.63.0 + + postcss@8.5.29: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + prettier@3.9.9: {} + + react-dom@19.3.0(react@19.3.0): + dependencies: + react: 19.3.0 + scheduler: 0.28.0 + + react-refresh@0.19.0: {} + + react@19.3.0: {} + + readdirp@5.1.1: {} + + require-from-string@2.0.2: {} + + rolldown@1.2.12: + dependencies: + '@oxc-project/types': 0.152.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.12 + '@rolldown/binding-android-arm64': 1.2.12 + '@rolldown/binding-darwin-arm64': 1.2.12 + '@rolldown/binding-darwin-x64': 1.2.12 + '@rolldown/binding-freebsd-x64': 1.2.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.12 + '@rolldown/binding-linux-arm64-gnu': 1.2.12 + '@rolldown/binding-linux-arm64-musl': 1.2.12 + '@rolldown/binding-linux-ppc64-gnu': 1.2.12 + '@rolldown/binding-linux-s390x-gnu': 1.2.12 + '@rolldown/binding-linux-x64-gnu': 1.2.12 + '@rolldown/binding-linux-x64-musl': 1.2.12 + '@rolldown/binding-openharmony-arm64': 1.2.12 + '@rolldown/binding-win32-arm64-msvc': 1.2.12 + '@rolldown/binding-win32-x64-msvc': 1.2.12 + + rou3@0.8.1: {} + + rsbuild-plugin-dts@1.0.3(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(typescript@7.0.2): + dependencies: + '@ast-grep/napi': 0.45.3 + '@rsbuild/core': 2.2.11(@module-federation/runtime-tools@2.9.2) + optionalDependencies: + typescript: 7.0.2 + + safer-buffer@2.1.2: {} + + scheduler@0.28.0: {} + + schema-utils@4.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1 + ajv-keywords: 5.1.0(ajv@8.20.0) + + semver@6.3.1: {} + + semver@7.8.5: {} + + seroval-plugins@1.6.8(seroval@1.6.8): + dependencies: + seroval: 1.6.8 + + seroval@1.6.8: {} + + shell-quote@1.12.0: {} + + sisteransi@1.0.5: {} + + source-map-js@1.2.2: {} + + source-map@0.7.6: {} + + srvx@0.11.22: {} + + tapable@2.3.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + turbo@2.11.7: + optionalDependencies: + '@turbo/darwin-64': 2.11.7 + '@turbo/darwin-arm64': 2.11.7 + '@turbo/linux-64': 2.11.7 + '@turbo/linux-arm64': 2.11.7 + '@turbo/windows-64': 2.11.7 + '@turbo/windows-arm64': 2.11.7 + + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + + ufo@1.6.4: {} + + undici-types@8.9.0: {} + + undici@7.29.1: {} + + unplugin@3.4.0(@rsbuild/core@2.2.11(@module-federation/runtime-tools@2.9.2))(@rspack/core@2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23))(rolldown@1.2.12)(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)): + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.7 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + '@rsbuild/core': 2.2.11(@module-federation/runtime-tools@2.9.2) + '@rspack/core': 2.2.8(@module-federation/runtime-tools@2.9.2)(@swc/helpers@0.5.23) + rolldown: 1.2.12 + vite: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + + update-browserslist-db@1.3.3(browserslist@4.29.3): + dependencies: + browserslist: 4.29.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.7.0(react@19.3.0): + dependencies: + react: 19.3.0 + + vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.29 + rolldown: 1.2.12 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.6.4 + fsevents: 2.3.3 + jiti: 2.7.0 + yaml: 2.9.1 + + vitefu@1.1.3(vite@8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)): + optionalDependencies: + vite: 8.3.2(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1) + + webidl-conversions@3.0.1: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + ws@8.21.3: {} + + xmlbuilder2@4.0.3: + dependencies: + '@oozcitak/dom': 2.0.2 + '@oozcitak/infra': 2.0.2 + '@oozcitak/util': 10.0.0 + js-yaml: 4.3.2 + + yallist@3.1.1: {} + + yaml@2.9.1: {} + + zod@4.6.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..1cc58b6 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +packages: + - apps/* + - packages/* + +autoInstallPeers: false +minimumReleaseAgeExclude: + - "@module-federation/vite@1.23.3" +preferFrozenLockfile: true diff --git a/test/example-build.test.mjs b/test/example-build.test.mjs new file mode 100644 index 0000000..d362d90 --- /dev/null +++ b/test/example-build.test.mjs @@ -0,0 +1,101 @@ +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const viteHostDist = join(root, "apps", "vite-host", "dist"); +const viteRemoteDist = join(root, "apps", "vite-remote", "dist"); +const rsbuildHostDist = join(root, "apps", "rsbuild-host", "dist"); +const rsbuildRemoteDist = join(root, "apps", "rsbuild-remote", "dist"); + +function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +test("all TanStack Start apps produce client and server builds", () => { + for (const [name, dist] of [ + ["Vite host", viteHostDist], + ["Vite remote", viteRemoteDist], + ["rsbuild host", rsbuildHostDist], + ["rsbuild remote", rsbuildRemoteDist], + ]) { + assert.ok(existsSync(join(dist, "client", "mf-manifest.json")), `${name} client`); + const serverEntry = name.startsWith("rsbuild") ? "index.js" : "server.js"; + assert.ok(existsSync(join(dist, "server", serverEntry)), `${name} server`); + } +}); + +test("remote publishes browser and server federation entries", () => { + const client = join(viteRemoteDist, "client"); + assert.ok(existsSync(join(client, "remoteEntry.js"))); + assert.ok(existsSync(join(client, "remoteEntry.ssr.js"))); + + const manifest = readJson(join(client, "mf-manifest.json")); + assert.equal(manifest.name, "tanstack_vite_remote"); + assert.equal(manifest.metaData.remoteEntry.name, "remoteEntry.js"); + assert.equal(manifest.metaData.ssrRemoteEntry.name, "remoteEntry.ssr.js"); + + const statusCard = manifest.exposes.find(({ path }) => path === "./StatusCard"); + assert.ok(statusCard, "StatusCard expose is listed"); + assert.ok(statusCard.assets.css.sync.length > 0, "StatusCard CSS is published"); + + for (const dependency of ["react", "react-dom"]) { + const shared = manifest.shared.find(({ name }) => name === dependency); + assert.equal(shared?.singleton, true, `${dependency} is a singleton`); + } +}); + +test("Vite host records the TanStack remote and server-side loader", () => { + const manifest = readJson(join(viteHostDist, "client", "mf-manifest.json")); + const remote = manifest.remotes.find(({ alias }) => alias === "tanstack_vite_remote"); + assert.equal(remote?.moduleName, "StatusCard"); + + const serverFiles = readFileSync(join(viteHostDist, "server", ".vite", "manifest.json"), "utf8"); + assert.match(serverFiles, /ssrEntryLoader/); + assert.match(serverFiles, /tanstack_vite_remote/); +}); + +test("Vite and Rsbuild publish reciprocal client interoperability contracts", () => { + const viteHostManifest = readJson(join(viteHostDist, "client", "mf-manifest.json")); + const rsbuildHostManifest = readJson(join(rsbuildHostDist, "client", "mf-manifest.json")); + const rsbuildRemoteClient = readJson(join(rsbuildRemoteDist, "client", "mf-manifest.json")); + + assert.equal( + viteHostManifest.remotes.find(({ alias }) => alias === "tanstack_rsbuild_remote")?.moduleName, + "StatusCard", + ); + assert.equal( + rsbuildHostManifest.remotes.find(({ alias }) => alias === "tanstack_vite_remote")?.entry, + "http://127.0.0.1:3001/mf-manifest.json", + ); + assert.equal( + rsbuildHostManifest.remotes.find(({ alias }) => alias === "tanstack_rsbuild_remote")?.entry, + "http://127.0.0.1:3002/mf-manifest.json", + ); + assert.equal(rsbuildRemoteClient.metaData.remoteEntry.type, "global"); + assert.equal(rsbuildRemoteClient.metaData.remoteEntry.name, "remoteEntry.js"); + const statusCard = rsbuildRemoteClient.exposes.find(({ path }) => path === "./StatusCard"); + assert.ok(statusCard?.assets.css.sync.length > 0, "Rsbuild StatusCard CSS is published"); + assert.equal( + existsSync(join(rsbuildRemoteDist, "server", "serverRemoteEntry.cjs")), + false, + "client-only cross-bundler remote does not publish an unused SSR container", + ); + assert.equal( + existsSync(join(rsbuildHostDist, "server", "mf-manifest.json")), + false, + "client-only cross-bundler host does not initialize remotes during SSR", + ); + + for (const manifest of [rsbuildRemoteClient]) { + for (const dependency of ["react", "react-dom"]) { + assert.equal( + manifest.shared.find(({ name }) => name === dependency)?.singleton, + true, + `${dependency} is a singleton`, + ); + } + } +}); diff --git a/test/example-production.test.mjs b/test/example-production.test.mjs new file mode 100644 index 0000000..e2686d5 --- /dev/null +++ b/test/example-production.test.mjs @@ -0,0 +1,4 @@ +// Production builds served by `vite preview` and `rsbuild preview`. +import { describeExamples } from "./support/examples.mjs"; + +describeExamples("preview"); diff --git a/test/example-runtime.test.mjs b/test/example-runtime.test.mjs new file mode 100644 index 0000000..2b0b102 --- /dev/null +++ b/test/example-runtime.test.mjs @@ -0,0 +1,4 @@ +// Development servers: Vite and Rsbuild dev servers with in-memory builds. +import { describeExamples } from "./support/examples.mjs"; + +describeExamples("start"); diff --git a/test/release-artifacts.test.mjs b/test/release-artifacts.test.mjs new file mode 100644 index 0000000..f7399af --- /dev/null +++ b/test/release-artifacts.test.mjs @@ -0,0 +1,73 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const packageRoot = join(root, "packages", "tanstack"); +const dist = join(packageRoot, "dist"); + +test("published declarations reference included source maps", () => { + for (const declarationName of [ + "index.d.cts", + "index.d.ts", + "node-entry-loader.d.cts", + "node-entry-loader.d.ts", + "rsbuild.d.cts", + "rsbuild.d.ts", + "runtime.d.cts", + "runtime.d.ts", + "vite.d.cts", + "vite.d.ts", + ]) { + const declaration = readFileSync(join(dist, declarationName), "utf8"); + const sourceMap = declaration.match(/\/\/[#@]\s*sourceMappingURL=([^\s]+)\s*$/m)?.[1]; + + assert.ok(sourceMap, `${declarationName} has no source map reference`); + assert.ok( + existsSync(join(dist, sourceMap)), + `${declarationName} references missing source map ${sourceMap}`, + ); + } +}); + +test("npm package contains only runtime, metadata, and documentation files", () => { + const packed = spawnSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], { + cwd: packageRoot, + encoding: "utf8", + }); + + assert.equal(packed.status, 0, packed.stderr); + const [manifest] = JSON.parse(packed.stdout); + const files = manifest.files.map(({ path }) => path).sort(); + + const entries = [ + "index", + "node-entry-loader", + "rsbuild", + "runtime", + "shared", + "vite", + "vite-compat", + ]; + const runtimeFiles = entries.flatMap((entry) => { + const sourceMaps = entry === "index" ? [] : [`dist/${entry}.js.map`]; + return [ + `dist/${entry}.cjs`, + `dist/${entry}.cjs.map`, + `dist/${entry}.d.cts`, + `dist/${entry}.d.cts.map`, + `dist/${entry}.d.ts`, + `dist/${entry}.d.ts.map`, + `dist/${entry}.js`, + ...sourceMaps, + ]; + }); + + assert.deepEqual( + files, + ["CHANGELOG.md", "LICENSE", "README.md", ...runtimeFiles, "package.json"].sort(), + ); +}); diff --git a/test/release-contracts.test.mjs b/test/release-contracts.test.mjs new file mode 100644 index 0000000..90ba1f0 --- /dev/null +++ b/test/release-contracts.test.mjs @@ -0,0 +1,582 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import { tmpdir } from "node:os"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import test from "node:test"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const packageRoot = join(root, "packages", "tanstack"); +const packageRequire = createRequire(join(packageRoot, "package.json")); +const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")); +const rootPackageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); +const ciWorkflow = readFileSync(join(root, ".github", "workflows", "ci.yml"), "utf8"); +const previewWorkflow = readFileSync(join(root, ".github", "workflows", "pkg-pr-new.yml"), "utf8"); +const releaseWorkflow = readFileSync(join(root, ".github", "workflows", "release.yml"), "utf8"); +const { tanstackStartModuleFederation } = await import( + pathToFileURL(join(packageRoot, "dist", "index.js")).href +); +const { eagerShared, resolveShared } = await import( + pathToFileURL(join(packageRoot, "dist", "shared.js")).href +); +const { tanstackStartModuleFederation: tanstackStartRsbuildModuleFederation } = await import( + pathToFileURL(join(packageRoot, "dist", "rsbuild.js")).href +); + +function upstreamOptions(options) { + const plugins = tanstackStartModuleFederation(options); + assert.ok(Array.isArray(plugins), "federation wrapper returns Vite plugins"); + const upstream = plugins.filter((plugin) => plugin.name === "module-federation-vite"); + assert.equal(upstream.length, 1, "wrapper delegates to one federation instance"); + assert.ok(upstream[0]._options, "upstream plugin exposes normalized options"); + return upstream[0]._options; +} + +test("applies TanStack-safe defaults", () => { + const options = upstreamOptions({ + name: "host", + filename: undefined, + manifest: undefined, + hostInitInjectLocation: undefined, + }); + assert.equal(options.filename, "remoteEntry.js"); + assert.equal(options.manifest, true); + assert.equal(options.hostInitInjectLocation, "entry"); + assert.equal(options.shared.react.shareConfig.singleton, true); + assert.equal(options.shared.react.shareConfig.eager, false, "remote-only builds stay lazy"); + assert.equal(options.shared["react-dom"].shareConfig.singleton, true); +}); + +test("Vite hosts provide their own React eagerly so remotes cannot replace it", () => { + const options = upstreamOptions({ + name: "host", + remotes: { remote: { type: "module", name: "remote", entry: "remoteEntry.js" } }, + }); + for (const dependency of ["react", "react-dom"]) { + assert.equal(options.shared[dependency].shareConfig.singleton, true); + assert.equal(options.shared[dependency].shareConfig.eager, true, `${dependency} is eager`); + } + + const overridden = upstreamOptions({ + name: "host", + remotes: { remote: { type: "module", name: "remote", entry: "remoteEntry.js" } }, + shared: { react: { singleton: true } }, + }); + assert.equal(overridden.shared.react.shareConfig.eager, false, "explicit entries win"); +}); + +test("preserves explicit federation settings and shared entries", () => { + const options = upstreamOptions({ + name: "remote", + filename: "custom-entry.js", + manifest: false, + hostInitInjectLocation: "html", + shared: { + react: { singleton: false, requiredVersion: "^19.0.0" }, + lodash: { singleton: true }, + }, + }); + assert.equal(options.filename, "custom-entry.js"); + assert.equal(options.manifest, false); + assert.equal(options.hostInitInjectLocation, "html"); + assert.equal(options.shared.react.shareConfig.singleton, false); + assert.equal(options.shared.react.shareConfig.requiredVersion, "^19.0.0"); + assert.equal(options.shared.lodash.shareConfig.singleton, true); + assert.ok(options.shared["react-dom"], "unconfigured React DOM remains shared"); +}); + +test("does not mutate the caller's options", () => { + const options = { + name: "host", + shared: { react: { singleton: false } }, + remotes: { remote: { type: "module", name: "remote", entry: "remoteEntry.js" } }, + }; + const before = structuredClone(options); + upstreamOptions(options); + assert.deepEqual(options, before); +}); + +test("accepts the upstream array form", () => { + const options = upstreamOptions({ name: "remote", shared: ["lodash", "react"] }); + assert.deepEqual(Object.keys(options.shared).sort(), ["lodash", "react", "react-dom"]); + assert.equal(options.shared.react.shareConfig.singleton, true); + assert.equal(options.shared["react-dom"].shareConfig.singleton, true); +}); + +test("leaves the build target to each TanStack environment", () => { + const options = upstreamOptions({ name: "host", target: "node" }); + assert.equal(options.target, undefined); +}); + +function vitePlugin(name) { + const plugin = tanstackStartModuleFederation({ name: "host" }).find( + (candidate) => candidate.name === name, + ); + assert.ok(plugin, `${name} is registered`); + return plugin; +} + +test("the Vite adapter fails fast on Vite 7", () => { + const plugin = vitePlugin("tanstack-start-federation:vite-version"); + assert.throws( + () => plugin.config.call({ meta: { viteVersion: "7.3.6" } }), + /requires Vite 8; this project runs Vite 7\.3\.6/, + ); + assert.doesNotThrow(() => plugin.config.call({ meta: { viteVersion: "8.3.2" } })); +}); + +test("Rsbuild shared defaults never mutate caller overrides", () => { + const callerShared = { react: { eager: false, singleton: false } }; + const before = structuredClone(callerShared); + const shared = resolveShared(callerShared, eagerShared); + + assert.deepEqual(callerShared, before); + assert.deepEqual(shared.react, { eager: false, singleton: false }); + assert.deepEqual(shared["react-dom"], { eager: true, singleton: true }); +}); + +const rsbuildRemote = { name: "test_remote", exposes: { "./Card": "./src/Card.tsx" } }; +const rsbuildHost = { + name: "test_host", + remotes: { remote: "remote@http://remote/mf-manifest.json" }, +}; + +test("Rsbuild hosts share eager React; remotes and server containers stay lazy", () => { + const [hostClient, hostServer] = federationOptions({ federation: rsbuildHost, server: true }); + const [remoteClient, remoteContainer] = federationOptions({ + federation: rsbuildRemote, + server: true, + }); + + for (const options of [hostClient, hostServer]) { + assert.deepEqual(options.shared.react, { eager: true, singleton: true }); + assert.deepEqual(options.shared["react-dom"], { eager: true, singleton: true }); + } + for (const options of [remoteClient, remoteContainer]) { + assert.deepEqual(options.shared.react, { singleton: true }); + assert.deepEqual(options.shared["react-dom"], { singleton: true }); + } +}); + +test("Rsbuild enables async startup unless the caller disables it", () => { + const [enabled] = federationOptions({ federation: rsbuildRemote }); + assert.equal(enabled.experiments.asyncStartup, true); + + const [disabled] = federationOptions({ + federation: { ...rsbuildRemote, experiments: { asyncStartup: false } }, + }); + assert.equal(disabled.experiments.asyncStartup, false); +}); + +test("Rsbuild federation is browser-only by default", () => { + const pluginNames = (federation) => + tanstackStartRsbuildModuleFederation({ federation }).map(({ name }) => name); + assert.deepEqual(pluginNames(rsbuildRemote), [ + "rsbuild:module-federation-enhanced", + "tanstack-start-federation-client-compat", + ]); + assert.deepEqual(pluginNames(rsbuildHost), [ + "rsbuild:module-federation-enhanced", + "tanstack-start-federation-remote-retry", + "tanstack-start-federation-client-compat", + ]); +}); + +test("Rsbuild remotes resolve browser chunks from their own origin", () => { + const remoteClient = { name: "client", output: { publicPath: "/" } }; + runHooks(clientCompat(rsbuildRemote)).onBeforeCreateCompiler({ bundlerConfigs: [remoteClient] }); + assert.equal(remoteClient.output.publicPath, "auto"); + + const cdnClient = { name: "client", output: { publicPath: "https://cdn.example/remote/" } }; + runHooks(clientCompat(rsbuildRemote)).onBeforeCreateCompiler({ bundlerConfigs: [cdnClient] }); + assert.equal(cdnClient.output.publicPath, "https://cdn.example/remote/"); + + const hostClient = { name: "client", output: { publicPath: "/" } }; + runHooks(clientCompat(rsbuildHost)).onBeforeCreateCompiler({ bundlerConfigs: [hostClient] }); + assert.deepEqual(hostClient.output, { + chunkFormat: "array-push", + chunkLoading: "jsonp", + chunkLoadingGlobal: "chunk_test_host", + module: false, + publicPath: "/", + uniqueName: "test_host", + }); +}); + +test("Rsbuild hosts can load a remote again after it failed", () => { + const plugin = tanstackStartRsbuildModuleFederation({ federation: rsbuildHost }).find( + ({ name }) => name === "tanstack-start-federation-remote-retry", + ); + let transform; + plugin.setup({ transform: (options, handler) => (transform = { handler, options }) }); + const runtimeRoot = findPackageRoot( + createRequire(packageRequire.resolve("@module-federation/rsbuild-plugin")).resolve( + "@module-federation/webpack-bundler-runtime", + ), + ); + for (const file of ["remotes.cjs", "remotes.js"]) { + const path = join(runtimeRoot, "dist", file); + assert.ok(transform.options.test.test(path), file); + const code = readFileSync(path, "utf8"); + const patched = transform.handler({ code }); + assert.equal(patched.split("\n").length, code.split("\n").length, `${file} keeps its lines`); + // Without strict handling, a module that threw stays cached with empty exports; Rspack's + // strict handling caches the error instead. The failed module must leave the cache. + assert.match( + patched, + /webpackRequire\.m\[id\] = \(\) => \{ if \(webpackRequire\.c\) delete webpackRequire\.c\[id\];\s*throw error;/, + file, + ); + } +}); + +test("Rsbuild SSR remotes ship a Node container with the browser assets", async () => { + const plugins = tanstackStartRsbuildModuleFederation({ federation: rsbuildRemote, server: true }); + assert.deepEqual( + plugins.map(({ name }) => name), + [ + "rsbuild:module-federation-enhanced", + "tanstack-start-federation-client-compat", + "tanstack-start-federation-server-container", + "rsbuild:module-federation-enhanced", + ], + ); + + const hooks = runHooks( + plugins.find(({ name }) => name === "tanstack-start-federation-server-container"), + ); + const rsbuildConfig = { environments: { client: {}, ssr: {} } }; + assert.equal(hooks.modifyRsbuildConfig.order, "pre"); + hooks.modifyRsbuildConfig.handler(rsbuildConfig); + assert.deepEqual(rsbuildConfig.environments["mf-server"], { + source: { entry: { "mf-server": "data:text/javascript," } }, + output: { cleanDistPath: false, emitAssets: false, target: "node" }, + }); + + // The container sits next to the manifest; its chunks load over HTTP from the same + // public path as the browser assets. + for (const publicPath of ["http://127.0.0.1:3004/", "auto"]) { + const client = { name: "client", output: { path: "/app/dist/client", publicPath } }; + const container = { name: "mf-server", output: {} }; + hooks.onBeforeCreateCompiler({ bundlerConfigs: [client, container] }); + assert.deepEqual(container.output, { + chunkFilename: "ssr/[id].[contenthash:8].js", + filename: "ssr/[name].js", + path: "/app/dist/client", + publicPath, + }); + } + assert.throws( + () => hooks.onBeforeCreateCompiler({ bundlerConfigs: [{ name: "mf-server", output: {} }] }), + /Register tanstackStart\(\) before tanstackStartModuleFederation\(\)/, + ); + + // The container is emitted as Module Federation builds it. + assert.equal(hooks.processAssets, undefined); + + const [client, container] = federationOptions({ federation: rsbuildRemote, server: true }); + assert.equal(container.filename, "remoteEntry.ssr.cjs"); + assert.equal(container.manifest, false); + assert.equal(container.remotes, undefined); + const stats = await client.manifest.additionalData({ stats: { metaData: {} } }); + assert.deepEqual(stats.metaData.ssrRemoteEntry, { + name: "remoteEntry.ssr.cjs", + path: "", + type: "commonjs-module", + }); +}); + +test("Rsbuild SSR hosts load remotes from an async-node CommonJS server", () => { + const plugins = tanstackStartRsbuildModuleFederation({ federation: rsbuildHost, server: true }); + assert.deepEqual( + plugins.map(({ name }) => name), + [ + "rsbuild:module-federation-enhanced", + "tanstack-start-federation-remote-retry", + "tanstack-start-federation-client-compat", + "rsbuild:module-federation-enhanced", + "tanstack-start-federation-ssr-compat", + ], + ); + + const [, server] = federationOptions({ federation: rsbuildHost, server: true }); + assert.equal(server.manifest, false); + assert.equal(server.exposes, undefined); + assert.ok(server.runtimePlugins.includes("@module-federation/tanstack/node-entry-loader")); + + const hooks = runHooks( + plugins.find(({ name }) => name === "tanstack-start-federation-ssr-compat"), + ); + const serverConfig = { name: "ssr", output: { chunkLoadingGlobal: "stale" } }; + hooks.onBeforeCreateCompiler({ bundlerConfigs: [{ name: "client", output: {} }, serverConfig] }); + assert.equal(serverConfig.target, "async-node"); + assert.deepEqual(serverConfig.output, { + chunkFilename: "[id].test_host.cjs", + chunkFormat: "commonjs", + chunkLoading: "async-node", + filename: "[name].cjs", + library: { type: "commonjs2" }, + module: false, + }); + + const environmentConfig = () => ({ dev: {}, output: { distPath: { root: "dist/server" } } }); + const buildConfig = environmentConfig(); + hooks.modifyEnvironmentConfig(buildConfig, { name: "ssr" }); + assert.equal(buildConfig.dev.writeToDisk, true, "async-node chunks are read from disk"); + assert.equal(buildConfig.output.distPath.root, "dist/server"); + + // A dev server writes its bundle elsewhere, so a production build in dist/server survives. + const devHooks = runHooks( + plugins.find(({ name }) => name === "tanstack-start-federation-ssr-compat"), + { action: "dev" }, + ); + const devConfig = environmentConfig(); + devHooks.modifyEnvironmentConfig(devConfig, { name: "ssr" }); + assert.equal(devConfig.dev.writeToDisk, true); + assert.equal( + devConfig.output.distPath.root, + join("/app", "node_modules", ".cache", "tanstack-start-federation", "ssr"), + ); + + const emitted = {}; + hooks.processAssets.handler({ + assets: { "index.cjs": {} }, + compilation: { emitAsset: (name, source) => (emitted[name] = source.source()) }, + sources: { + RawSource: class { + constructor(value) { + this.value = value; + } + source() { + return this.value; + } + }, + }, + }); + assert.deepEqual(hooks.processAssets.options, { stage: "additional", environments: ["ssr"] }); + assert.match(emitted["index.js"], /await createRequire\(import\.meta\.url\)\("\.\/index\.cjs"\)/); +}); + +test("Rsbuild ESM server output is left to TanStack Start", () => { + const plugins = tanstackStartRsbuildModuleFederation({ + federation: rsbuildHost, + server: { forceCommonJsOutput: false }, + }); + const hooks = runHooks( + plugins.find(({ name }) => name === "tanstack-start-federation-ssr-compat"), + ); + const serverConfig = { name: "ssr", output: { chunkFilename: "[id].js", filename: "[name].js" } }; + hooks.onBeforeCreateCompiler({ bundlerConfigs: [serverConfig] }); + assert.deepEqual(serverConfig, { + name: "ssr", + output: { chunkFilename: "[id].js", filename: "[name].js" }, + }); + assert.equal(hooks.processAssets, undefined); + assert.equal(hooks.modifyEnvironmentConfig, undefined); +}); + +function clientCompat(federation) { + return tanstackStartRsbuildModuleFederation({ federation }).find( + ({ name }) => name === "tanstack-start-federation-client-compat", + ); +} + +/** Returns the Module Federation options each enhanced plugin receives, in plugin order. */ +function federationOptions(options) { + return tanstackStartRsbuildModuleFederation(options) + .filter(({ name }) => name === "rsbuild:module-federation-enhanced") + .map((plugin) => { + let exposed; + plugin.setup( + new Proxy( + {}, + { + get(_, key) { + if (key === "context") return { callerName: "rsbuild" }; + if (key === "expose") return (_name, api) => (exposed = api); + if (key === "getRsbuildConfig") return () => ({}); + return () => {}; + }, + }, + ), + ); + return exposed.getOptions(); + }); +} + +/** Runs a plugin's setup against a recording API and returns the hooks it registered. */ +function runHooks(plugin, { action = "build" } = {}) { + const hooks = {}; + plugin.setup({ + context: { action, rootPath: "/app" }, + modifyEnvironmentConfig: (handler) => (hooks.modifyEnvironmentConfig = handler), + modifyRsbuildConfig: (hook) => (hooks.modifyRsbuildConfig = hook), + onBeforeCreateCompiler: (handler) => (hooks.onBeforeCreateCompiler = handler), + processAssets: (options, handler) => (hooks.processAssets = { handler, options }), + }); + return hooks; +} + +test("published metadata selects ESM and CommonJS builds with matching types", () => { + assert.equal(packageJson.type, "module"); + assert.equal(packageJson.exports["."].import.default, "./dist/index.js"); + assert.equal(packageJson.exports["."].import.types, "./dist/index.d.ts"); + assert.equal(packageJson.exports["."].require.default, "./dist/index.cjs"); + assert.equal(packageJson.exports["."].require.types, "./dist/index.d.cts"); + assert.equal( + packageRequire.resolve("@module-federation/tanstack"), + join(packageRoot, "dist", "index.cjs"), + ); + for (const file of ["index.cjs", "index.js", "index.d.cts", "index.d.ts"]) { + assert.ok(existsSync(join(packageRoot, "dist", file)), `dist/${file} exists`); + } +}); + +test("clean consumers resolve CJS and ESM entrypoints without the other adapter", () => { + const viteConsumer = createConsumer(["@module-federation/vite"]); + const rsbuildConsumer = createConsumer(["@module-federation/rsbuild-plugin", "@rsbuild/core"]); + + try { + const viteRequire = createRequire(join(viteConsumer, "consumer.cjs")); + const rsbuildRequire = createRequire(join(rsbuildConsumer, "consumer.cjs")); + const cjsRoot = viteRequire("@module-federation/tanstack"); + const cjsRsbuild = rsbuildRequire("@module-federation/tanstack/rsbuild"); + + assert.equal(typeof cjsRoot.tanstackStartModuleFederation, "function"); + assert.equal(typeof cjsRsbuild.tanstackStartModuleFederation, "function"); + assertConsumerImport(viteConsumer, "@module-federation/tanstack"); + assertConsumerImport(viteConsumer, "@module-federation/tanstack/vite"); + assertConsumerImport(rsbuildConsumer, "@module-federation/tanstack/rsbuild"); + } finally { + rmSync(viteConsumer, { force: true, recursive: true }); + rmSync(rsbuildConsumer, { force: true, recursive: true }); + } + + assert.doesNotMatch( + readFileSync(join(packageRoot, "dist", "rsbuild.js"), "utf8"), + /@module-federation\/vite/, + ); + assert.doesNotMatch( + readFileSync(join(packageRoot, "dist", "vite.js"), "utf8"), + /@module-federation\/rsbuild-plugin/, + ); +}); + +test("runtime subpaths load without a bundler adapter", () => { + // Route code imports the runtime helpers, so React is always installed next to them. + const consumer = createConsumer(["react"]); + try { + const consumerRequire = createRequire(join(consumer, "consumer.cjs")); + const runtime = consumerRequire("@module-federation/tanstack/runtime"); + const loader = consumerRequire("@module-federation/tanstack/node-entry-loader"); + assert.equal(typeof runtime.getRemoteStylesheets, "function"); + assert.equal(typeof runtime.lazyRemote, "function"); + assert.equal(typeof loader.default, "function"); + assert.equal(typeof loader.RemoteEntryError, "function"); + } finally { + rmSync(consumer, { force: true, recursive: true }); + } + + // Route code imports the runtime helpers in the browser too. + for (const file of ["runtime.js", "runtime.cjs"]) { + assert.doesNotMatch( + readFileSync(join(packageRoot, "dist", file), "utf8"), + /node:|@module-federation\/(?:vite|rsbuild-plugin)|@rsbuild\/core/, + file, + ); + } +}); + +function assertConsumerImport(consumer, specifier) { + const entry = join(consumer, "consumer.mjs"); + writeFileSync( + entry, + `import { tanstackStartModuleFederation } from ${JSON.stringify(specifier)};\n` + + `if (typeof tanstackStartModuleFederation !== "function") process.exit(2);\n`, + ); + const result = spawnSync(process.execPath, [entry], { + cwd: consumer, + encoding: "utf8", + }); + assert.equal(result.status, 0, result.stderr); +} + +function createConsumer(adapterPackages) { + const consumer = mkdtempSync(join(tmpdir(), "mf-tanstack-consumer-")); + const packageTarget = join(consumer, "node_modules", "@module-federation", "tanstack"); + mkdirSync(dirname(packageTarget), { recursive: true }); + cpSync(packageRoot, packageTarget, { + filter: (source) => !source.includes("/node_modules/") && !source.includes("/.turbo/"), + recursive: true, + }); + + for (const packageName of adapterPackages) { + const adapterRoot = findPackageRoot(packageRequire.resolve(packageName)); + const adapterTarget = join(consumer, "node_modules", ...packageName.split("/")); + mkdirSync(dirname(adapterTarget), { recursive: true }); + symlinkSync(adapterRoot, adapterTarget); + } + + return consumer; +} + +function findPackageRoot(entryPoint) { + let directory = dirname(entryPoint); + while (!existsSync(join(directory, "package.json"))) { + const parent = dirname(directory); + if (parent === directory) throw new Error(`Could not find package root for ${entryPoint}`); + directory = parent; + } + return directory; +} + +test("CI uses Node versions supported by the package build toolchain", () => { + const supportedNode = "^22.18.0 || ^24.11.0 || >=26.0.0"; + assert.equal(rootPackageJson.engines.node, supportedNode); + assert.equal(packageJson.engines.node, supportedNode); + assert.match(ciWorkflow, /node: \["22\.18\.0", "24", "26"\]/); + assert.doesNotMatch(ciWorkflow, /22\.12\.0/); +}); + +test("bundler adapters are isolated optional peers", () => { + for (const dependency of [ + "@module-federation/rsbuild-plugin", + "@module-federation/vite", + "@rsbuild/core", + "vite", + ]) { + assert.ok(packageJson.peerDependencies[dependency], `${dependency} has a peer range`); + assert.equal(packageJson.peerDependenciesMeta[dependency]?.optional, true); + } + assert.equal(packageJson.optionalDependencies, undefined); +}); + +test("GitHub prereleases publish the version validated against their tag", () => { + assert.match(releaseWorkflow, /Tag\/version mismatch/); + assert.doesNotMatch(releaseWorkflow, /Set deterministic prerelease version/); + assert.doesNotMatch( + releaseWorkflow, + /github\.event\.release\.prerelease[\s\S]*npm pkg set version/, + ); +}); + +test("npm publications share one package-wide concurrency queue", () => { + assert.match(releaseWorkflow, /group: publish-module-federation-tanstack/); + assert.doesNotMatch(releaseWorkflow, /group:.*github\.(?:ref|run)/); +}); + +test("first package preview does not require an existing npm release", () => { + assert.match(previewWorkflow, /pkg-pr-new@0\.0\.54 publish \.\/packages\/tanstack/); + assert.doesNotMatch(previewWorkflow, /--compact/); +}); diff --git a/test/runtime.test.mjs b/test/runtime.test.mjs new file mode 100644 index 0000000..9cd4e95 --- /dev/null +++ b/test/runtime.test.mjs @@ -0,0 +1,307 @@ +import assert from "node:assert/strict"; +import { execFile, spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { createServer } from "node:http"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { promisify } from "node:util"; +import { after, before, test } from "node:test"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const dist = join(root, "packages", "tanstack", "dist"); +const loaderUrl = pathToFileURL(join(dist, "node-entry-loader.js")).href; +const { default: nodeEntryLoader, RemoteEntryError } = await import(loaderUrl); +const { getRemoteStylesheets, lazyRemote } = await import( + pathToFileURL(join(dist, "runtime.js")).href +); +// The React copy the runtime resolves. +const packageRequire = createRequire(join(root, "packages", "tanstack", "package.json")); +const { createElement, Suspense } = packageRequire("react"); +const { renderToString } = packageRequire("react-dom/server"); +const { prerenderToNodeStream } = packageRequire("react-dom/static"); + +const files = new Map(); +const requests = []; +let server; +let origin; + +before(async () => { + server = createServer((request, response) => { + requests.push(request.url); + const file = files.get(request.url); + if (!file) { + response.writeHead(404).end(); + return; + } + response.writeHead(200, { "content-type": file.type }).end(file.body); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + origin = `http://127.0.0.1:${server.address().port}`; +}); + +after(() => server.close()); + +function serve(path, body, type = "text/javascript") { + files.set(path, { body, type }); + return `${origin}${path}`; +} + +function loadEntry(entry, type = "commonjs-module") { + const remoteInfo = { entry, entryGlobalName: "remote", name: "remote", type }; + return nodeEntryLoader().loadEntry({ origin: {}, remoteInfo }); +} + +test("loads HTTP CommonJS containers as CommonJS modules", async () => { + // Rsbuild containers import Node built-ins through `new Function` at runtime. + const entry = serve( + "/cjs/remoteEntry.ssr.cjs", + ` + const importNode = new Function("name", "return import(name)"); + module.exports = { + init() {}, + async get(request) { + const path = await importNode("node:path"); + return () => ({ request, separator: path.sep, filename: __filename }); + }, + }; + `, + ); + + const container = await loadEntry(entry); + const factory = await container.get("./Card"); + assert.deepEqual(factory(), { request: "./Card", separator: "/", filename: entry }); +}); + +test("reports which step failed and keeps the cause", async () => { + await assert.rejects(loadEntry(`${origin}/missing/remoteEntry.ssr.cjs`), (error) => { + assert.ok(error instanceof RemoteEntryError); + assert.equal(error.phase, "fetch"); + assert.equal(error.remote, "remote"); + assert.equal(error.url, `${origin}/missing/remoteEntry.ssr.cjs`); + assert.match(error.cause.message, /HTTP 404/); + return true; + }); + + const closed = createServer(); + await new Promise((resolve) => closed.listen(0, "127.0.0.1", resolve)); + const offline = `http://127.0.0.1:${closed.address().port}/remoteEntry.ssr.cjs`; + await new Promise((resolve) => closed.close(resolve)); + await assert.rejects(loadEntry(offline), { name: "RemoteEntryError", phase: "fetch" }); + + const throwing = serve("/throws/remoteEntry.ssr.cjs", 'throw new Error("container failed");'); + await assert.rejects(loadEntry(throwing), (error) => { + assert.equal(error.phase, "evaluate"); + assert.equal(error.cause.message, "container failed"); + return true; + }); + + const empty = serve("/empty/remoteEntry.ssr.cjs", "module.exports = {};"); + await assert.rejects(loadEntry(empty), { + message: /did not export a federation container/, + phase: "evaluate", + }); +}); + +test("leaves other entries to the federation runtime", async () => { + assert.equal(await loadEntry(`${origin}/remoteEntry.js`, "global"), undefined); + assert.equal(await loadEntry("/local/remoteEntry.ssr.cjs"), undefined); +}); + +test("Vite remotes ask for @module-federation/vite when the host lacks it", () => { + const app = mkdtempSync(join(tmpdir(), "mf-tanstack-host-")); + try { + const script = join(app, "server.mjs"); + writeFileSync( + script, + ` + const { default: loader } = await import(${JSON.stringify(loaderUrl)}); + const remoteInfo = { entry: "http://127.0.0.1:9/remoteEntry.ssr.js", name: "vite_remote", type: "module" }; + const origin = { name: "host", options: { shared: {} }, shareScopeMap: {} }; + try { + await loader().loadEntry({ origin, remoteInfo }); + } catch (error) { + console.log(JSON.stringify({ message: error.message, phase: error.phase })); + } + `, + ); + const result = spawnSync(process.execPath, [script], { cwd: app, encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr); + const error = JSON.parse(result.stdout); + assert.equal(error.phase, "loader"); + assert.match(error.message, /Add @module-federation\/vite to the host's dependencies/); + } finally { + rmSync(app, { force: true, recursive: true }); + } +}); + +/** + * Runs `body` as an ES module in the Rsbuild SSR example host, which depends on + * @module-federation/vite and React, and returns the JSON it logs last. It runs in a child + * process so the loader's module registrations stay out of this one. + */ +async function runInHost(body) { + const app = join(root, "apps", "rsbuild-ssr-host"); + const directory = mkdtempSync(join(tmpdir(), "mf-tanstack-host-")); + try { + const script = join(directory, "server.mjs"); + writeFileSync( + script, + `const { default: loader } = await import(${JSON.stringify(loaderUrl)});\n${body}`, + ); + // Asynchronous, so this process's test server can answer the child. + const { stdout } = await promisify(execFile)(process.execPath, [script], { cwd: app }); + return JSON.parse(stdout.trim().split("\n").at(-1)); + } finally { + rmSync(directory, { force: true, recursive: true }); + } +} + +test("Vite remotes resolve node_modules React to the host's own instance", async () => { + const outcome = await runInHost(` + import { createRequire } from "node:module"; + const hostReact = { version: "host" }; + const unused = { version: "not loaded" }; + const origin = { + name: "host", + options: { shared: {} }, + shareScopeMap: { + default: { + react: { "19.3.0": { from: "host", loaded: true, lib: () => hostReact } }, + "react-dom": { "19.3.0": { from: "host", lib: () => unused } }, + }, + }, + }; + // A closed port: the load fails, after the host's modules are registered. + const remoteInfo = { entry: "http://127.0.0.1:9/remoteEntry.js", name: "vite_remote", type: "module" }; + const phase = await loader().loadEntry({ origin, remoteInfo }).catch((error) => error.phase); + const appRequire = createRequire(process.cwd() + "/package.json"); + console.log(JSON.stringify({ + phase, + react: appRequire("react") === hostReact, + reactDom: appRequire("react-dom") === unused, + })); + `); + assert.deepEqual(outcome, { phase: "fetch", react: true, reactDom: false }); +}); + +test("Vite remotes report a reachable remote without a server entry", async () => { + // A Vite remote built without a server entry: its manifest lists no ssrRemoteEntry. + const entry = serve("/no-ssr/remoteEntry.js", "export {};"); + serve( + "/no-ssr/mf-manifest.json", + JSON.stringify({ + exposes: [], + metaData: { remoteEntry: { name: "remoteEntry.js", path: "" } }, + }), + "application/json", + ); + const outcome = await runInHost(` + const origin = { name: "host", options: { shared: {} }, shareScopeMap: {} }; + const remoteInfo = { entry: ${JSON.stringify(entry)}, name: "vite_remote", type: "module" }; + const error = await loader().loadEntry({ origin, remoteInfo }).catch((error) => error); + console.log(JSON.stringify({ message: error.message, phase: error.phase })); + `); + assert.equal(outcome.phase, "loader"); + assert.match(outcome.message, /could not load the server entry of remote "vite_remote"/); +}); + +test("getRemoteStylesheets resolves an expose's CSS from its manifest", async () => { + const manifest = (publicPath) => + JSON.stringify({ + exposes: [ + { + assets: { css: { async: ["assets/lazy.css"], sync: ["assets/card.css"] } }, + name: "Card", + path: "./Card", + }, + ], + metaData: { publicPath }, + }); + + const auto = serve("/auto/mf-manifest.json", manifest("auto"), "application/json"); + assert.deepEqual(await getRemoteStylesheets(auto, "./Card"), [ + `${origin}/auto/assets/card.css`, + `${origin}/auto/assets/lazy.css`, + ]); + assert.deepEqual(await getRemoteStylesheets(auto, "Card"), [ + `${origin}/auto/assets/card.css`, + `${origin}/auto/assets/lazy.css`, + ]); + await assert.rejects(getRemoteStylesheets(auto, "./Missing"), /does not expose "\.\/Missing"/); + + const cdn = serve( + "/cdn/mf-manifest.json", + manifest("https://cdn.test/remote/"), + "application/json", + ); + assert.deepEqual(await getRemoteStylesheets(cdn, "./Card"), [ + "https://cdn.test/remote/assets/card.css", + "https://cdn.test/remote/assets/lazy.css", + ]); +}); + +test("getRemoteStylesheets reuses manifests briefly and retries failures", async () => { + const path = "/cache/mf-manifest.json"; + const url = `${origin}${path}`; + const fetches = () => requests.filter((request) => request === path).length; + + await assert.rejects(getRemoteStylesheets(url, "./Card"), /HTTP 404/); + serve(path, JSON.stringify({ exposes: [{ name: "Card", path: "./Card" }] }), "application/json"); + assert.deepEqual(await getRemoteStylesheets(url, "./Card"), []); + assert.deepEqual(await getRemoteStylesheets(url, "./Card"), []); + assert.equal(fetches(), 2, "the failure was retried, the success reused"); + + assert.deepEqual(await getRemoteStylesheets(url, "./Card", { maxAgeMs: 0 }), []); + assert.equal(fetches(), 3, "an expired manifest is fetched again"); +}); + +/** Server-renders `element` to HTML once every Suspense boundary has settled. */ +async function prerender(element) { + const errors = []; + const { prelude } = await prerenderToNodeStream( + createElement(Suspense, { fallback: "fallback" }, element), + { onError: (error) => void errors.push(error.message) }, + ); + let html = ""; + for await (const chunk of prelude) html += chunk; + return { errors, html }; +} + +test("lazyRemote renders the remote, and loads again only after retryAfterMs", async () => { + let online = false; + let loads = 0; + const Card = ({ label }) => createElement("p", null, `card ${label}`); + const RemoteCard = lazyRemote( + async () => { + loads++; + if (!online) throw new Error("remote offline"); + return { default: Card }; + }, + { retryAfterMs: 100 }, + ); + + const offline = await prerender(createElement(RemoteCard, { label: "one" })); + assert.deepEqual(offline.errors, ["remote offline"]); + assert.doesNotMatch(offline.html, /card/); + assert.equal(loads, 1); + + // The remote is back, but the failure stands until retryAfterMs passes. + online = true; + const tooSoon = await prerender(createElement(RemoteCard, { label: "two" })); + assert.deepEqual(tooSoon.errors, ["remote offline"]); + assert.equal(loads, 1); + + await new Promise((resolve) => setTimeout(resolve, 120)); + const recovered = await prerender(createElement(RemoteCard, { label: "three" })); + assert.deepEqual(recovered.errors, []); + assert.match(recovered.html, /card three/); + assert.equal(loads, 2); + + // A loaded remote is never loaded again. + online = false; + await new Promise((resolve) => setTimeout(resolve, 120)); + assert.match(renderToString(createElement(RemoteCard, { label: "four" })), /card four/); + assert.equal(loads, 2); +}); diff --git a/test/support/examples.mjs b/test/support/examples.mjs new file mode 100644 index 0000000..94057a3 --- /dev/null +++ b/test/support/examples.mjs @@ -0,0 +1,430 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { after, before, test } from "node:test"; +import { chromium } from "playwright"; +import { getRemoteStylesheets } from "../../packages/tanstack/dist/runtime.js"; + +const root = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +export const apps = { + viteHost: { name: "tanstack-start-vite-host", url: "http://127.0.0.1:3000/" }, + viteRemote: { name: "tanstack-start-vite-remote", url: "http://127.0.0.1:3001/" }, + rsbuildRemote: { name: "tanstack-start-rsbuild-remote", url: "http://127.0.0.1:3002/" }, + rsbuildHost: { name: "tanstack-start-rsbuild-host", url: "http://127.0.0.1:3003/" }, + rsbuildSsrRemote: { name: "tanstack-start-rsbuild-ssr-remote", url: "http://127.0.0.1:3004/" }, + rsbuildSsrHost: { name: "tanstack-start-rsbuild-ssr-host", url: "http://127.0.0.1:3005/" }, +}; + +const remotes = ["viteRemote", "rsbuildRemote", "rsbuildSsrRemote"]; +const hosts = ["viteHost", "rsbuildHost", "rsbuildSsrHost"]; + +const cards = { + vite: "Owned by the remote app", + rsbuild: "Federated from Rsbuild", + rsbuildSsr: "Federated SSR from Rsbuild", +}; + +// The remotes each SSR host renders on the server, across both bundlers. +const serverRendered = { + viteHost: ["viteRemote", "rsbuildSsrRemote"], + rsbuildSsrHost: ["rsbuildSsrRemote", "viteRemote"], +}; + +const remoteCards = { viteRemote: cards.vite, rsbuildSsrRemote: cards.rsbuildSsr }; + +/** + * Starts the six example apps with `script` ("start" for development servers, "preview" + * for production builds) and registers the shared federation scenarios against them. + */ +export function describeExamples(script) { + const processes = {}; + let browser; + + const start = async (key) => { + processes[key] = startApp(apps[key].name, script); + // A missing route renders TanStack's not-found page without touching any remote. + await waitForResponse(`${apps[key].url}__ready`, processes[key], ({ status }) => status < 500); + }; + + // Production server entries of the Rsbuild SSR host, read before its dev server starts. + const serverBuild = join(root, "apps", "rsbuild-ssr-host", "dist", "server", "index.cjs"); + let productionServerEntry; + + before(async () => { + if (existsSync(serverBuild)) productionServerEntry = readFileSync(serverBuild, "utf8"); + await Promise.all(remotes.map(start)); + await Promise.all(hosts.map(start)); + browser = await chromium.launch(); + }); + + after(async () => { + await browser?.close(); + await Promise.all(Object.values(processes).map(stopApp)); + }); + + test("SSR hosts render Vite and Rsbuild remote markup for concurrent first requests", async () => { + for (const [host, remoteKeys] of Object.entries(serverRendered)) { + const responses = await Promise.all( + Array.from({ length: 8 }, () => + fetch(apps[host].url, { signal: AbortSignal.timeout(60_000) }).then(readResponse), + ), + ); + for (const { body, status } of responses) { + assert.equal(status, 200, host); + for (const remote of remoteKeys) { + assert.ok(body.includes(remoteCards[remote]), `${host} initial HTML contains ${remote}`); + } + assert.equal( + body.match(/Rendered on the server/g)?.length, + remoteKeys.length, + `${host} renders every remote on the server`, + ); + } + } + }); + + test("server-rendered remotes are styled before JavaScript runs", async () => { + for (const [host, remoteKeys] of Object.entries(serverRendered)) { + const stylesheets = Object.fromEntries( + await Promise.all( + remoteKeys.map(async (remote) => [ + remote, + await getRemoteStylesheets( + new URL("mf-manifest.json", apps[remote].url).href, + "./StatusCard", + ), + ]), + ), + ); + // A Vite dev server injects CSS from JavaScript, so its manifest lists none. Builds + // must list every remote's stylesheet. + const styled = remoteKeys.filter((remote) => stylesheets[remote].length > 0); + if (script === "preview") assert.deepEqual(styled, remoteKeys, host); + + const context = await browser.newContext({ javaScriptEnabled: false }); + try { + const page = await context.newPage(); + // Without JavaScript, streamed Suspense content stays hidden, so warm the host up + // first: once its remotes are loaded, it renders them inline. + await fetch(apps[host].url, { signal: AbortSignal.timeout(60_000) }); + await page.goto(apps[host].url); + const loaded = await page.evaluate(() => [...document.styleSheets].map(({ href }) => href)); + + for (const remote of styled) { + for (const href of stylesheets[remote]) { + assert.ok(loaded.includes(href), `${host} loads ${href}`); + } + const background = await page + .locator("article", { hasText: remoteCards[remote] }) + .evaluate((card) => getComputedStyle(card).backgroundColor); + assert.notEqual(background, "rgba(0, 0, 0, 0)", `${host} styles ${remote}`); + } + } finally { + await context.close(); + } + } + }); + + if (script === "start") { + test("development servers leave production server builds alone", (t) => { + if (productionServerEntry === undefined) { + t.skip("needs a production build from before the development servers started"); + return; + } + // `rsbuild preview` serves dist/server, so a dev server must not write its bundle there. + assert.equal(readFileSync(serverBuild, "utf8"), productionServerEntry); + }); + } + + test("remote manifests reference reachable assets", async () => { + for (const key of remotes) { + const manifestUrl = new URL("mf-manifest.json", apps[key].url).href; + const manifest = await (await fetch(manifestUrl)).json(); + const assets = manifestAssets(manifest); + assert.ok(assets.length > 0, `${key} lists assets`); + for (const asset of assets) { + const url = new URL(asset, manifestUrl).href; + const { status } = await fetch(url); + assert.equal(status, 200, url); + } + } + }); + + test("Vite host hydrates the Vite and Rsbuild remotes with one React instance", async () => { + await withPage(apps.viteHost.url, async (page) => { + await page.getByText("Hydrated on the host").first().waitFor({ timeout: 20_000 }); + for (const card of [cards.vite, cards.rsbuildSsr, cards.rsbuild]) { + await assertInteractive(page, card); + } + await assertHostContext(page, [cards.vite, cards.rsbuildSsr, cards.rsbuild], "Vite host"); + }); + }); + + test("Rsbuild host loads the Vite and Rsbuild remotes with one React instance", async () => { + await withPage(apps.rsbuildHost.url, async (page) => { + await assertInteractive(page, cards.vite); + await assertInteractive(page, cards.rsbuild); + await assertHostContext(page, [cards.vite, cards.rsbuild], "Rsbuild host"); + }); + }); + + test("Rsbuild SSR host hydrates the server-rendered Rsbuild and Vite remotes", async () => { + await withPage(apps.rsbuildSsrHost.url, async (page) => { + await page.getByText("Hydrated on the host").first().waitFor({ timeout: 20_000 }); + await assertInteractive(page, cards.rsbuildSsr); + await assertInteractive(page, cards.vite); + await assertHostContext(page, [cards.rsbuildSsr, cards.vite], "Rsbuild SSR host"); + }); + }); + + test("remotes work as standalone TanStack Start apps", async () => { + for (const [key, card] of [ + ["viteRemote", cards.vite], + ["rsbuildRemote", cards.rsbuild], + ["rsbuildSsrRemote", cards.rsbuildSsr], + ]) { + await withPage(apps[key].url, async (page) => { + await assertInteractive(page, card); + await assertHostContext(page, [card], "standalone"); + }); + } + }); + + // The last two scenarios stop remotes, then start them again. + test("hosts fall back while remotes are offline and recover when they return", async () => { + await Promise.all(remotes.map((key) => stopApp(processes[key]))); + + for (const [host, expected] of [ + [ + "viteHost", + [ + "Vite remote is unavailable", + "Rsbuild SSR remote is unavailable", + "Rsbuild remote is unavailable", + ], + ], + ["rsbuildHost", ["Remote unavailable"]], + ["rsbuildSsrHost", ["Rsbuild SSR remote is unavailable", "Vite remote is unavailable"]], + ]) { + const { status } = await fetch(apps[host].url); + assert.equal(status, 200, `${host} still serves its page`); + await withPage( + apps[host].url, + async (page) => { + for (const fallback of expected) { + await page.getByText(fallback).first().waitFor({ timeout: 20_000 }); + } + }, + // React reports the failed server render as recoverable and renders the + // boundary on the client, which is the fallback under test. + { allowErrors: true }, + ); + } + + await Promise.all(remotes.map(start)); + await assertServerRendersRemotes("again"); + await withPage(apps.rsbuildHost.url, async (page) => { + await assertInteractive(page, cards.vite); + await assertInteractive(page, cards.rsbuild); + }); + await withPage(apps.viteHost.url, async (page) => { + await page.getByText("Hydrated on the host").first().waitFor({ timeout: 20_000 }); + for (const card of [cards.vite, cards.rsbuildSsr, cards.rsbuild]) { + await assertInteractive(page, card); + } + }); + await withPage(apps.rsbuildSsrHost.url, async (page) => { + await assertInteractive(page, cards.rsbuildSsr); + await assertInteractive(page, cards.vite); + }); + }); + + test("SSR hosts that start during an outage render the remotes once they return", async () => { + await Promise.all(remotes.map((key) => stopApp(processes[key]))); + const ssrHosts = Object.keys(serverRendered); + await Promise.all(ssrHosts.map((key) => stopApp(processes[key]))); + await Promise.all(ssrHosts.map(start)); + + // The first requests fail to load every remote. + for (const host of ssrHosts) { + const { body, status } = await readResponse( + await fetch(apps[host].url, { signal: AbortSignal.timeout(60_000) }), + ); + assert.equal(status, 200, `${host} serves its page while remotes are offline`); + assert.equal(body.includes("Rendered on the server"), false, host); + } + + await Promise.all(remotes.map(start)); + await assertServerRendersRemotes("after starting during an outage"); + await withPage(apps.viteHost.url, async (page) => { + await page.getByText("Hydrated on the host").first().waitFor({ timeout: 20_000 }); + for (const card of [cards.vite, cards.rsbuildSsr, cards.rsbuild]) { + await assertInteractive(page, card); + } + await assertHostContext(page, [cards.vite, cards.rsbuildSsr, cards.rsbuild], "Vite host"); + }); + await withPage(apps.rsbuildSsrHost.url, async (page) => { + await assertInteractive(page, cards.rsbuildSsr); + await assertInteractive(page, cards.vite); + await assertHostContext(page, [cards.rsbuildSsr, cards.vite], "Rsbuild SSR host"); + }); + }); + + /** Waits until every SSR host renders all of its remotes on the server. */ + async function assertServerRendersRemotes(when) { + for (const [host, remoteKeys] of Object.entries(serverRendered)) { + const { body } = await waitForResponse(apps[host].url, processes[host], ({ body }) => + remoteKeys.every((remote) => body.includes(remoteCards[remote])), + ); + assert.equal( + body.match(/Rendered on the server/g)?.length, + remoteKeys.length, + `${host} renders its remotes on the server ${when}`, + ); + } + } + + /** Opens a page, runs `check`, and fails on uncaught errors or console errors and warnings. */ + async function withPage(url, check, { allowErrors = false } = {}) { + const page = await browser.newPage(); + const errors = []; + if (!allowErrors) { + page.on("pageerror", (error) => errors.push(error.stack ?? error.message)); + page.on("console", (message) => { + if (["error", "warning"].includes(message.type())) { + errors.push(`${message.type()}: ${message.text()}`); + } + }); + } + try { + await page.goto(url); + await check(page); + assert.deepEqual(errors, [], url); + } finally { + await page.close(); + } + } +} + +/** Lists every file a manifest points to, relative to the manifest URL. */ +function manifestAssets(manifest) { + const { metaData } = manifest; + const entries = [metaData.remoteEntry, metaData.ssrRemoteEntry] + .filter(Boolean) + .map(({ path, name }) => (path ? `${path.replace(/^\/|\/$/g, "")}/${name}` : name)); + const files = [...manifest.exposes, ...manifest.shared].flatMap(({ assets }) => [ + ...assets.js.sync, + ...assets.js.async, + ...assets.css.sync, + ...assets.css.async, + ]); + return [...new Set([...entries, ...files])]; +} + +/** Proves each remote card reads the context its host provides through a shared module. */ +async function assertHostContext(page, cardTexts, hostName) { + for (const cardText of cardTexts) { + const card = page.locator("article", { hasText: cardText }); + await card.getByText(`Host: ${hostName}`).waitFor({ timeout: 5_000 }); + } +} + +/** Clicks a remote card's counter and proves the remote's state updates on the host. */ +async function assertInteractive(page, cardText) { + const button = page + .locator("article", { hasText: cardText }) + .getByRole("button", { name: /Remote count/ }); + await button.waitFor({ timeout: 20_000 }); + const count = async () => Number((await button.textContent()).match(/\d+/)?.[0]); + + // A server-rendered button ignores clicks until React hydrates it. + const deadline = Date.now() + 20_000; + while ((await count()) === 0) { + assert.ok(Date.now() < deadline, `${cardText} never became interactive`); + await button.click(); + await page.waitForTimeout(100); + } + + const before = await count(); + await button.click(); + await page.waitForFunction( + ([text, expected]) => + [...document.querySelectorAll("article")] + .find((article) => article.textContent.includes(text)) + ?.querySelector("button") + ?.textContent.includes(String(expected)), + [cardText, before + 1], + { timeout: 5_000 }, + ); +} + +function startApp(name, script) { + const child = spawn("pnpm", ["--filter", name, script], { + cwd: root, + detached: true, + env: { ...process.env, NODE_ENV: script === "start" ? "development" : "production" }, + stdio: ["ignore", "pipe", "pipe"], + }); + child.output = ""; + + for (const stream of [child.stdout, child.stderr]) { + stream.on("data", (chunk) => { + child.output = `${child.output}${chunk}`.slice(-12_000); + }); + } + + return child; +} + +async function readResponse(response) { + return { body: await response.text(), status: response.status }; +} + +async function waitForResponse(url, child, predicate = () => true) { + const deadline = Date.now() + 60_000; + let lastError; + let lastResponse; + + while (Date.now() < deadline) { + if (child.exitCode !== null || child.signalCode !== null) { + assert.fail(`server stopped while waiting for ${url}\n${child.output}`); + } + + try { + lastResponse = await readResponse(await fetch(url, { signal: AbortSignal.timeout(5_000) })); + if (predicate(lastResponse)) return lastResponse; + } catch (error) { + lastError = error; + } + + await new Promise((resolve) => setTimeout(resolve, 250)); + } + + assert.fail( + [ + `timed out waiting for ${url}`, + lastResponse ? `last status: ${lastResponse.status}` : undefined, + lastError ? `last error: ${lastError.message}` : undefined, + child.output, + ] + .filter(Boolean) + .join("\n"), + ); +} + +async function stopApp(child) { + if (!child || child.exitCode !== null || child.signalCode !== null) return; + process.kill(-child.pid, "SIGTERM"); + + await Promise.race([ + new Promise((resolve) => child.once("exit", resolve)), + new Promise((resolve) => setTimeout(resolve, 3_000)), + ]); + + if (child.exitCode === null && child.signalCode === null) { + process.kill(-child.pid, "SIGKILL"); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3c0eb91 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "files": [], + "references": [ + { + "path": "./packages/tanstack/tsconfig.json" + }, + { + "path": "./apps/vite-host/tsconfig.json" + }, + { + "path": "./apps/vite-remote/tsconfig.json" + }, + { + "path": "./apps/rsbuild-host/tsconfig.json" + }, + { + "path": "./apps/rsbuild-remote/tsconfig.json" + }, + { + "path": "./apps/rsbuild-ssr-host/tsconfig.json" + }, + { + "path": "./apps/rsbuild-ssr-remote/tsconfig.json" + } + ] +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..f26e535 --- /dev/null +++ b/turbo.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://turborepo.com/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": [".output/**", "dist/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "start": { + "cache": false, + "persistent": true + }, + "typecheck": { + "dependsOn": ["^build", "^typecheck"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": [] + }, + "preview": { + "cache": false, + "persistent": true + } + } +}