From 2f8ddb84585d0f869dabd8e6bee5f7703228c4fe Mon Sep 17 00:00:00 2001 From: Kevin Beier Date: Tue, 11 Aug 2026 19:56:37 +0200 Subject: [PATCH 1/4] feat(examples): expose Bridge export-app from Nuxt remote Add a minimal createBridgeComponent export on the remote and a host catch-all under /bridge so the same app-level remote can be loaded alongside existing component federation. Co-authored-by: Cursor --- apps/host/README.md | 4 ++ apps/host/app/app.vue | 60 ++++++++----------- apps/host/app/components/BridgeRemotePage.vue | 44 ++++++++++++++ apps/host/app/pages/index.vue | 40 +++++++++++++ apps/host/app/types/remote.d.ts | 8 +++ apps/host/nuxt.config.ts | 12 ++++ apps/host/package.json | 1 + apps/remote/README.md | 11 +++- apps/remote/app/bridge/App.vue | 27 +++++++++ apps/remote/app/bridge/pages/Detail.vue | 5 ++ apps/remote/app/bridge/pages/Home.vue | 5 ++ apps/remote/app/bridge/router.ts | 17 ++++++ apps/remote/app/export-app.ts | 14 +++++ apps/remote/nuxt.config.ts | 4 ++ apps/remote/package.json | 1 + packages/nuxt/README.md | 29 +++++++++ pnpm-lock.yaml | 52 ++++++++++++++++ 17 files changed, 299 insertions(+), 35 deletions(-) create mode 100644 apps/host/app/components/BridgeRemotePage.vue create mode 100644 apps/host/app/pages/index.vue create mode 100644 apps/remote/app/bridge/App.vue create mode 100644 apps/remote/app/bridge/pages/Detail.vue create mode 100644 apps/remote/app/bridge/pages/Home.vue create mode 100644 apps/remote/app/bridge/router.ts create mode 100644 apps/remote/app/export-app.ts diff --git a/apps/host/README.md b/apps/host/README.md index ea3f690..90f2e67 100644 --- a/apps/host/README.md +++ b/apps/host/README.md @@ -47,6 +47,10 @@ The page consumes it like any other Nuxt component: ``` +### Bridge application remote + +`/bridge/*` mounts `remote/export-app` via `@module-federation/bridge-vue3` (`createRemoteAppComponent`) as a client-only island. Component federation on `/` is unchanged. The host registers a Vue Router catch-all (`/bridge/:pathMatch(.*)*`) so basename auto-detect works with current `bridge-vue3` releases. + ## SSR behavior Nuxt 4.5 runs development with Vite 8 and Rolldown. Remote components render on the server during `pnpm dev`, then hydrate and remain interactive in the browser. diff --git a/apps/host/app/app.vue b/apps/host/app/app.vue index b457ed5..c55bf94 100644 --- a/apps/host/app/app.vue +++ b/apps/host/app/app.vue @@ -1,29 +1,15 @@ - - diff --git a/apps/host/app/components/BridgeRemotePage.vue b/apps/host/app/components/BridgeRemotePage.vue new file mode 100644 index 0000000..8187d92 --- /dev/null +++ b/apps/host/app/components/BridgeRemotePage.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/apps/host/app/pages/index.vue b/apps/host/app/pages/index.vue new file mode 100644 index 0000000..5c6a2b3 --- /dev/null +++ b/apps/host/app/pages/index.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/apps/host/app/types/remote.d.ts b/apps/host/app/types/remote.d.ts index 2f5c114..45b75a0 100644 --- a/apps/host/app/types/remote.d.ts +++ b/apps/host/app/types/remote.d.ts @@ -9,3 +9,11 @@ declare module "remote/Counter" { const component: Component; export default component; } + +declare module "remote/export-app" { + const createProvider: () => { + render: (info: Record) => void | Promise; + destroy: (info: { dom: HTMLElement }) => void; + }; + export default createProvider; +} diff --git a/apps/host/nuxt.config.ts b/apps/host/nuxt.config.ts index 7870de6..f392f80 100644 --- a/apps/host/nuxt.config.ts +++ b/apps/host/nuxt.config.ts @@ -9,6 +9,18 @@ export default defineNuxtConfig({ buildCache: false, }, + hooks: { + // Vue Router catch-all name so bridge-vue3 basename auto-detect works + // without waiting for explicit-basename releases. + "pages:extend"(pages) { + pages.push({ + name: "bridge-remote", + path: "/bridge/:pathMatch(.*)*", + file: "~/components/BridgeRemotePage.vue", + }); + }, + }, + moduleFederation: { remoteComponents: { remote: ["Widget", "Counter"], diff --git a/apps/host/package.json b/apps/host/package.json index 48c2a32..34c858f 100644 --- a/apps/host/package.json +++ b/apps/host/package.json @@ -10,6 +10,7 @@ "typecheck": "nuxt typecheck" }, "dependencies": { + "@module-federation/bridge-vue3": "^2.8.2", "@module-federation/nuxt": "workspace:*", "@pinia/nuxt": "0.11.3", "nuxt": "4.5.1", diff --git a/apps/remote/README.md b/apps/remote/README.md index 0e461b0..86c1d3e 100644 --- a/apps/remote/README.md +++ b/apps/remote/README.md @@ -6,6 +6,7 @@ This Nuxt application provides Vue components to the host example and runs stand - Host consumer: `http://localhost:4173` - Configuration: [`nuxt.config.ts`](nuxt.config.ts) - Exposed components: [`app/components/exposed`](app/components/exposed) +- Bridge app export: [`app/export-app.ts`](app/export-app.ts) → `./export-app` ## Run @@ -33,6 +34,14 @@ The port is fixed because the host configuration points to `4174`. To add another auto-registered component, create `app/components/exposed/Example.vue`. After restarting the applications, a single-remote host can render it as ``. +### Bridge application export + +In addition to components, this remote exposes an application-level Bridge module: + +- `./export-app` — `createBridgeComponent` wrapping a small vue-router app under `app/bridge/` + +Hosts load it with `@module-federation/bridge-vue3` `createRemoteAppComponent` (see host `/bridge/*`). The same `./export-app` can be consumed from React/Next via `@module-federation/bridge-react`. + ## Federation assets Development and production serve the federation contract from the public root: @@ -52,4 +61,4 @@ pnpm build pnpm preview ``` -Verify all three federation URLs above return successfully, then open the host at `http://localhost:4173`. Its initial HTML should already include both remote components, and their counters should become interactive after hydration. +Verify all three federation URLs above return successfully, then open the host at `http://localhost:4173`. Its initial HTML should already include both remote components, and their counters should become interactive after hydration. Open `/bridge` to exercise the Bridge remote (client island). diff --git a/apps/remote/app/bridge/App.vue b/apps/remote/app/bridge/App.vue new file mode 100644 index 0000000..adae53b --- /dev/null +++ b/apps/remote/app/bridge/App.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/remote/app/bridge/pages/Detail.vue b/apps/remote/app/bridge/pages/Detail.vue new file mode 100644 index 0000000..60e1ed2 --- /dev/null +++ b/apps/remote/app/bridge/pages/Detail.vue @@ -0,0 +1,5 @@ + diff --git a/apps/remote/app/bridge/pages/Home.vue b/apps/remote/app/bridge/pages/Home.vue new file mode 100644 index 0000000..fd441f2 --- /dev/null +++ b/apps/remote/app/bridge/pages/Home.vue @@ -0,0 +1,5 @@ + diff --git a/apps/remote/app/bridge/router.ts b/apps/remote/app/bridge/router.ts new file mode 100644 index 0000000..dc8b7bd --- /dev/null +++ b/apps/remote/app/bridge/router.ts @@ -0,0 +1,17 @@ +import { createRouter, createWebHistory } from "vue-router"; +import Home from "./pages/Home.vue"; +import Detail from "./pages/Detail.vue"; + +/** + * Standalone vue-router for the Bridge export. + * Mounted under the host basename (e.g. /bridge) by createBridgeComponent. + */ +export function createBridgeRouter() { + return createRouter({ + history: createWebHistory(), + routes: [ + { path: "/", name: "bridge-home", component: Home }, + { path: "/detail", name: "bridge-detail", component: Detail }, + ], + }); +} diff --git a/apps/remote/app/export-app.ts b/apps/remote/app/export-app.ts new file mode 100644 index 0000000..6852898 --- /dev/null +++ b/apps/remote/app/export-app.ts @@ -0,0 +1,14 @@ +import { createBridgeComponent } from "@module-federation/bridge-vue3"; +import App from "./bridge/App.vue"; +import { createBridgeRouter } from "./bridge/router"; + +/** + * Application-level Bridge export for hosts (Nuxt, Vue, React, …). + * Component federation (./Widget, ./Counter) is unchanged. + */ +export default createBridgeComponent({ + rootComponent: App, + appOptions: () => ({ + router: createBridgeRouter(), + }), +}); diff --git a/apps/remote/nuxt.config.ts b/apps/remote/nuxt.config.ts index d648457..7833d4a 100644 --- a/apps/remote/nuxt.config.ts +++ b/apps/remote/nuxt.config.ts @@ -15,6 +15,10 @@ export default defineNuxtConfig({ filename: "remoteEntry.js", remotes: {}, manifest: true, + exposes: { + // App-level Bridge export (in addition to auto component exposes) + "./export-app": "./app/export-app.ts", + }, }, }, vite: { diff --git a/apps/remote/package.json b/apps/remote/package.json index 6a9e857..2843c06 100644 --- a/apps/remote/package.json +++ b/apps/remote/package.json @@ -10,6 +10,7 @@ "typecheck": "nuxt typecheck" }, "dependencies": { + "@module-federation/bridge-vue3": "^2.8.2", "@module-federation/nuxt": "workspace:*", "nuxt": "4.5.1", "ufo": "1.6.4", diff --git a/packages/nuxt/README.md b/packages/nuxt/README.md index a6f69c3..bb37093 100644 --- a/packages/nuxt/README.md +++ b/packages/nuxt/README.md @@ -110,6 +110,35 @@ With multiple configured remotes, the remote name is included to prevent collisi Only expose names beginning with a letter and containing letters, numbers, underscores, or hyphens are registered as Nuxt components. Other MF exposes remain available through normal runtime imports. +### Bridge application export (optional) + +To expose a full routing app (not only components), add a Bridge entry and list it under `config.exposes`: + +```ts +// app/export-app.ts +import { createBridgeComponent } from "@module-federation/bridge-vue3"; +import App from "./bridge/App.vue"; +import { createBridgeRouter } from "./bridge/router"; + +export default createBridgeComponent({ + rootComponent: App, + appOptions: () => ({ router: createBridgeRouter() }), +}); +``` + +```ts +moduleFederation: { + config: { + name: "catalog", + exposes: { + "./export-app": "./app/export-app.ts", + }, + }, +} +``` + +Hosts load it with `createRemoteAppComponent` from `@module-federation/bridge-vue3` (or `@module-federation/bridge-react` for React/Next). Prefer an explicit `basename` and a host catch-all route such as `/catalog/:pathMatch(.*)*`. See the example apps under `apps/host` and `apps/remote`. + ## Server rendering `ssr` defaults to `true`. When Nuxt SSR is enabled, the module creates client and server federation builds: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4549862..630632f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: apps/host: dependencies: + '@module-federation/bridge-vue3': + specifier: ^2.8.2 + version: 2.8.2(vue-router@5.2.0(@vue/compiler-sfc@3.5.40)(esbuild@0.28.1)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(rolldown@1.2.0)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3)) '@module-federation/nuxt': specifier: workspace:* version: link:../../packages/nuxt @@ -65,6 +68,9 @@ importers: apps/remote: dependencies: + '@module-federation/bridge-vue3': + specifier: ^2.8.2 + version: 2.8.2(vue-router@5.2.0(@vue/compiler-sfc@3.5.40)(esbuild@0.28.1)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(rolldown@1.2.0)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3)) '@module-federation/nuxt': specifier: workspace:* version: link:../../packages/nuxt @@ -658,6 +664,15 @@ packages: engines: {node: '>=18'} hasBin: true + '@module-federation/bridge-shared@2.8.2': + resolution: {integrity: sha512-pgK9PxLfK8voZztF3ONDjmUgNdOsDL1JmXJaw9d5I2LcbQQuGWFnk23DE66BFqeBI+x6wls+5Cmm/CiW1I/zhQ==} + + '@module-federation/bridge-vue3@2.8.2': + resolution: {integrity: sha512-kpddUr/dydVkDxAjO/Q1pwAO71hvnXUrBCexJWLKf4SZaYUae9uGShqEqHBhQLjOzJlByGREd0Lvmqfwca9WuA==} + peerDependencies: + vue: '=3' + vue-router: '=4' + '@module-federation/dts-plugin@2.8.0': resolution: {integrity: sha512-defjq4jOWMEfeejezPWLP5sc8kw0O6FqTT7/E5rbZPEVyjB1A0U3ynhW6GDE5/6hk9/TzdbWS+fBNi4MqUOY6Q==} peerDependencies: @@ -673,6 +688,9 @@ packages: '@module-federation/error-codes@2.8.1': resolution: {integrity: sha512-0mQ+bWt1LRCZyURx3g2b8G+aAlvk8iXIgrp3Jit/75blrlVda/eVqnHz1L+YOxwkP3xSrdbUb4423AoWti31ZQ==} + '@module-federation/error-codes@2.8.2': + resolution: {integrity: sha512-8inlDv48QOjA//CLQ3epjoHEiMQGsz1Pmtu2N+s7gQVggn6AYHpjnMe8AsyGxtpaPg3wbX0HmBZtRFggpXUB9A==} + '@module-federation/managers@2.8.0': resolution: {integrity: sha512-SnVBCwmi962WGg6hLFElxZUCnrRJdR6glE2ZKPBY/iK07AHUN2ZxuaBCBsVzyws+xLZGHZxBHmVstijTh8dSUA==} @@ -682,18 +700,27 @@ packages: '@module-federation/runtime-core@2.8.1': resolution: {integrity: sha512-Dif+3u7fvq6qBATFIv5qB7ay6Rgo2HNzhaNOt1yRfpVXjiJqQ3UPnHZ+TLP9znkXiZvg6Bg5W9EV2ZX4nH2S0w==} + '@module-federation/runtime-core@2.8.2': + resolution: {integrity: sha512-PEkkK9MUp+nUCeQMS4ox3QGZfwwxgfjGA7P4umEnr5c3y8DLNDR+26tyHf/Gkjen2VsjlcyL+mEAQo1Zi4IE3g==} + '@module-federation/runtime@2.8.0': resolution: {integrity: sha512-cGtUBQ1/TVy7KrXy6xPgy3FEmOGyIYkBA2T4iGH3ZH5PNPPTmqN9jF2AfneTSOj0RtBr7Pxq3CUt81E/UCvK1A==} '@module-federation/runtime@2.8.1': resolution: {integrity: sha512-+xpq/r6Om4plbGJisZp6/rl7usEUlQszz34E+JUKgl9uW3QIRzVgxwOAzGiF7U+dqOKxMqmiq+nTJwK2wAwteA==} + '@module-federation/runtime@2.8.2': + resolution: {integrity: sha512-SUoP+PD5EjSPSi6FxEPGIZoRkFifxdeYcVQbJE9mO0VEjF51gAk3/TgX8k0vzUryOBPmXekLr9SfQXU6DqUtvA==} + '@module-federation/sdk@2.8.0': resolution: {integrity: sha512-yBP+9+0Z8nlvKEXAZS3AsQVy7bFbZf8eMivGk4q4ZdwG3TsLMlsPjb1dQb2i7gcAG6ux9y2LWLkj/0LVk74cnQ==} '@module-federation/sdk@2.8.1': resolution: {integrity: sha512-3EVljiNilY2pFIG2RO4KNCC6gIPnYc9J+p5U6Nn8D5X3PtJeEcPyBvKGttxZnSLlXCi+YXQfgexBOfnkvEuzuQ==} + '@module-federation/sdk@2.8.2': + resolution: {integrity: sha512-OPS/lbQjraLXoWniQpCwQ/vqgURHTrhsackSNcOPmcJHM3LyR+DabxUc0pl8jAqExsW2l+uepQq7+/Gkei871w==} + '@module-federation/third-party-dts-extractor@2.8.0': resolution: {integrity: sha512-nAMlr74OKIylkfRwlunOhytQbmsgb3gCqdXWnPQhG+ZtqWXGELLfMT4a1Q1ht3cS+sRpWj2SZRqK2M7GadI6tA==} @@ -5288,6 +5315,16 @@ snapshots: - encoding - supports-color + '@module-federation/bridge-shared@2.8.2': {} + + '@module-federation/bridge-vue3@2.8.2(vue-router@5.2.0(@vue/compiler-sfc@3.5.40)(esbuild@0.28.1)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(rolldown@1.2.0)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3))': + dependencies: + '@module-federation/bridge-shared': 2.8.2 + '@module-federation/runtime': 2.8.2 + '@module-federation/sdk': 2.8.2 + vue: 3.5.40(typescript@6.0.3) + vue-router: 5.2.0(@vue/compiler-sfc@3.5.40)(esbuild@0.28.1)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(rolldown@1.2.0)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)) + '@module-federation/dts-plugin@2.8.0(typescript@7.0.2)(vue-tsc@3.3.8(typescript@7.0.2))': dependencies: '@module-federation/error-codes': 2.8.0 @@ -5309,6 +5346,8 @@ snapshots: '@module-federation/error-codes@2.8.1': {} + '@module-federation/error-codes@2.8.2': {} + '@module-federation/managers@2.8.0': dependencies: '@module-federation/sdk': 2.8.0 @@ -5323,6 +5362,11 @@ snapshots: '@module-federation/error-codes': 2.8.1 '@module-federation/sdk': 2.8.1 + '@module-federation/runtime-core@2.8.2': + dependencies: + '@module-federation/error-codes': 2.8.2 + '@module-federation/sdk': 2.8.2 + '@module-federation/runtime@2.8.0': dependencies: '@module-federation/error-codes': 2.8.0 @@ -5335,10 +5379,18 @@ snapshots: '@module-federation/runtime-core': 2.8.1 '@module-federation/sdk': 2.8.1 + '@module-federation/runtime@2.8.2': + dependencies: + '@module-federation/error-codes': 2.8.2 + '@module-federation/runtime-core': 2.8.2 + '@module-federation/sdk': 2.8.2 + '@module-federation/sdk@2.8.0': {} '@module-federation/sdk@2.8.1': {} + '@module-federation/sdk@2.8.2': {} + '@module-federation/third-party-dts-extractor@2.8.0': {} '@module-federation/vite@1.20.0(typescript@7.0.2)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(yaml@2.9.0))(vue-tsc@3.3.8(typescript@7.0.2))': From 3003ee07a1104d1ad9ab2b70a433968208b2af43 Mon Sep 17 00:00:00 2001 From: Kevin Beier Date: Tue, 11 Aug 2026 20:07:22 +0200 Subject: [PATCH 2/4] fix(examples): use slashed Bridge expose name Avoid host manifest discovery registering the Bridge factory as RemoteExportApp by exposing ./bridge/export-app instead of ./export-app. Co-authored-by: Cursor --- apps/host/README.md | 2 +- apps/host/app/components/BridgeRemotePage.vue | 4 ++-- apps/host/app/types/remote.d.ts | 2 +- apps/remote/README.md | 6 +++--- apps/remote/nuxt.config.ts | 5 +++-- packages/nuxt/README.md | 4 ++-- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/host/README.md b/apps/host/README.md index 90f2e67..e8b46bb 100644 --- a/apps/host/README.md +++ b/apps/host/README.md @@ -49,7 +49,7 @@ The page consumes it like any other Nuxt component: ### Bridge application remote -`/bridge/*` mounts `remote/export-app` via `@module-federation/bridge-vue3` (`createRemoteAppComponent`) as a client-only island. Component federation on `/` is unchanged. The host registers a Vue Router catch-all (`/bridge/:pathMatch(.*)*`) so basename auto-detect works with current `bridge-vue3` releases. +`/bridge/*` mounts `remote/bridge/export-app` via `@module-federation/bridge-vue3` (`createRemoteAppComponent`) as a client-only island. Component federation on `/` is unchanged. The host registers a Vue Router catch-all (`/bridge/:pathMatch(.*)*`) so basename auto-detect works with current `bridge-vue3` releases. ## SSR behavior diff --git a/apps/host/app/components/BridgeRemotePage.vue b/apps/host/app/components/BridgeRemotePage.vue index 8187d92..27d0c04 100644 --- a/apps/host/app/components/BridgeRemotePage.vue +++ b/apps/host/app/components/BridgeRemotePage.vue @@ -1,6 +1,6 @@