@asteby/metacore-runtime-react 9.2.0 → 11.0.0

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 CHANGED
@@ -1,5 +1,167 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 11.0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 3a3ea4b: fix: unify slot priority ordering across SDK and runtime-react (was
8
+ inconsistent — DESC is now canonical, see `docs/slot-priority.md`).
9
+
10
+ `Registry.registerSlot` in `@asteby/metacore-sdk` sorted ascending
11
+ ("lower renders first") while `slotRegistry` in
12
+ `@asteby/metacore-runtime-react` sorted descending ("higher renders
13
+ first"). The runtime-react behaviour matches `docs/dynamic-ui.md`,
14
+ `mergeNavigation` and every other priority sort in the codebase, so the
15
+ SDK has been flipped to match. Addons that register a single
16
+ contribution per slot — i.e. every in-tree consumer we audited — are
17
+ unaffected. Addons relying on the inverted SDK order will need to swap
18
+ their priority values.
19
+
20
+ - Updated dependencies [dee623a]
21
+ - Updated dependencies [56d2013]
22
+ - Updated dependencies [1c4a108]
23
+ - Updated dependencies [3a3ea4b]
24
+ - @asteby/metacore-sdk@2.6.0
25
+
26
+ ## 10.0.0
27
+
28
+ ### Minor Changes
29
+
30
+ - 9ce8269: feat: hot-swap reload policy (RFC-0001 D4 close)
31
+
32
+ Closes the gap between `useManifestHotSwapSubscriber` (already invalidates
33
+ the metadata cache) and the federation container of an already-mounted
34
+ addon, which keeps the old code in memory until something forces a
35
+ re-evaluation.
36
+
37
+ **runtime-react:**
38
+ - New `hotswap-reload-policy` module ships three policies and a single
39
+ hook that wires the chosen policy to the manifest hot-swap stream:
40
+ - `useHotSwapReload(client, { strategy })` returns `{ addonVersionMap }`
41
+ — a reactive map `addonKey → hashShort` that hosts wire into
42
+ `<AddonRoute version={addonVersionMap[addonKey]} ... />`. Default
43
+ strategy is `"rekey"`: React unmounts and remounts the addon subtree
44
+ on every swap, which forces the federation loader to re-fetch
45
+ `remoteEntry.js?v=<hash8>` and re-evaluate the exposed module.
46
+ - `strategy: "page-reload"` is an opt-in `window.location.reload()`
47
+ escape hatch for immersive addons with critical in-progress state
48
+ (POS, kitchen-display). Pair with `onBeforeReload` to surface an
49
+ "unsaved changes" prompt — returning `false` cancels the reload.
50
+ - `strategy: "manual"` only invokes `onSwap` with the message;
51
+ the host decides what to do.
52
+ - `clearFederationContainer(scope)` helper for hosts that hit
53
+ `Container already registered` after a re-key; call it from the
54
+ `onSwap` callback before the addon route re-mounts.
55
+ - `applyHotSwapReload` exported for non-React shells that want to
56
+ drive the policy from a vanilla container.
57
+
58
+ **sdk:**
59
+ - `loadFederatedAddon(spec, addonKey, version?)` accepts an optional
60
+ `version` so the loader cache-busts `remoteEntry.js` via
61
+ `?v=<hash8>` when the manifest hash bumps. Cache key includes the
62
+ version so a fresh hash triggers a fresh load instead of returning
63
+ the memoized old container.
64
+ - New `withVersionParam(url, hash)` helper (idempotent, fragment-safe,
65
+ replaces prior `v=` entries) exported for symmetry — the
66
+ runtime-react module re-uses the same algorithm.
67
+
68
+ **starter-core:**
69
+ - `<AddonRoute>` accepts a new optional `version?: string` prop. When
70
+ it changes, the route's children are wrapped in a `Fragment` with a
71
+ new `key`, forcing the federation loader to re-evaluate.
72
+
73
+ ### Host wire-up (4-5 lines)
74
+
75
+ ```tsx
76
+ const ws = useWebSocket()
77
+ useManifestHotSwapSubscriber(ws) // metadata cache
78
+ const { addonVersionMap } = useHotSwapReload(ws, { strategy: 'rekey' })
79
+ // …in your router:
80
+ <AddonRoute version={addonVersionMap[addonKey]} shell={renderShell}>
81
+ <AddonLoader scope={addonKey} url={remoteEntryUrl} api={api} />
82
+ </AddonRoute>
83
+ ```
84
+
85
+ Re-keying is intentionally destructive: any state inside the addon is
86
+ lost because the code version changed. Hosts that need a confirmation
87
+ gate should pass `onBeforeReload` and prompt the user before the swap
88
+ applies.
89
+
90
+ - 04362f2: feat: immersive layout, federation shared-deps helper polish, wasm client
91
+
92
+ **sdk:**
93
+ - `FrontendSpec` now carries `layout?: "shell" | "immersive"`. Mirrors the
94
+ upcoming kernel-side `manifest.FrontendSpec.Layout` field. `undefined` is
95
+ treated as `"shell"` (legacy behaviour) so the change is purely additive.
96
+ Exposed as the `AddonLayout` type alias for explicit consumers.
97
+ - New `wasm-client` module — frontend twin of `kernel/runtime/wasm`. Ships
98
+ `loadAddonWasm({ url, integrity, imports })` (SRI verification + instantiate
99
+ pipeline) and `callAddonExport(instance, fn, payload)` honouring the same
100
+ `ptr<<32 | len` packed ABI the Go example backends use (`alloc`, `free`,
101
+ `memory`). Lets POS / kitchen-display / signage addons run their compiled
102
+ module locally for sub-50ms latency without a webhook round trip. Typed
103
+ errors (`WasmIntegrityError`, `WasmAbiError`) surface failure cause cleanly.
104
+
105
+ **runtime-react:**
106
+ - New `<AddonLayoutProvider>`, `useAddonLayout()`, `useAddonLayoutControl()`
107
+ and `useDeclareAddonLayout()` API in `addon-layout-context`. The host shell
108
+ reads the active layout and hides Sidebar / Topbar / breadcrumbs when an
109
+ addon declares `layout: "immersive"`. Cleanup restores chrome on unmount,
110
+ so navigating away from an immersive addon brings the shell back.
111
+ - `<AddonLoader>` accepts an optional `layout` prop and propagates it through
112
+ the context, so hosts get the chrome switch wired without per-route plumbing.
113
+
114
+ **starter-config:**
115
+ - `metacoreFederationShared()` now accepts `extra: Record<string, ShareConfig>`
116
+ for the typical "I just want to add a package with explicit config" case
117
+ (`extra: { lodash: { singleton: true } }`). The existing `extras: string[]`
118
+ and `overrides` knobs are retained for backwards compatibility.
119
+ - `METACORE_FEDERATION_SINGLETONS` adds `@asteby/metacore-app-providers` so
120
+ the SDK's transport-agnostic platform provider keeps a single instance
121
+ between host and addons.
122
+
123
+ - ba60c8f: feat: immersive route wrapper + manifest hot-swap subscriber (RFC-0001 D1 + D4)
124
+
125
+ **runtime-react:**
126
+ - `metadata-cache` gains `invalidateAddon(addonKey, matcher?)` and `clearAll()`
127
+ so consumers can flush scoped cache entries when an addon's manifest hash
128
+ changes. The default matcher recognises `addonKey`, `${addonKey}.`,
129
+ `${addonKey}:` and `${addonKey}/` prefixes; hosts that namespace their
130
+ `model` keys differently can pass a custom matcher.
131
+ - New `manifest-hotswap-subscriber` module ships:
132
+ - `ADDON_MANIFEST_CHANGED_TYPE` — the `ws.MessageType` constant the kernel
133
+ emits via `bridge.WSManifestBroadcaster`.
134
+ - `wireHotSwapInvalidation(client, options?)` — imperative helper hosts call
135
+ once at boot. Accepts any object exposing `subscribe(type, handler)`
136
+ (structurally compatible with `useWebSocket().subscribe`), invalidates
137
+ the metadata cache for the bumped addon, and optionally invokes an
138
+ `onSwap` side-effect callback (handy for forcing a `window.location.reload()`
139
+ when the running addon's bundle hash changes, since metadata invalidation
140
+ alone does not swap the federation container already in memory).
141
+ - `useManifestHotSwapSubscriber(client)` — React hook variant for hosts
142
+ that prefer mounting the wire-up next to their WebSocket provider.
143
+
144
+ **starter-core:**
145
+ - New `AddonRoute` component closes the host side of RFC-0001 D1 (immersive
146
+ end-to-end). It reads `useAddonLayout()` from runtime-react and either
147
+ renders the addon inside a caller-provided shell renderer (default
148
+ `"shell"` layout) or strips chrome and pins the addon to the viewport
149
+ (`fixed inset-0 z-50`) when the active layout is `"immersive"`. Supports
150
+ both prop-driven layout (no shell flash for always-immersive routes like
151
+ POS / kitchen-display) and context-driven layout (addon calls
152
+ `useDeclareAddonLayout("immersive")` after mount). Cleanup restores
153
+ `"shell"` so navigating away brings the chrome back.
154
+
155
+ Together these closures unblock zero-polling hot-swap reloads in the
156
+ metadata layer and let immersive addons own the viewport without each app
157
+ re-implementing the shell branch.
158
+
159
+ ### Patch Changes
160
+
161
+ - Updated dependencies [9ce8269]
162
+ - Updated dependencies [04362f2]
163
+ - @asteby/metacore-sdk@2.5.0
164
+
3
165
  ## 9.2.0
4
166
 
5
167
  ### Minor Changes
@@ -0,0 +1,49 @@
1
+ import type { AddonLayout } from '@asteby/metacore-sdk';
2
+ export type { AddonLayout };
3
+ interface AddonLayoutState {
4
+ /** Active layout. `"shell"` (default) or `"immersive"`. */
5
+ layout: AddonLayout;
6
+ /**
7
+ * Imperative setter for the host or an addon-loader to mutate the active
8
+ * layout. Exposed for advanced use; most callers should use
9
+ * `useDeclareAddonLayout(layout)` from a route component, which scopes the
10
+ * change to the route's mount lifetime.
11
+ */
12
+ setLayout: (layout: AddonLayout) => void;
13
+ }
14
+ export interface AddonLayoutProviderProps {
15
+ /** Initial layout — usually `"shell"`. */
16
+ initial?: AddonLayout;
17
+ children: React.ReactNode;
18
+ }
19
+ /**
20
+ * Wrap the host app once, above the router outlet. The provider keeps the
21
+ * currently-active layout in state; addon-loader and `useDeclareAddonLayout`
22
+ * mutate it from below.
23
+ */
24
+ export declare function AddonLayoutProvider({ initial, children, }: AddonLayoutProviderProps): import("react/jsx-runtime").JSX.Element;
25
+ /**
26
+ * Read the currently-active layout. The host shell calls this and decides
27
+ * whether to render its chrome. Returns `"shell"` when no provider is
28
+ * mounted, so apps that have not adopted immersive addons keep working.
29
+ */
30
+ export declare function useAddonLayout(): AddonLayout;
31
+ /**
32
+ * Imperative API — the value returned mirrors `useAddonLayout()` but also
33
+ * exposes the setter for hosts that need to flip the layout outside of a
34
+ * route lifecycle (e.g. a hotkey forcing kiosk mode). Most addon entries do
35
+ * NOT need this; prefer `useDeclareAddonLayout`.
36
+ */
37
+ export declare function useAddonLayoutControl(): AddonLayoutState;
38
+ /**
39
+ * Declare the layout from the addon side. Mounts the value, restores
40
+ * `"shell"` on unmount. Skip when `layout` is undefined so route components
41
+ * can pass `manifest.frontend?.layout` directly without branching.
42
+ *
43
+ * function PosEntry({ manifest }: { manifest: Manifest }) {
44
+ * useDeclareAddonLayout(manifest.frontend?.layout)
45
+ * return <PosScreen />
46
+ * }
47
+ */
48
+ export declare function useDeclareAddonLayout(layout: AddonLayout | undefined): void;
49
+ //# sourceMappingURL=addon-layout-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addon-layout-context.d.ts","sourceRoot":"","sources":["../src/addon-layout-context.tsx"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAEvD,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,UAAU,gBAAgB;IACtB,2DAA2D;IAC3D,MAAM,EAAE,WAAW,CAAA;IACnB;;;;;OAKG;IACH,SAAS,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAA;CAC3C;AAWD,MAAM,WAAW,wBAAwB;IACrC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAChC,OAAiB,EACjB,QAAQ,GACX,EAAE,wBAAwB,2CAW1B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,WAAW,CAE5C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,IAAI,gBAAgB,CAExD;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAY3E"}
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // AddonLayoutContext — broadcast the active addon entry's layout selection
3
+ // (`shell` vs `immersive`) up to the host so it can hide/show its chrome
4
+ // (Sidebar, Topbar, breadcrumbs) when an immersive addon is mounted.
5
+ //
6
+ // Why a context rather than a prop on the host shell:
7
+ //
8
+ // 1. The host shell is rendered ABOVE the addon route in the tree, but the
9
+ // decision about what layout the addon wants comes from the addon itself
10
+ // (manifest.frontend.layout) which the AddonLoader knows about at mount
11
+ // time. A bottom-up signal via context inverts the dependency cleanly.
12
+ //
13
+ // 2. Addon entries can swap layouts at runtime (think a kiosk-mode toggle
14
+ // inside a POS). A context value reactively updates the host without
15
+ // asking each route to wire props.
16
+ //
17
+ // 3. When the user navigates AWAY from an immersive addon, the AddonLoader
18
+ // unmounts, its layout context updater fires `setLayout("shell")` from
19
+ // a cleanup effect, and the chrome restores automatically.
20
+ //
21
+ // Host integration (starter-core, ops, …):
22
+ //
23
+ // function AppShell({ children }) {
24
+ // const layout = useAddonLayout()
25
+ // const chrome = layout !== "immersive"
26
+ // return (
27
+ // <div className={chrome ? "grid grid-cols-[280px_1fr]" : "h-dvh w-dvw"}>
28
+ // {chrome && <Sidebar />}
29
+ // <main>{chrome && <Topbar />}{children}</main>
30
+ // </div>
31
+ // )
32
+ // }
33
+ //
34
+ // The context defaults to `"shell"`, so apps that never mount an
35
+ // `<AddonLayoutProvider>` keep the legacy behaviour.
36
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
37
+ const defaultState = {
38
+ layout: 'shell',
39
+ setLayout: () => {
40
+ /* noop — provider missing; consumers degrade to legacy "shell" */
41
+ },
42
+ };
43
+ const AddonLayoutContext = createContext(defaultState);
44
+ /**
45
+ * Wrap the host app once, above the router outlet. The provider keeps the
46
+ * currently-active layout in state; addon-loader and `useDeclareAddonLayout`
47
+ * mutate it from below.
48
+ */
49
+ export function AddonLayoutProvider({ initial = 'shell', children, }) {
50
+ const [layout, setLayout] = useState(initial);
51
+ const value = useMemo(() => ({ layout, setLayout }), [layout]);
52
+ return (_jsx(AddonLayoutContext.Provider, { value: value, children: children }));
53
+ }
54
+ /**
55
+ * Read the currently-active layout. The host shell calls this and decides
56
+ * whether to render its chrome. Returns `"shell"` when no provider is
57
+ * mounted, so apps that have not adopted immersive addons keep working.
58
+ */
59
+ export function useAddonLayout() {
60
+ return useContext(AddonLayoutContext).layout;
61
+ }
62
+ /**
63
+ * Imperative API — the value returned mirrors `useAddonLayout()` but also
64
+ * exposes the setter for hosts that need to flip the layout outside of a
65
+ * route lifecycle (e.g. a hotkey forcing kiosk mode). Most addon entries do
66
+ * NOT need this; prefer `useDeclareAddonLayout`.
67
+ */
68
+ export function useAddonLayoutControl() {
69
+ return useContext(AddonLayoutContext);
70
+ }
71
+ /**
72
+ * Declare the layout from the addon side. Mounts the value, restores
73
+ * `"shell"` on unmount. Skip when `layout` is undefined so route components
74
+ * can pass `manifest.frontend?.layout` directly without branching.
75
+ *
76
+ * function PosEntry({ manifest }: { manifest: Manifest }) {
77
+ * useDeclareAddonLayout(manifest.frontend?.layout)
78
+ * return <PosScreen />
79
+ * }
80
+ */
81
+ export function useDeclareAddonLayout(layout) {
82
+ const { setLayout } = useAddonLayoutControl();
83
+ // useCallback so the effect only re-runs on a real layout change, not on
84
+ // every render of the consumer that happens to forward an inline literal.
85
+ const apply = useCallback(setLayout, [setLayout]);
86
+ useEffect(() => {
87
+ if (!layout || layout === 'shell')
88
+ return;
89
+ apply(layout);
90
+ return () => {
91
+ apply('shell');
92
+ };
93
+ }, [layout, apply]);
94
+ }
@@ -1,4 +1,4 @@
1
- import type { AddonAPI } from '@asteby/metacore-sdk';
1
+ import type { AddonAPI, AddonLayout } from '@asteby/metacore-sdk';
2
2
  declare global {
3
3
  interface Window {
4
4
  [key: string]: any;
@@ -21,7 +21,19 @@ export interface AddonLoaderProps {
21
21
  onReady?: () => void;
22
22
  /** Called if loading fails. */
23
23
  onError?: (err: Error) => void;
24
+ /**
25
+ * Layout the host shell should render the addon under, mirroring
26
+ * `manifest.frontend.layout`. Default (undefined / `"shell"`) keeps the
27
+ * legacy chrome (Sidebar, Topbar, breadcrumbs). `"immersive"` flips the
28
+ * shared {@link useAddonLayout} context so the host shell hides chrome
29
+ * while the addon is mounted and restores it on unmount.
30
+ *
31
+ * Hosts that consume the context (see `useAddonLayout` /
32
+ * `<AddonLayoutProvider>`) do NOT need to branch on this prop themselves
33
+ * — the loader sets the context value via {@link useDeclareAddonLayout}.
34
+ */
35
+ layout?: AddonLayout;
24
36
  children?: React.ReactNode;
25
37
  }
26
- export declare function AddonLoader({ scope, url, module, api, fallback, onReady, onError, children, }: AddonLoaderProps): import("react/jsx-runtime").JSX.Element;
38
+ export declare function AddonLoader({ scope, url, module, api, fallback, onReady, onError, layout, children, }: AddonLoaderProps): import("react/jsx-runtime").JSX.Element;
27
39
  //# sourceMappingURL=addon-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"addon-loader.d.ts","sourceRoot":"","sources":["../src/addon-loader.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAEpD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,MAAM;QACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;QAClB,wBAAwB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3D,wBAAwB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KACrD;CACJ;AAED,MAAM,WAAW,gBAAgB;IAC7B,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,GAAG,EAAE,QAAQ,CAAA;IACb,wCAAwC;IACxC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAuCD,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,GAAG,EACH,MAAqB,EACrB,GAAG,EACH,QAAe,EACf,OAAO,EACP,OAAO,EACP,QAAQ,GACX,EAAE,gBAAgB,2CAgClB"}
1
+ {"version":3,"file":"addon-loader.d.ts","sourceRoot":"","sources":["../src/addon-loader.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAGjE,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,MAAM;QACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;QAClB,wBAAwB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3D,wBAAwB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KACrD;CACJ;AAED,MAAM,WAAW,gBAAgB;IAC7B,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,GAAG,EAAE,QAAQ,CAAA;IACb,wCAAwC;IACxC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAuCD,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,GAAG,EACH,MAAqB,EACrB,GAAG,EACH,QAAe,EACf,OAAO,EACP,OAAO,EACP,MAAM,EACN,QAAQ,GACX,EAAE,gBAAgB,2CAsClB"}
@@ -3,6 +3,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  // waits for the `window[scope]` container to initialize, then calls the
4
4
  // addon's `register(api)` export with the AddonAPI injected by the host.
5
5
  import { useEffect, useRef, useState } from 'react';
6
+ import { useDeclareAddonLayout } from './addon-layout-context';
6
7
  const loadedScripts = new Map();
7
8
  function loadScript(url, scope) {
8
9
  const key = `${scope}::${url}`;
@@ -34,10 +35,15 @@ async function loadRemote(scope, module) {
34
35
  const factory = await container.get(module);
35
36
  return factory();
36
37
  }
37
- export function AddonLoader({ scope, url, module = './register', api, fallback = null, onReady, onError, children, }) {
38
+ export function AddonLoader({ scope, url, module = './register', api, fallback = null, onReady, onError, layout, children, }) {
38
39
  const [status, setStatus] = useState('loading');
39
40
  const [error, setError] = useState(null);
40
41
  const didRegister = useRef(false);
42
+ // Propagate the addon's preferred layout to the host shell via context.
43
+ // No-op when `layout` is undefined or `"shell"` (legacy default). Cleanup
44
+ // restores `"shell"` automatically when the loader unmounts, so chrome
45
+ // returns as soon as the user navigates away from an immersive addon.
46
+ useDeclareAddonLayout(layout);
41
47
  useEffect(() => {
42
48
  let cancelled = false;
43
49
  (async () => {
@@ -0,0 +1,155 @@
1
+ import { type AddonManifestChangedMessage, type ManifestHotSwapClient, type WireHotSwapInvalidationOptions } from './manifest-hotswap-subscriber';
2
+ /**
3
+ * One of three strategies for reacting to an `ADDON_MANIFEST_CHANGED` event:
4
+ *
5
+ * * `"rekey"` — re-mount the addon route by flipping the key. Default.
6
+ * * `"page-reload"` — `window.location.reload()`. Opt-in.
7
+ * * `"manual"` — no automatic action; the host handles it via `onSwap`.
8
+ */
9
+ export type HotSwapReloadStrategy = 'rekey' | 'page-reload' | 'manual';
10
+ /**
11
+ * Config for {@link useHotSwapReload}. `strategy` is the only required field
12
+ * — pass `{ strategy: "rekey" }` for the default behaviour or omit the
13
+ * config entirely.
14
+ */
15
+ export interface HotSwapReloadConfig {
16
+ /** Reload policy. See {@link HotSwapReloadStrategy}. */
17
+ strategy?: HotSwapReloadStrategy;
18
+ /**
19
+ * Optional gate invoked **before** the reload action fires. Return
20
+ * `false` (or a Promise resolving to `false`) to cancel — useful for
21
+ * "unsaved changes" prompts on immersive addons. Receives the original
22
+ * `ADDON_MANIFEST_CHANGED` message so the prompt can name the addon.
23
+ *
24
+ * Runs for `"page-reload"` (cancels the `window.location.reload()`)
25
+ * and `"rekey"` (cancels the version bump, leaving the addon mounted
26
+ * with the old code — the host can re-trigger the swap later by
27
+ * re-calling the hook output's `reload()` method).
28
+ *
29
+ * Ignored for `"manual"` — the host owns the reload there.
30
+ */
31
+ onBeforeReload?: (event: AddonManifestChangedMessage) => boolean | Promise<boolean>;
32
+ /**
33
+ * Side-effect hook invoked after the policy has run (or after
34
+ * `onBeforeReload` returned `false`). Receives the message and the
35
+ * effective action that was taken: `"rekey"`, `"page-reload"`,
36
+ * `"cancelled"` or `"manual"`. Hosts wire telemetry / toasts here.
37
+ */
38
+ onSwap?: (event: AddonManifestChangedMessage, action: 'rekey' | 'page-reload' | 'cancelled' | 'manual') => void;
39
+ /**
40
+ * Optional matcher forwarded to the underlying
41
+ * {@link useManifestHotSwapSubscriber} for cache invalidation.
42
+ */
43
+ matcher?: WireHotSwapInvalidationOptions['matcher'];
44
+ }
45
+ export interface UseHotSwapReloadResult {
46
+ /**
47
+ * Reactive map `addonKey → hashShort`. Stable identity per render
48
+ * (only changes when a swap lands). Wire it into
49
+ * `<AddonRoute version={addonVersionMap[addonKey]} ... />` so React
50
+ * re-keys the subtree on hash change.
51
+ *
52
+ * Missing entries return `undefined`; the AddonRoute treats that as
53
+ * "no version pinned yet" and keeps a stable key.
54
+ */
55
+ addonVersionMap: Record<string, string>;
56
+ }
57
+ /**
58
+ * Subscribe to manifest hot-swap events and apply a reload policy.
59
+ *
60
+ * **Strategy = `"rekey"` (default):**
61
+ * maintains `addonVersionMap` so `<AddonRoute version=...>` re-keys
62
+ * the subtree on every swap. The federation loader picks the new hash
63
+ * up via {@link withVersionParam}, fetches a fresh `remoteEntry.js`,
64
+ * and registers a new container.
65
+ *
66
+ * **Strategy = `"page-reload"` (opt-in):**
67
+ * calls `onBeforeReload` (if supplied); if it resolves truthy,
68
+ * `window.location.reload()` fires. The `addonVersionMap` is still
69
+ * updated for callers that want to mirror it elsewhere.
70
+ *
71
+ * **Strategy = `"manual"`:**
72
+ * no automatic action. The `onSwap` callback fires with `"manual"`;
73
+ * the host decides what to do. `addonVersionMap` is updated so a
74
+ * later opt-in remount picks up the right hash.
75
+ *
76
+ * @example
77
+ * const ws = useWebSocket()
78
+ * useManifestHotSwapSubscriber(ws) // invalidates metadata cache
79
+ * const { addonVersionMap } = useHotSwapReload({ strategy: 'rekey' })
80
+ * // …in your router:
81
+ * <AddonRoute version={addonVersionMap[addonKey]}>
82
+ * <AddonLoader scope={addonKey} url={url} api={api} />
83
+ * </AddonRoute>
84
+ */
85
+ /**
86
+ * Effect that {@link applyHotSwapReload} can take. Useful as a discriminator
87
+ * for tests and telemetry callbacks. `"noop"` is emitted when a malformed
88
+ * message is ignored (e.g. missing `addonKey`).
89
+ */
90
+ export type HotSwapReloadAction = 'rekey' | 'page-reload' | 'cancelled' | 'manual' | 'noop';
91
+ export interface HotSwapReloadDeps {
92
+ /** Hash → versionMap setter. Receives an updater fn, à la React state. */
93
+ setVersionMap: (updater: (prev: Record<string, string>) => Record<string, string>) => void;
94
+ /** Defaults to `window.location.reload`. Overridable for tests / SSR. */
95
+ reload?: () => void;
96
+ }
97
+ /**
98
+ * Pure (testable) implementation of the swap handler. Decides the action
99
+ * given a message + config + deps, applies side effects via `deps`, and
100
+ * returns the action it took so callers can fire telemetry.
101
+ *
102
+ * Exported for unit tests; the React hook below composes it with React
103
+ * state. Hosts that want to drive the policy from a non-React context
104
+ * (e.g. a vanilla web component shell) can call this directly.
105
+ */
106
+ export declare function applyHotSwapReload(message: AddonManifestChangedMessage, config: HotSwapReloadConfig, deps: HotSwapReloadDeps): Promise<HotSwapReloadAction>;
107
+ export declare function useHotSwapReload(client: ManifestHotSwapClient | undefined | null, config?: HotSwapReloadConfig): UseHotSwapReloadResult;
108
+ /**
109
+ * Append a `?v=<hash8>` query string to a `remoteEntry.js` URL so the
110
+ * browser treats it as a distinct resource and bypasses any HTTP / module
111
+ * cache. Idempotent — calling twice with the same hash returns the same
112
+ * URL. Preserves existing query params; replaces a previous `v=` entry if
113
+ * present so successive bumps don't accumulate stale parameters.
114
+ *
115
+ * Pure function (no `window` access) — safe to call in SSR.
116
+ *
117
+ * @example
118
+ * withVersionParam('/api/addons/pos/frontend/remoteEntry.js', 'abc123ef')
119
+ * // → '/api/addons/pos/frontend/remoteEntry.js?v=abc123ef'
120
+ *
121
+ * withVersionParam('/r.js?foo=1', 'abc123ef')
122
+ * // → '/r.js?foo=1&v=abc123ef'
123
+ *
124
+ * withVersionParam('/r.js?v=oldhash', 'abc123ef')
125
+ * // → '/r.js?v=abc123ef'
126
+ */
127
+ export declare function withVersionParam(url: string, hash: string | undefined): string;
128
+ /**
129
+ * Remove the federation container previously registered on `window[scope]`.
130
+ * Hosts call this from `onSwap` before letting the addon route re-mount
131
+ * so the next `remoteEntry.js` injection creates a fresh container instead
132
+ * of short-circuiting on the cached one.
133
+ *
134
+ * Best-effort: if `window` is undefined (SSR) or the scope was never
135
+ * registered, this is a no-op. Returns `true` if a container was actually
136
+ * removed, `false` otherwise — useful for telemetry.
137
+ *
138
+ * **Caveat:** some federation runtimes wrap the container in a Proxy
139
+ * whose internal state survives `delete`. If you hit `Container already
140
+ * registered` after calling this, the federation runtime is holding the
141
+ * reference internally and the only reliable swap is `"page-reload"`.
142
+ */
143
+ export declare function clearFederationContainer(scope: string): boolean;
144
+ /**
145
+ * Normalise a manifest hash for cache-busting. Accepts the full kernel
146
+ * format (`sha256:abc...`), a bare hex digest, or `undefined`. Returns
147
+ * an 8-character lowercase prefix that's short enough to keep URLs
148
+ * readable while remaining collision-resistant across realistic addon
149
+ * versioning timelines.
150
+ *
151
+ * Exported for tests; hosts that want the full hash for their own
152
+ * telemetry should read `message.payload.newHash` directly.
153
+ */
154
+ export declare function shortenHash(hash: string | undefined): string | undefined;
155
+ //# sourceMappingURL=hotswap-reload-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hotswap-reload-policy.d.ts","sourceRoot":"","sources":["../src/hotswap-reload-policy.ts"],"names":[],"mappings":"AA+DA,OAAO,EAEH,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,EACtC,MAAM,+BAA+B,CAAA;AAEtC;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,aAAa,GAAG,QAAQ,CAAA;AAEtE;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAChC,wDAAwD;IACxD,QAAQ,CAAC,EAAE,qBAAqB,CAAA;IAChC;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,CACb,KAAK,EAAE,2BAA2B,KACjC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CACL,KAAK,EAAE,2BAA2B,EAClC,MAAM,EAAE,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,KACvD,IAAI,CAAA;IACT;;;OAGG;IACH,OAAO,CAAC,EAAE,8BAA8B,CAAC,SAAS,CAAC,CAAA;CACtD;AAED,MAAM,WAAW,sBAAsB;IACnC;;;;;;;;OAQG;IACH,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GACzB,OAAO,GACP,aAAa,GACb,WAAW,GACX,QAAQ,GACR,MAAM,CAAA;AAEZ,MAAM,WAAW,iBAAiB;IAC9B,0EAA0E;IAC1E,aAAa,EAAE,CACX,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAChE,IAAI,CAAA;IACT,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACpC,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,mBAAmB,EAC3B,IAAI,EAAE,iBAAiB,GACxB,OAAO,CAAC,mBAAmB,CAAC,CA6C9B;AAED,wBAAgB,gBAAgB,CAC5B,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,EAChD,MAAM,GAAE,mBAAwB,GACjC,sBAAsB,CA2BxB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAiB9E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAa/D;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE"}