@fulgurjs/federation 6.12.2 → 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.
- package/CHANGELOG.md +10 -0
- package/README.en.md +12 -5
- package/README.md +11 -4
- package/dist/cli.js +5 -3
- package/dist/index.cjs +6 -4
- package/dist/index.js +6 -4
- package/dist/runtime.js +1 -1
- package/docs/en/reference/api.md +64 -100
- package/docs/en/reference/errors.md +2 -2
- package/docs/en/reference/types.md +161 -162
- package/docs/zh/reference/api.md +69 -104
- package/docs/zh/reference/errors.md +3 -3
- package/docs/zh/reference/types.md +86 -86
- package/package.json +1 -1
package/docs/en/reference/api.md
CHANGED
|
@@ -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` |
|
|
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
|
|
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
|
-
|
|
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, //
|
|
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, //
|
|
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** (
|
|
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) |
|
|
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
|
|
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 = **
|
|
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 —
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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 (
|
|
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
|
-
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
**
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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).
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
584
|
+
### Page keep-alive: render KeepAlive in the host
|
|
592
585
|
|
|
593
|
-
|
|
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
|
-
###
|
|
588
|
+
### Page loading UI: use existing options
|
|
605
589
|
|
|
606
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
//
|
|
625
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
607
|
+
### Diagnostics pages: project-owned routes and access control
|
|
637
608
|
|
|
638
|
-
|
|
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
|
-
>
|
|
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` |
|
|
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 |
|