@fulgurjs/federation 6.12.5 → 6.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@
7
7
  | Entry | Purpose | Export surface |
8
8
  |---|---|---|
9
9
  | `@fulgurjs/federation` (package root) | Vite config | the `federation(options)` plugin + the `FederationOptions` type (full field set in the [configuration reference](configuration.md)) |
10
- | `@fulgurjs/federation/vue` | **The single entry for Vue apps** | full runtime + `provideAppContext`/`getAppContext`/`requireAppContext`/`clearAppContext` + `definePages`/`validatePages` + `remoteComponent` + `bindStoreToApp` + `createHostPages` + `defineApplication` + `defineBridgeApp` + `createVueBridgeApp` + `createVueBridgeNavigation` + `connectVueBridgeRouter` + `remoteSchema` |
10
+ | `@fulgurjs/federation/vue` | **The single entry for Vue apps** | full runtime + `provideAppContext`/`getAppContext`/`requireAppContext`/`clearAppContext` + `definePages`/`validatePages` + `remoteComponent` + `bindStoreToApp` + `createHostPages` + `defineApplication` + `defineBridgeApp` + `createVueBridgeApp` + `createVueBridgeNavigation` + `connectVueBridgeRouter`/`installVueBridgeRouter` + `createBridgeSession`/`hasBridgeMount` + `remoteSchema` |
11
11
  | `@fulgurjs/federation/react` | **The single entry for React apps** | the same full runtime + context + pages (`createReactHostPages`) + `remoteComponent`/`useLoadRemote`/`RemoteErrorBoundary` + `defineApplication` + `defineBridgeApp` + `createReactBridgeApp` + `createReactBridgeNavigation` + `createReactBridgeRouter` + `remoteSchema` |
12
12
  | `@fulgurjs/federation/runtime` | **Framework-agnostic** | full runtime (loadRemote/loadShare/initSharing/registerRemotes/registerShare/registerRemote/registerPlugins/preloadRemote/getContainer/getLoadedShare/pinLoadedShare/parseSpec/getRuntime/shareScopeMap/unwrapDefault/version/clearSessionState) + context + pages (`definePages`/`validatePages`) + `remoteSchema`; zero Vue/React/router dependency |
13
13
 
@@ -29,7 +29,7 @@ import { loadRemote } from '@fulgurjs/federation/runtime'
29
29
 
30
30
  | Function | Signature | Purpose / boundary |
31
31
  |---|---|---|
32
- | `loadRemote` | `<S extends string, T = FgRemoteModule<S>>(spec: S \| dynamic string, opts?) => Promise<T>` | Loads a remote module; typing comes from the [remote type registry](#entry-checks-for-the-string-apis-type-registry): synced literals get real module types, typos error, dynamic variables yield `unknown` (explicit `T` overrides). `spec = 'remoteName/./ExposeKey'` (`./` optional). When the remote has a `setup` configured, this is the **unified trigger** of the initialization lifecycle (after container init and before returning the module, setup/onSession run); `loadRemote('remote')` only fetches the container without running initialization. See opts below |
32
+ | `loadRemote` | `loadRemote(spec, opts?) => Promise<registry-inferred module>` | Loads a remote module; typing comes from the [remote type registry](#entry-checks-for-the-string-apis-type-registry): synced literals get real module types, typos error, dynamic variables yield `unknown` (explicit `T` overrides). `spec = 'remoteName/./ExposeKey'` (`./` optional). When the remote has a `setup` configured, this is the **unified trigger** of the initialization lifecycle (after container init and before returning the module, setup/onSession run); `loadRemote('remote')` only fetches the container without running initialization. See opts below |
33
33
  | `loadShare` | `(name: string, opts?) => Promise<namespace>` | Shared module negotiation (highest version wins / already-loaded first / singleton convergence). opts: `{ requiredVersion?, singleton?, strictVersion?, shareKey?, shareScope?, fallback? }` |
34
34
  | `preloadRemote` | `(spec: string, opts?: { mode?: 'preload' \| 'prefetch' }) => Promise<void>` | `remote/Expose` preloads just that expose's chunk + CSS; passing only the remote name preloads all its exposes. `preload` waits for CSS load/error; `prefetch` is low-priority and returns immediately. **Only prefetches resources, never executes modules**, and does not trigger setup/onSession; failure does not block the app (`MFU-007`) |
35
35
  | `getContainer` | `(name: string) => Promise<container>` | Fetches the remote container (triggers load + init), container protocol `{ name, init, get }`; **does not run setup/onSession**. Calling `container.get()` directly likewise guarantees no initialization — any load that needs the lifecycle goes through `loadRemote` |
@@ -75,7 +75,7 @@ Configuration: `runtimePlugins: ['./src/fulgurjsPlugin.ts']` (relative paths res
75
75
 
76
76
  > Hook error contract: `beforeLoadRemote` / `afterLoadRemote` are **observational hooks** — their own throws only warn and never rewrite the load result; `resolveShare` is a **decision hook** — an explicit throw propagates to the caller (it never silently falls back to another shared copy).
77
77
  >
78
- > **resolveShare and the consumption path (since 5.7.1)**: HTML entries configured with `runtimePlugins` finish shared arbitration and loading before executing the app; remote containers likewise finish async arbitration before executing an expose. Synchronous facades reuse the decision and instance of the same consumption conditions; an async hook may choose a lower version or an entry outside the original table and is not overwritten by the local copy. The dynamic import boundary between app and provider is preserved; the consumer facade on Vite 8 still has no TLA.
78
+ > **resolveShare and the consumption path**: HTML entries configured with `runtimePlugins` finish shared arbitration and loading before executing the app; remote containers likewise finish async arbitration before executing an expose. Synchronous facades reuse the decision and instance of the same consumption conditions; an async hook may choose a lower version or an entry outside the original table and is not overwritten by the local copy. The dynamic import boundary between app and provider is preserved; the consumer facade on Vite 8 still has no TLA.
79
79
  >
80
80
  > **Entry boundary**: library/custom entries without an HTML entry, or calling `registerPlugins` to change policy after the app runs, require first `await loadShare(name, opts)` and then dynamically importing the new consumer; already-evaluated static bindings cannot be rewritten retroactively. An unprepared synchronous consumer hitting an async hook gets `MFU-004` (`details.syncUnsupported: true`) and its late rejection is taken over. `strictVersion` conflicts give MFU-003; locally adopted instances are registered under their real version, never borrowing another version's slot to bypass checks. To use React 18/19 together, give the whole React group, renderer, and their consumers an independent `shareScope` (see [shared dependencies](../guide/sharing.md#sharescope-group-isolation-react-1819-on-the-same-page)). Runnable example: [React version isolation and recovery](https://github.com/chenmingye/fulgurjs-federation/blob/master/examples/demos/react-versions/README.md).
81
81
 
@@ -108,7 +108,7 @@ export default {
108
108
 
109
109
  ## AppContext — passing values and method references across apps
110
110
 
111
- The first-class channel for the host to pass values to child apps and for child apps to register methods back to the host (with types and an error contract) — no more ad-hoc bare `window.*` hooks.
111
+ A page-level channel for values and callbacks. Normal bridges provide snapshots through factory `getContext`; Vue sessions should use the `session` identity getter. Do not additionally provide/renew/clear that same automatic session manually. The following is a low-level, manually managed context example for integrations without the bridge factory or with explicit ownership. Variables come from real business code; only provide fields that are actually consumed.
112
112
 
113
113
  ```ts
114
114
  // ── Host bridge (host/src/fulgurjs/host/bridge.ts): login state sync (callable repeatedly, idempotent merge) ──
@@ -116,9 +116,9 @@ import { provideAppContext, clearAppContext } from '@fulgurjs/federation/vue'
116
116
 
117
117
  provideAppContext({
118
118
  user, // the host's logged-in user in its original shape (snapshot at provide time)
119
- getToken, // fetch the latest token (pull-based, never expires)
119
+ getToken, // read the latest token value; authentication owns validity
120
120
  store: piniaInstance, // the host pinia: child apps call useUserStore(ctx.store) to share reactive state
121
- hostApp: app, // the host Vue App instance: registration target for global components/directives
121
+ hostApp: app, // a host Vue App reference, not the consuming child app or its registry
122
122
  locale, // UI configuration (e.g. an EP locale)
123
123
  sessionKey: 's-101-1730…', // non-sensitive login generation ID: a new value on every successful login/re-login;
124
124
  // kept when a token refreshes but the session does not. The remote's onSession dedupes on it;
@@ -151,11 +151,11 @@ getAppContext().events!.bpm = { formEvent, formSubmitEvent }
151
151
  | Field | Type | Semantics | Writer |
152
152
  |---|---|---|---|
153
153
  | `user` | `Record<string, any>` | The host's logged-in user in its original shape (snapshot when provided) | Host bridge (read-only convention; re-provide to overwrite on login state change) |
154
- | `getToken` | `() => string \| undefined` | **Fetch the latest token** (pull-based, never expires; the context provides no one-shot token snapshot field) | Host bridge (read-only convention) |
154
+ | `getToken` | `() => string \| undefined` | **Fetch the latest token** (read the latest value; authentication owns expiry; the context provides no one-shot token snapshot field) | Host bridge (read-only convention) |
155
155
  | `store` | `unknown` (at runtime the host pinia) | Child apps mount/read the host's shared reactive state | Host bridge (read-only convention) |
156
- | `hostApp` | Vue App instance (same-realm direct reference) | Registration target for global components/directives | Host bridge (read-only convention) |
156
+ | `hostApp` | Vue App instance (same-realm direct reference) | Host app reference; consumer globals must target the actual consumer app | Host bridge (read-only convention) |
157
157
  | `locale` | `unknown` | UI configuration | Host bridge (read-only convention) |
158
- | `sessionKey` | `string` | Non-sensitive login generation ID: a new value on every successful login/re-login; kept when a token refreshes but the session does not. The remote's onSession dedupes on it (one run per generation; a new generation reruns automatically); after logout `clearAppContext` forces a rerun. Generated by the host login flow; **never a real token**, and not an authorization credential | Host bridge (after login) |
158
+ | `sessionKey` | `string` | Non-sensitive login generation ID: a new value on every successful login/re-login; kept when a token refreshes but the session does not. The remote's onSession dedupes on it (one run per generation; a new generation reruns automatically); after logout `clearAppContext` forces a rerun. Generated by the plugin in automatic Vue mode; owned by host authentication state in explicit mode; **never a real token**, and not an authorization credential | Host bridge (after login) |
159
159
  | `events` | `Record<string, any>` | Event/method pool: `events.main.*` provided by the host; other prefixes registered back by child apps | Host bridge builds the pool; child apps attach |
160
160
  | (extension slot) | `[key: string]: unknown` | Project-specific keys provided as needed (templates pass none by default) | Host bridge; remotes only add keys, never change host keys |
161
161
 
@@ -163,7 +163,7 @@ getAppContext().events!.bpm = { formEvent, formSubmitEvent }
163
163
 
164
164
  - `provide` = top-level merge (later writes win, idempotent, callable repeatedly); the convention is "the host writes the standard fields first, remotes only add keys and never change host keys"; nested objects (like `events`) are **reference-shared** — child app property attaches are visible immediately (same-realm direct reference);
165
165
  - Timing contract: **bridge (provide) → remote setup/onSession (require) → page module returned** — violating it fails explicitly at the initialization point (CC-001 / MFU-013), never silently;
166
- - Data semantics = **transport-layer snapshot + function references, not reactive**. "Live" data travels through two official channels: ① `getToken()` / `events.main.*` function references execute the host's latest closure on every call; ② `context.store` hands the host pinia to the child app (a shared reactive instance). **Same-page account switch (logout → B logs in → reopen the remote page) needs no page refresh**: the host re-provides the latest context + `sessionKey`, and the remote's `onSession` detects the new generation and reruns automatically. The context body is deliberately not Vue-reactive (the runtime is framework-agnostic + a gzip size red line + the double-copy trap of cross-bundle proxies); no real need for "notify on mid-flight change" exists today, so no empty API is reserved;
166
+ - Data semantics = **snapshots plus function references, not reactive context updates**. Stable getters read current business values; an explicitly passed Vue state instance can be observed by the same runtime, while React/cross-framework consumers need an appropriate subscription adapter. Use agreed project events for active notifications. Session changes rerun onSession under the current generation; normal bridge factories own context timing, not duplicate manual provides.
167
167
  - Storage note: the context's storage body is the global mirror object `window.__FULGURJS_APP_CONFIG__` (a page-level singleton, one copy shared across bundle copies; debug panels can read it directly).
168
168
 
169
169
  ### Two channels for method references
@@ -173,7 +173,7 @@ getAppContext().events!.bpm = { formEvent, formSubmitEvent }
173
173
  | **Function references carried on the context** | Synchronous direct call (the bridge runs before any page loads) | High-frequency hot paths (`getToken`/dictionaries/file URLs), child app back-registrations |
174
174
  | **exposes method modules** | `exposes: { './api': './src/fulgurjs/exposes/api.ts' }` → `const { xxx } = await loadRemote('remote/api')` | Low-frequency/heavy cross-app calls; any expose consumed by anyone; remote types cover it automatically |
175
175
 
176
- Method module conventions: export pure functions/service objects (no framework components); functions depending on host singletons (the http client going through shared etc.) are written directly — federation negotiation guarantees the same module graph. End-to-end example: [loading components and modules](../guide/components-and-modules.md#option-2-loadremote--imperative-loading-any-framework-or-pure-ts).
176
+ Method module conventions: export pure functions/service objects (no framework components); functions depending on host singletons (the http client going through shared etc.) are written directly — exposing a function does not guarantee the same business instance; explicitly share any required HTTP client or library and define its state ownership. End-to-end example: [loading components and modules](../guide/components-and-modules.md#option-2-loadremote--imperative-loading-any-framework-or-pure-ts).
177
177
 
178
178
  <a id="pages-api"></a>
179
179
 
@@ -182,25 +182,19 @@ Method module conventions: export pure functions/service objects (no framework c
182
182
  The host hands it the "URL path → remote exposes key" mapping for validation, so silent conflicts of parameterized routes fail at startup instead of loading the wrong component at runtime:
183
183
 
184
184
  ```ts
185
- import { definePages, remoteSchema } from '@fulgurjs/federation/vue' // same names from /react, /runtime
186
-
187
- export const PAGES = definePages(
188
- [
189
- { route: '/remote-a/home', name: 'RemoteAHome', title: 'Home' },
190
- // Parameterized route: by default spec is derived as drop the first segment + strip :param segments;
191
- // when the derivation collapses onto another entry that is an ERROR;
192
- // point at a dedicated expose with an explicit spec
193
- { route: '/remote-a/detail/:id', name: 'RemoteADetail', spec: 'pages/remote-a/detail', title: 'Detail' },
194
- ],
195
- {
196
- deriveSpec: (route) => 'pages/' + route.replace(/^\//, '').split('/').filter(s => !s.startsWith(':')).join('/'),
197
- remotes: { '/remote-a/': 'remote-a' }, // route prefix → remote name
198
- schema: remoteSchema, // { [remoteName]: { exposes: string[], exists?: boolean } }
199
- strict: true, // ERROR throws by default; false downgrades to console.error
200
- },
201
- )
185
+ // src/fulgurjs/host/pages.ts
186
+ import { definePages, remoteSchema } from '@fulgurjs/federation/vue'
187
+ import { pages, remotePrefixes } from './pages.data'
188
+
189
+ export const PAGES = definePages(pages, {
190
+ remotes: remotePrefixes,
191
+ schema: remoteSchema,
192
+ strict: true,
193
+ })
202
194
  ```
203
195
 
196
+ Reuse the same pages.data source; React imports these APIs from /react. createHostPages/createReactHostPages already perform this validation, so normal page adapters need no extra PAGES copy. Call definePages directly for standalone validation. Without an explicit spec the default drops the first route segment and :param segments, without adding pages/. Define a custom deriveSpec once and share it between runtime and CLI if needed.
197
+
204
198
  - **`definePages(pages, options?)`**: call timing = host startup (module evaluation). Returns the validated page table. Rules R1–R5: [remote page integration](../guide/remote-pages.md#define-pages); R3 depends on `schema` (the dev probe result), always an empty table in build (honest degradation).
205
199
  - **`validatePages(pages, options?)`**: returns the violation list (`PageViolation[]`, with `level` and descriptions) without throwing, useful for self-testing.
206
200
  - **`remoteSchema`**: the remote exposes probe the plugin fills in automatically during dev; business code only forwards it to `schema`.
@@ -215,9 +209,9 @@ Call timing and lifecycle notes (identical on both ends):
215
209
  - Page-table creation has zero loading side effects; components call `loadRemote(spec)` only **at render time**;
216
210
  - Load order: `beforeLoad` → optional remote `setup`/`onSession` → page module; failures enter the error state with no silent fallback;
217
211
  - The component cache is reused per spec and login generation: rebuilt **only when a new non-empty `sessionKey` arrives** (logout to `undefined` does not rebuild) — reloading after an account switch triggers a new `onSession` generation, while the module itself is reused from the runtime cache without re-downloading;
218
- - Vue-only `keepAliveNames` return value and `keepAlive: true` page-level keep-alive (cache cap max=8 LRU, cache key = sanitized page spec name, why everything is off by default): [remote page integration](../guide/remote-pages.md#page-adapters);
212
+ - Vue-only `keepAliveNames` return value and `keepAlive: true` page-level keep-alive (the host owns the actual KeepAlive, max and cache key): [remote page integration](../guide/remote-pages.md#page-adapters);
219
213
  - The React side provides no `keepAliveNames` (no keep-alive promise); routing is not a runtime dependency of the plugin — the example uses React Router 7 (`path` declared in the route table, `element` rendering the `component(spec)` output; parameterized routes pass params to remote page props via `useParams`/`useSearchParams`);
220
- - Cross-framework Context sharing: host and remote consumers get the same Context object through **the same expose instance** (the remote exposes `'./theme-context'` exporting a `createContext` instance; the host fetches it with `useLoadRemote` as the Provider, and the remote component reads the host's value via `useContext`); the plugin does not auto-bridge arbitrary React Contexts — the object must be shared explicitly.
214
+ - Sharing one Context object within a React tree: host and remote consumers get the same Context object through **the same expose instance** (the remote exposes `'./theme-context'` exporting a `createContext` instance; the host fetches it with `useLoadRemote` as the Provider, and the remote component reads the host's value via `useContext`); the plugin does not auto-bridge arbitrary React Contexts — the object must be shared explicitly. Separate bridge roots do not inherit the Provider; do not pass Context or ReactElement across different React renderers/versions.
221
215
 
222
216
  ## `remoteComponent` — direct rendering of remote components
223
217
 
@@ -244,7 +238,7 @@ const FederatedForm = remoteComponent('remote-a/Form', {
244
238
  Semantics and boundaries:
245
239
 
246
240
  - Internally = `defineAsyncComponent({ loader: () => loadRemote(spec, opts).then(m => m.default ?? m) })`, returning a standard Vue async component; `props` pass straight through at the usage site;
247
- - **Global-component auto-install (6.1.0)**: a remote component renders inside the **consumer app** context, and string tags in its templates (e.g. `<a-divider>`) resolve against the consumer's global registry — the provider app's `app.use(X)` registrations do not travel with the component. When the remote declares `globalComponents` in its setup module, this factory captures the currently rendering app on load and registers them idempotently (including consumption inside bridge child apps); see Section 6 `federation({ setup })` for the globalComponents contract;
241
+ - **Global-component auto-install **: a remote component renders inside the **consumer app** context, and string tags in its templates (e.g. `<a-divider>`) resolve against the consumer's global registry — the provider app's `app.use(X)` registrations do not travel with the component. When the remote declares `globalComponents` in its setup module, this factory captures the currently rendering app on load and registers them idempotently (including consumption inside bridge child apps); see Section 6 `federation({ setup })` for the globalComponents contract;
248
242
  - **No fallback/degradation whatsoever** (zero silent fallback): a load failure explicitly enters the error state; without an `errorComponent`, the built-in placeholder renders (error code + root cause + fix + **Retry load / Refresh page**), and the `fulgurjs:error` event is emitted as usual by the runtime layer;
249
243
  - Module dedup reuses `loadRemote`'s internal Promise cache — multiple component instances with the same spec load the container module only once;
250
244
  - Call timing = zero side effects at factory declaration; loading happens at render time;
@@ -304,7 +298,8 @@ federation({ name: 'remote-a', exposes: { /* … */ }, setup: './src/fulgurjs/se
304
298
  import type { RemoteSetupContext, RemoteSetupModule } from '@fulgurjs/federation/runtime'
305
299
 
306
300
  export default async function setup(context: RemoteSetupContext) {
307
- // App-level one-time registration: global components/directives, global styles, locale injection, plugin installs on the host app
301
+ // Container-level side effects, not initialization of every consumer app.
302
+ // Use globalComponents/Providers for consumer globals and factories for per-instance plugins.
308
303
  // context.appContext = the AppContext snapshot at call time; context.signal = the lifecycle signal
309
304
  }
310
305
  export async function onSession(context: RemoteSetupContext) {
@@ -331,7 +326,7 @@ export const globalComponents: RemoteSetupModule['globalComponents'] = {
331
326
  // AButton: Button,
332
327
  }
333
328
 
334
- // Optional named export since 6.6.0: ancestor provider declaration (array, outer→inner).
329
+ // Optional named export: ancestor provider declaration (array, outer→inner).
335
330
  // Components of this remote are automatically wrapped when rendered via
336
331
  // remoteComponent/createHostPages — exposed pages no longer hand-write AppProvider wrappers.
337
332
  // Complementary to globalComponents and not interchangeable: registration solves "string
@@ -346,6 +341,8 @@ export const pageProviders: RemoteSetupModule['pageProviders'] = [
346
341
 
347
342
  A module with only pageProviders/globalComponents/onSession can omit the default function. Empty modules and non-function defaults remain invalid.
348
343
 
344
+ **Keep execution scopes separate**: default setup runs once per remote container; onSession once per login generation; globalComponents installation targets each actual consumer app, and pageProviders applies during component rendering. defineApplication/defineBridgeApp factories run on every mount. Create independent app/router/store instances in factories, not one-time setup. A direct static/dynamic remote import uses the plugin loading chain, but does not render the export as a component or wrap manually rendered components in Providers.
345
+
349
346
  Fixed contract:
350
347
 
351
348
  | Dimension | Semantics |
@@ -373,18 +370,22 @@ Fixed contract:
373
370
 
374
371
  **Product scope**: bidirectional embedding with site-level mount/unmount — a Vue 3 host embedding a React 18/19 child app, a React host embedding a Vue 3 child app. For sub app internal routes synced with the host URL see [section 8](#url-sync-api) — explicit opt-in, off by default. Component-level interconversion, Angular, SSR/RSC, JS sandbox, and CSS isolation are outside the supported surface (see [supported scope](../troubleshooting/compatibility.md)).
375
372
 
376
- **Dual-framework installation contract (required)**: a bridge host installs `vue` + `react` + `react-dom` together, with all three shared keys `singleton: true`:
373
+ **Dependencies follow the provider arrangement**: same-framework bridges install their own framework only. Below is the cross-framework templates’ host-provides-both arrangement, which installs `vue` + `react` + `react-dom` and shares all three as singletons. This is not required for every bridge; see [dependency choices](../guide/app-bridge.md#framework-dependencies-same-framework-versus-cross-framework).
377
374
 
378
375
  ```ts
379
- // Bridge host fulgurjs.config.ts
380
- shared: {
381
- vue: { singleton: true },
382
- react: { singleton: true },
383
- 'react-dom': { singleton: true },
384
- }
376
+ import type { FederationOptions } from '@fulgurjs/federation'
377
+
378
+ export default {
379
+ name: 'bridge-host',
380
+ shared: {
381
+ vue: { singleton: true },
382
+ react: { singleton: true },
383
+ 'react-dom': { singleton: true },
384
+ }
385
+ } satisfies FederationOptions
385
386
  ```
386
387
 
387
- The child app only installs and shares its own framework (Vue child app: `vue`; React child app: `react` + `react-dom`). The zero-other-framework promise of pure Vue / pure React projects is unaffected. Shared subpaths (`react/jsx-runtime`, `react/jsx-dev-runtime`, `react-dom/client`) are negotiated to a single instance by the shared mechanism; the host must configure shared for `react` and `react-dom` (subpath negotiation depends on the parent keys). Real symptoms of a missing singleton (Invalid hook call, double instances) are error code `MFU-010` — the plugin runs the negotiation mechanism as-is and does not block misconfiguration.
388
+ The child app only installs and shares its own framework (Vue child app: `vue`; React child app: `react` + `react-dom`). The zero-other-framework promise of pure Vue / pure React projects is unaffected. Shared subpaths (`react/jsx-runtime`, `react/jsx-dev-runtime`, `react-dom/client`) are negotiated to a single instance by the shared mechanism; the host must configure shared for `react` and `react-dom` (subpath negotiation depends on the parent keys). Missing required instance identity can cause Invalid hook call; MFU-010 specifically reports an incompatible selected singleton version, not every instance-identity error.
388
389
 
389
390
  ### Child app side: `defineBridgeApp(factory, options?)`
390
391
 
@@ -446,14 +447,15 @@ export default defineBridgeApp(async (props, ctx) => {
446
447
  // Vue host (/vue)
447
448
  import { createVueBridgeApp } from '@fulgurjs/federation/vue'
448
449
  const RemoteReactApp = createVueBridgeApp<P>('react-remote/bridge', {
449
- loadingComponent?, errorComponent?, retries?, timeout?, getContext?, session?, sessionStorageKey?,
450
+ // Optional: loadingComponent, errorComponent, retries, timeout, getContext.
451
+ // Vue automatic mode: session: getIdentity; omit the sessionKey prop.
450
452
  })
451
453
  // Template: <RemoteReactApp :session-key="loginKey" :app-props="{ userId, onReady }" />
452
454
 
453
455
  // React host (/react)
454
456
  import { createReactBridgeApp } from '@fulgurjs/federation/react'
455
457
  const RemoteVueApp = createReactBridgeApp<P>('vue-remote/bridge', {
456
- fallback?, error?, retries?, timeout?, getContext?,
458
+ // Optional: fallback, error, retries, timeout, getContext.
457
459
  })
458
460
  // JSX: <RemoteVueApp sessionKey={loginKey} appProps={{ userId, onReady }} />
459
461
  ```
@@ -468,7 +470,7 @@ const RemoteVueApp = createReactBridgeApp<P>('vue-remote/bridge', {
468
470
 
469
471
  - **`appProps` snapshot semantics**: top-level fields are shallow-copied at mount time, while nested objects/reactive stores/functions keep their original references; later top-level replacements are **not tracked and do not re-render the child app** — to reset, rebuild with `:key`/key. New host closures are not passed to the child app automatically — to read live host state, pass stable callbacks or remount explicitly. Crossing roots inherits nothing: no host provide/inject, Pinia, React Context, or routing.
470
472
  - **`getContext`**: a side-effect-free **synchronous** getter, called before the actual load on first load, retries, and session switches; it returns a snapshot object (Promises/thenables and non-objects are rejected — `MFU-016`, `phase: 'getContext'`). The bridge layer first validates that the snapshot's `sessionKey` matches the controlled value (on mismatch `MFU-017`, and nothing is written globally); **after validation passes, the bridge layer calls `provideAppContext`** — the getter itself never writes globally. Without a getter, the existing `AppContext.sessionKey` must match the controlled value. On a generation switch, the bridge layer first calls `clearAppContext()` to clear old-account-only fields, then writes the new snapshot.
471
- - **`sessionKey` controlled semantics**: only `undefined` (no controlled session) / `null` (logged-out state: unmount immediately, keep an empty container, no more loadRemote) / a non-empty string (login generation) are accepted. Illegal values such as empty strings or numbers are rejected at mount with `MFU-017`. The full trigger table: [sub app bridge · session](../guide/app-bridge.md#session-sessionkey-and-appcontext). Vue hosts can delegate the generation to `createBridgeSession()` (below).
473
+ - **`sessionKey` controlled semantics**: only `undefined` (no controlled session) / `null` (logged-out state: unmount immediately, keep an empty container, no more loadRemote) / a non-empty string (login generation) are accepted. Illegal values such as empty strings or numbers are rejected at mount with `MFU-017`. The full trigger table: [sub app bridge · session](../guide/app-bridge.md#session-sessionkey-and-appcontext). Normal Vue hosts should use the session identity getter; use createBridgeSession only for explicit generation ownership (below).
472
474
  - **Multiple instances and page-level single session**: multiple instances of the same spec on one page are legal (keyed per el); `AppContext` is a page-level singleton — all controlled bridge instances on a page must share one session, and mismatched generations are rejected with `MFU-017`. Hosting two accounts simultaneously on one page is not promised.
473
475
  - **DOM ownership**: the wrapper component only creates and keeps a stable empty mount container; host re-renders never patch inside the child app's root. React host StrictMode double effects are safe. Vue `<KeepAlive>` deactivate is not unmount — a child app in a cached page keeps its root and state.
474
476
  - **Old requests never pretend to be cancelled**: work already inside `loadRemote` is not cancelled because the bridge layer invalidates it — late old results are discarded by generation; the remote's `onSession` must honor the `signal.aborted` contract.
@@ -498,7 +500,7 @@ export const bridgeSession = createBridgeSession()
498
500
  |---|---|
499
501
  | Return value | `{ sessionKey: Ref<string \| null>; renew(): string; clear(): void }`. `renew` generates and persists a new generation (calling it again while a generation is active = a forced account switch remount, consistent with the controlled semantics) and returns the new value; `clear` sets null and removes the stored value (the bridge component unmounts immediately on receiving null) |
500
502
  | Persistence | sessionStorage (default key `fulgurjs-bridge-session`): unchanged across reloads within the session, naturally invalidated when the tab closes. When storage is unavailable (private mode etc.) it silently degrades to pure memory — logout/account switch keep working, a reload lands logged out, no throw |
501
- | `options.storageKey` | Custom storage key. Nested hosts (B being both a child app and a host) coexisting on one page **must** pass distinct keys so refresh-restore cannot overwrite each other |
503
+ | `options.storageKey` | Custom storage key. Use distinct keys only for independent host sites/namespaces. Same-page controlled instances must still share one generation; nested B→C forwards the parent generation rather than renewing another one. Changing storageKey does not isolate page-level AppContext |
502
504
  | Call timing | `renew`/`clear` are decided by the host's login flow (successful login / logout); the helper is unaware of business login state |
503
505
  | React hosts | Not provided yet (React hosts manage the generation value themselves with the same semantic requirements; see the guide) |
504
506
 
@@ -508,7 +510,7 @@ export const bridgeSession = createBridgeSession()
508
510
 
509
511
  **Import locations**: `createVueBridgeNavigation`/`connectVueBridgeRouter` from `/vue`; `createReactBridgeNavigation`/`createReactBridgeRouter` from `/react`. Router libraries are optional peers (vue-router / react-router-dom installed by the consumer; a missing React-side dependency errors clearly only when `createReactBridgeRouter` is actually called).
510
512
 
511
- The bridge defaults to memory routing; URL sync makes the **host URL express the sub app's internal location** (refresh lands directly, bookmarks/sharing, back/forward). Explicit opt-in, off by default. **Architecture conventions**: the host Router is the only writer of browser history; the sub app uses a controlled memory router; the two ends exchange location through an independent routing channel; within the same instance, path/search/hash changes **do not remount the root, do not rebuild the store, do not reload the remote**.
513
+ The child factory must create a memory router; the plugin does not convert existing web routers. URL sync makes the **host URL express the sub app's internal location** (refresh lands directly, bookmarks/sharing, back/forward). Explicit opt-in, off by default. **Architecture conventions**: the host Router is the only writer of browser history; the sub app uses a controlled memory router; the two ends exchange location through an independent routing channel; within the same instance, path/search/hash changes **do not remount the root, do not rebuild the store, do not reload the remote**.
512
514
 
513
515
  ### API signatures
514
516
 
@@ -516,7 +518,7 @@ The bridge defaults to memory routing; URL sync makes the **host URL express the
516
518
  |---|---|---|
517
519
  | `createVueBridgeNavigation` | `(router: VueRouterLike) => BridgeHostNavigation` | Vue host navigation port. Vue Router fullPath is already the logical path; no deployment base needed. Vue Router 4, history or hash mode |
518
520
  | `createReactBridgeNavigation` | `(router: ReactDataRouterLike, { basename?, canNavigate? }) => BridgeHostNavigation` | React host navigation port. **Data router only** (`createBrowserRouter`/`createHashRouter` + `RouterProvider`); declarative mode (BrowserRouter) has no cancellation semantics and is not supported. React Router ≥ 6.11. `canNavigate` is optional and only an early rejection; the port observes the real blocker state — wait for `reset()` to resolve as cancelled and for the actual location to commit after `proceed()`; success is never judged solely by the settle of navigate's Promise |
519
- | `connectVueBridgeRouter` | `(routing: BridgeChildRouting, router: Router, { signal? }) => { ready: Promise, dispose(): void }` | Vue child app wiring for the controlled memory router (the low-level primitive); `await …ready` settles before `app.use(router)` (order must not be reversed). Regular bridged sub apps use `installVueBridgeRouter` above; this function remains for "dual-form" setups that need manual order control |
521
+ | `connectVueBridgeRouter` | `(routing: BridgeChildRouting, router: Router, { signal? }) => { ready: Promise, dispose(): void }` | Vue child app wiring for the controlled memory router (the low-level primitive); `await …ready` settles before `app.use(router)` (order must not be reversed). Normal Vue factories return { app, router } for automatic wiring; use installVueBridgeRouter when assembly continues after routing is ready, and this primitive only for custom connection management |
520
522
  | `installVueBridgeRouter` | `(app, router, ctx) => Promise<{ ready, dispose }>` | Vue child app router wiring in one call: validate the host channel (missing → MFU-034) → connect → await ready → `app.use(router)`; the ordering contract is owned by the plugin. `ctx` is the bridge factory's second parameter (`{ signal?, routing? }`) |
521
523
  | `createReactBridgeRouter` | `(routing, routes, { signal? }) => { element, dispose(), routerReady }` | React child app: returns a `RouterProvider` element used directly as the contract product; a missing routing throws the standardized MFU-034. `routerReady: Promise<Router>` is the wired memory-router readiness contract: the fast path (module-level warmup already settled, the common case) returns an **already-resolved** Promise whose value equals `element.props.router`; the slow path (rare warmup race) resolves when the lazy host finishes wiring on first render; a missing react-router-dom rejects with a clear error. Introspect via `await routerReady` instead of assuming `element.props.router` exists synchronously. `dispose()` tears down the wiring idempotently (auto-invoked by the signal); late tasks after dispose never write state |
522
524
 
@@ -577,72 +579,34 @@ Whether to commit the generated directory: ignored by default (CI syncs with `fu
577
579
 
578
580
  ## Optional project-side composition patterns: keep-alive, loading hints, preloading, and the diagnostics page
579
581
 
580
- This section records project-side composition patterns. The constants, pages, and diagnostics panel are implemented by the integrating project — they are not public APIs generated automatically after installing the plugin. `fulgurjs init` only generates the config starter template, not these project files; the plugin runtime is not involved at all. Configuration surface overview:
581
-
582
- | Capability | Config item | Type | Default | Configured at |
583
- |---|---|---|---|---|
584
- | Page keep-alive | `keepAlive` | `boolean` | `false` | Page route table entry |
585
- | Page loading skeleton | — (built-in, no config item) | — | See the built-in parameters below | Page factory |
586
- | Idle preload | `PREFETCH_REMOTES` | `string[]` | `[]` (whole-remote preload off, load on demand) | Constant at the top of the host bridge |
587
- | Federation diagnostics panel | — (a built-in page) | — | Always present | A custom route (e.g. `/fulgurjs-demo`) |
588
-
589
- ### 10.1 Page keep-alive — `keepAlive`
582
+ These are optional host compositions, not automatically generated project features. The plugin supplies page adapters, loading options and preloading functions; the host owns RouterView, cache policy, scheduling and diagnostics pages.
590
583
 
591
- A page-level boolean switch: when on, navigating away from the page **does not destroy the component instance** (deactivate), so form inputs, filters, and scroll position are restored verbatim on return.
584
+ ### Page keep-alive: render KeepAlive in the host
592
585
 
593
- ```ts
594
- // Page route table entry
595
- { route: '/some/page', name: 'SomePage', title: 'Page', keepAlive: true }
596
- // Off: omit the field, or set false explicitly (equivalent; off by default)
597
- ```
598
-
599
- - Cache cap `max: 8` (Vue's native LRU; the least recently used page instance is destroyed beyond the cap);
600
- - Cache key = the sanitized page spec name (`Fulgurjs_<remote>_<expose key>`); the same route with different params (different fullPath) each occupies a cache entry;
601
- - **Why everything is off by default**: caching heavy components (complex tables/form designers) costs a lot of memory; enable explicitly page by page;
602
- - If an enabled page's component registers window-level listeners/timers/context back-registrations, it must follow the [page unmount cleanup checklist](../guide/remote-pages.md#page-unmount-cleanup-checklist) (a kept-alive page only unmounts when actually evicted by the LRU).
586
+ A page flag `keepAlive: true` adds a wrapper name to the Vue adapter's `keepAliveNames`. The host must render a real `<KeepAlive :include="hostPages.keepAliveNames" :max="8">` to cache anything; 8 is an example cap, not a plugin default. Names select include, while the host chooses component keys. Caching parameter variants and restoring scroll are not automatic guarantees. React page adapters offer no equivalent keep-alive. See [ownership of page caching](../guide/remote-pages.md#page-specs-route-matching-and-keep-alive-ownership).
603
587
 
604
- ### 10.2 Page loading skeleton — built-in `loadingComponent` (no config item)
588
+ ### Page loading UI: use existing options
605
589
 
606
- The page component factory ships a built-in loading placeholder: during federation page chunk download/module execution it shows an animated gradient skeleton instead of a white screen. Built-in parameters: `delay: 200` (ms, shown only past the delay — fast loads do not flash) and the built-in error placeholder as `errorComponent` (spec + error code + root cause + fix). To customize the loading placeholder, bypass the page factory and use `remoteComponent(spec, { loadingComponent })`.
590
+ Vue `createHostPages({ loadingComponent, delay, errorComponent, ... })` accepts custom loading UI directly; there is no need to switch loading APIs. `loadingComponent` is unset by default, `delay` defaults to 200 ms, and the default error component provides diagnostics/recovery. React uses `fallback` and `error`. A sample project's animated skeleton is not a built-in plugin page.
607
591
 
608
- ### 10.3 Idle preload — `PREFETCH_REMOTES` (off by default)
609
-
610
- **First, separate the four layers (do not confuse "many declared routes" with "the first paint executes every page's code")**:
611
-
612
- | Layer | Mechanism | Timing | Network cost |
613
- |---|---|---|---|
614
- | ① Route table declaration | `pages.data.ts` is only a **data mapping**, importing no remote code | Build time | Zero |
615
- | ② Real page load | `createHostPages` wraps each page in an async component; `loadRemote(spec)` happens **at render time** | The user opens that page | That page's chunk + CSS (plus entry/shared dependencies/setup for that remote on first touch) |
616
- | ③ Single-page prefetch | `preloadRemote('remote-a/pages/home', { mode: 'prefetch' })` | Called proactively by the project | That expose's chunk + CSS (**download only, no execution**) |
617
- | ④ Whole-remote prefetch | `preloadRemote('remote-a', { mode: 'prefetch' })` | Explicitly enabled by the project | Every expose's chunk + CSS from the manifest (**download only, no execution**) |
618
-
619
- Prefetching is **downloading** (`modulepreload`/`stylesheet` links; `fetchPriority=low` only lowers priority — it does not mean no download) and **does not equal executing page code** — `container.get()`, component instantiation, and `setup/onSession` are triggered only by a real page's `loadRemote`. Loaded modules have a Promise cache: reopening the same page reuses the modules, and an account switch redoes session initialization but never re-downloads the JS.
592
+ ### Resource preloading: the project chooses when
620
593
 
621
594
  ```ts
622
- // Constant at the top of the host bridge; whole-remote preload off by default (load on demand)
623
- const PREFETCH_REMOTES: string[] = []
624
- // Explicitly warm a whole remote (downloads the full expose list; enable only with a real usage path)
625
- // const PREFETCH_REMOTES: string[] = ['remote-a', 'remote-b']
626
- // Single-page prefetch (recommended: only prefetch the clear next page; low priority, download only, no execution)
627
- // idle(() => preloadRemote('remote-a/pages/home', { mode: 'prefetch' }))
595
+ import { preloadRemote } from '@fulgurjs/federation/runtime'
596
+
597
+ // An already-registered full spec, e.g. a form the user is likely to open next.
598
+ void preloadRemote('remote-a/business-form', { mode: 'prefetch' }).catch(reportPreloadError)
628
599
  ```
629
600
 
630
- > The plugin configuration surface (`FederationOptions`) has **no** `host.prefetch` field — an older document wrongly claimed that config existed; the claim has been corrected. The preload list is just the constant in the host bridge; changing the list means changing exactly that one place.
601
+ `reportPreloadError` is a business logging function, not a plugin export. An existing async flow may instead await and catch errors. `preloadRemote` does not execute business modules or setup/onSession. A manifest supplies the selected expose's JS/CSS, or all exposes when only a remote name is given; without a manifest only the entry is preloaded. Preloaded JS is not executed, but injected stylesheets can affect the current page globally.
631
602
 
632
- Behavior and boundaries: preload failure **does not block the app** (the runtime emits the `fulgurjs:error` event with MFU-007 semantics plus a console warning); preloading injects `<link rel="modulepreload">` and `<link rel="stylesheet">` and never executes modules; `preload` waits for stylesheet load/error while `prefetch` loads in the background at low priority; the trigger is the host bridge executing synchronously on every page load (idempotent), with the actual prefetch happening in browser idle callbacks.
603
+ `prefetch` adds low-priority resource hints and still requests the manifest; **it does not schedule browser idle time automatically**. Projects choose idle scheduling if needed. It does not await all downloads. `preload` additionally waits only for tracked stylesheet load/error, not completion/executability of all JS. Its Promise is not evidence of a successful mount.
633
604
 
634
- ### 10.4 Federation diagnostics panel (a login-free page, no config items)
605
+ There is no `host.prefetch` option or plugin parameter named `PREFETCH_REMOTES`; projects may keep their own list. Preload network/style problems for registered remotes are recorded as MFU-007; invalid calls such as unknown remotes may still reject and must be handled.
635
606
 
636
- A project can build its own runtime diagnostics page (route `meta.ignoreAuth`), reading six blocks from the runtime registry live:
607
+ ### Diagnostics pages: project-owned routes and access control
637
608
 
638
- | Block | Content |
639
- |---|---|
640
- | ① Method module call demo | A button actually calls `loadRemote('remote/api')` and shows the result (a live sample of the method-reference channel) |
641
- | ② Remotes status | Each remote's entry / load status (idle/loading/loaded/failed) / container load duration |
642
- | ③ Shared negotiation | Shared key → version ← provider (multiple coexisting versions visible) |
643
- | ④ Context snapshot | The value shape of every AppContext key (function reference / object / string, including the events pool) |
644
- | ⑤ fulgurjs:error history log | Accumulated window events (timestamp + remote + error message); shows "no errors" when empty |
645
- | ⑥ Remote resource load durations | fulgurjs / remoteEntry / chunk entries and durations from performance resource |
609
+ The plugin supplies `__FULGURJS_INFO__`, `__FULGURJS_SCOPE__` and `fulgurjs:error`, not an automatic route or permanent panel. Create a page under the project's real access-control scheme if needed. Do not default it to ignoreAuth or display tokens, sensitive context values or business data. Durations, state, versions and error summaries can be displayed as needed; this page is not complete business acceptance.
646
610
 
647
611
  ## Related documents
648
612
 
@@ -1,6 +1,6 @@
1
1
  # Error code table
2
2
 
3
- > 49 error codes, grouped by segment: CFG (config time) / DEV (dev time) / BLD (build time) / MFU (runtime) / CC (cross-app context). Each entry gives the three-part **symptom / cause / fix**. The fix in the error message is concrete down to the config key / file / command; `fulgurjs doctor` can catch deployment-side MFU-001-class problems before release.
3
+ > All error codes, grouped by segment: CFG (config time) / DEV (dev time) / BLD (build time) / MFU (runtime) / CC (cross-app context). Each entry gives the three-part **symptom / cause / fix**. The fix in the error message is concrete down to the config key / file / command; `fulgurjs doctor` can catch deployment-side MFU-001-class problems before release.
4
4
  >
5
5
  > Maintenance note: adding or changing an error code requires a three-place sync — the source code tables (the MFU segment in `src/runtime/errors.ts` / the CC segment in `src/context.ts`) + the registry `CODE_REGISTRY` (`src/diagnostics.ts`) + this table; `scripts/check-manual-codes.mjs` inside `npm run build` verifies the three-way consistency (see [maintainers · testing (Chinese)](../../maintainers/testing.md)).
6
6
 
@@ -65,7 +65,7 @@
65
65
  | `MFU-007` | Console warning of a preload failure (business not blocked) | A chunk/CSS prefetched by `preloadRemote` is unreachable | Check the `fulgurjs:error` event history and the remote deployment; preload failure does not affect the later real load |
66
66
  | `MFU-008` | Runtime reports an unknown remote | The remote name in the spec/call is not in `remotes`/the runtime registry | Check the remote name spelling; for dynamic remotes call `registerRemote`/`registerRemotes` first |
67
67
  | `MFU-009` | Runtime reports the loaded module has no exports | The expose target file has no exports (an empty module / side effects only) | Add exports to the expose target file; confirm the expected file is what got loaded |
68
- | `MFU-010` | Runtime warns the selected shared singleton version fails some consumer's requirement (lists candidate versions, the provider, impact, and fixes; the same combination warns once) | The version singleton converged on is outside some consumer's requiredVersion; or a bridge host/child app missing singleton causes double-instance symptoms (Invalid hook call) | Align dependency versions; confirm the warning is acceptable or adjust requiredVersion; three singleton keys for cross-framework hosts |
68
+ | `MFU-010` | The selected shared singleton fails the consumer requiredVersion | The chosen instance in this shareScope/shareKey is incompatible with the requested range | Align versions or correct ranges after verifying compatibility; use strictVersion:true to reject; isolate the entire dependency group for different React majors |
69
69
  | `MFU-011` | Runtime reports an illegal setup lifecycle entry export shape (reports actual type / expected signature / fix) | Empty setup module, non-function default/onSession, or invalid component/Provider declarations | Export an initialization function or valid onSession/globalComponents/pageProviders; no empty function is required and an empty module is invalid |
70
70
  | `MFU-012` | That `loadRemote` rejects, reporting setup/onSession threw (cause carries the original exception) | The initialization code itself threw | Fix the error inside setup/onSession and retry directly — only the failed stage's cache is cleared (after a setup failure the retry starts at setup; an onSession failure only reruns the session segment); already-successful stages are not repeated |
71
71
  | `MFU-013` | Runtime reports the remote declares onSession but the host AppContext lacks sessionKey | The host did not provide a non-sensitive login generation ID (never use a token as one) | In the host login flow call `provideAppContext({ sessionKey })` — generate a new value on every successful login/re-login; token refreshes keep it |