@modular-react/journeys 0.1.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/LICENSE +21 -0
- package/README.md +1669 -0
- package/dist/index.d.ts +754 -0
- package/dist/index.js +303 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime-DyU_PmaC.js +599 -0
- package/dist/runtime-DyU_PmaC.js.map +1 -0
- package/dist/testing.d.ts +121 -0
- package/dist/testing.js +102 -0
- package/dist/testing.js.map +1 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./runtime-DyU_PmaC.js";
|
|
2
|
+
import { Component as s, createContext as c, createElement as l, useContext as u, useEffect as d, useMemo as f, useRef as p, useState as m, useSyncExternalStore as h } from "react";
|
|
3
|
+
import { ModuleErrorBoundary as g, ModuleExitProvider as _, useModuleExit as v } from "@modular-react/react";
|
|
4
|
+
import { jsx as y } from "react/jsx-runtime";
|
|
5
|
+
//#region src/define-journey.ts
|
|
6
|
+
var b = () => (e) => e;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/persistence.ts
|
|
9
|
+
function x(e) {
|
|
10
|
+
return e;
|
|
11
|
+
}
|
|
12
|
+
function S(e) {
|
|
13
|
+
let { keyFor: t, storage: n } = e, r = () => {
|
|
14
|
+
try {
|
|
15
|
+
return typeof n == "function" ? n() : n === void 0 ? typeof localStorage < "u" ? localStorage : null : n;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
keyFor: t,
|
|
22
|
+
load: (e) => {
|
|
23
|
+
let t = r();
|
|
24
|
+
if (!t) return null;
|
|
25
|
+
let n;
|
|
26
|
+
try {
|
|
27
|
+
n = t.getItem(e);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (n === null) return null;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(n);
|
|
34
|
+
} catch {
|
|
35
|
+
try {
|
|
36
|
+
t.removeItem(e);
|
|
37
|
+
} catch {}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
save: (e, t) => {
|
|
42
|
+
let n = r();
|
|
43
|
+
n && n.setItem(e, JSON.stringify(t));
|
|
44
|
+
},
|
|
45
|
+
remove: (e) => {
|
|
46
|
+
let t = r();
|
|
47
|
+
t && t.removeItem(e);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function C(e) {
|
|
52
|
+
let t = e.clone !== !1, n = (e) => t ? JSON.parse(JSON.stringify(e)) : e, r = new Map(e.initial ? Array.from(e.initial, ([e, t]) => [e, n(t)]) : void 0);
|
|
53
|
+
return {
|
|
54
|
+
keyFor: e.keyFor,
|
|
55
|
+
load: (e) => {
|
|
56
|
+
let t = r.get(e);
|
|
57
|
+
return t ? n(t) : null;
|
|
58
|
+
},
|
|
59
|
+
save: (e, t) => {
|
|
60
|
+
r.set(e, n(t));
|
|
61
|
+
},
|
|
62
|
+
remove: (e) => {
|
|
63
|
+
r.delete(e);
|
|
64
|
+
},
|
|
65
|
+
size: () => r.size,
|
|
66
|
+
entries: () => Array.from(r, ([e, t]) => [e, n(t)]),
|
|
67
|
+
clear: () => {
|
|
68
|
+
r.clear();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/provider.tsx
|
|
74
|
+
var w = c(null);
|
|
75
|
+
function T(e) {
|
|
76
|
+
let { runtime: t, onModuleExit: n, children: r } = e, i = {
|
|
77
|
+
runtime: t,
|
|
78
|
+
onModuleExit: n
|
|
79
|
+
};
|
|
80
|
+
return l(w.Provider, { value: i }, l(_, {
|
|
81
|
+
onExit: n,
|
|
82
|
+
children: r
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
function E() {
|
|
86
|
+
return u(w);
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/outlet.tsx
|
|
90
|
+
var D = 2;
|
|
91
|
+
function O(e) {
|
|
92
|
+
let t = E(), { runtime: r, instanceId: i, modules: a, loadingFallback: o, onFinished: s, onStepError: c, retryLimit: u = D, notFoundComponent: f, errorComponent: h } = e, g = r ?? t?.runtime;
|
|
93
|
+
if (!g) throw Error("[@modular-react/journeys] <JourneyOutlet> needs a runtime. Either pass `runtime` or mount a <JourneyProvider>.");
|
|
94
|
+
let _ = j(g, i), v = n(g), y = a ?? v.__moduleMap, [b, x] = m(0), S = p(!0);
|
|
95
|
+
d(() => (S.current = !0, () => {
|
|
96
|
+
S.current = !1, queueMicrotask(() => {
|
|
97
|
+
if (S.current) return;
|
|
98
|
+
let e = v.__getRecord(i);
|
|
99
|
+
e && (e.status !== "active" && e.status !== "loading" || e.listeners.size > 0 || g.end(i, { reason: "unmounted" }));
|
|
100
|
+
});
|
|
101
|
+
}), [
|
|
102
|
+
g,
|
|
103
|
+
i,
|
|
104
|
+
v
|
|
105
|
+
]);
|
|
106
|
+
let C = p(!1);
|
|
107
|
+
if (d(() => {
|
|
108
|
+
_ && (_.status !== "completed" && _.status !== "aborted" || C.current || (C.current = !0, s?.({
|
|
109
|
+
status: _.status,
|
|
110
|
+
payload: _.terminalPayload,
|
|
111
|
+
instanceId: _.id,
|
|
112
|
+
journeyId: _.journeyId
|
|
113
|
+
})));
|
|
114
|
+
}, [_, s]), !_) return null;
|
|
115
|
+
if (_.status === "loading") return o ?? null;
|
|
116
|
+
if (_.status === "completed" || _.status === "aborted") return null;
|
|
117
|
+
let w = _.step;
|
|
118
|
+
if (!w) return null;
|
|
119
|
+
let T = y[w.moduleId], O = T?.entryPoints?.[w.entry];
|
|
120
|
+
if (!T || !O) return l(f ?? k, {
|
|
121
|
+
moduleId: w.moduleId,
|
|
122
|
+
entry: w.entry
|
|
123
|
+
});
|
|
124
|
+
let A = v.__getRecord(i), N = v.__getRegistered(_.journeyId);
|
|
125
|
+
if (!A || !N) return null;
|
|
126
|
+
let { exit: P, goBack: F } = v.__bindStepCallbacks(A, N), I = (e) => {
|
|
127
|
+
v.__fireComponentError(i, e, w);
|
|
128
|
+
let t = c?.(e, { step: w }) ?? "abort";
|
|
129
|
+
if (t === "retry" && (A.retryCount >= u ? t = "abort" : A.retryCount += 1), t === "abort") {
|
|
130
|
+
g.end(i, {
|
|
131
|
+
reason: "component-error",
|
|
132
|
+
error: e
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
t === "retry" && x((e) => e + 1);
|
|
137
|
+
}, L = O.component, R = `${A.stepToken}:${b}`;
|
|
138
|
+
return l(M, {
|
|
139
|
+
moduleId: w.moduleId,
|
|
140
|
+
onError: I,
|
|
141
|
+
errorComponent: h,
|
|
142
|
+
key: R,
|
|
143
|
+
children: null
|
|
144
|
+
}, l(L, {
|
|
145
|
+
input: w.input,
|
|
146
|
+
exit: P,
|
|
147
|
+
goBack: F
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
function k({ moduleId: e, entry: t }) {
|
|
151
|
+
return l("div", { style: {
|
|
152
|
+
padding: "1rem",
|
|
153
|
+
color: "#c53030"
|
|
154
|
+
} }, `Journey outlet: no entry "${e}.${t}" on the registered modules.`);
|
|
155
|
+
}
|
|
156
|
+
function A({ moduleId: e, error: t }) {
|
|
157
|
+
let n = t instanceof Error ? t.message : String(t);
|
|
158
|
+
return l("div", {
|
|
159
|
+
style: {
|
|
160
|
+
padding: "1rem",
|
|
161
|
+
border: "1px solid #e53e3e",
|
|
162
|
+
borderRadius: "0.5rem",
|
|
163
|
+
margin: "1rem"
|
|
164
|
+
},
|
|
165
|
+
role: "alert",
|
|
166
|
+
"data-journey-step-error": e
|
|
167
|
+
}, l("h3", { style: {
|
|
168
|
+
color: "#e53e3e",
|
|
169
|
+
margin: "0 0 0.5rem 0"
|
|
170
|
+
} }, `Module "${e}" encountered an error`), l("pre", { style: {
|
|
171
|
+
fontSize: "0.875rem",
|
|
172
|
+
color: "#718096",
|
|
173
|
+
whiteSpace: "pre-wrap"
|
|
174
|
+
} }, n));
|
|
175
|
+
}
|
|
176
|
+
function j(e, t) {
|
|
177
|
+
let n = f(() => (n) => e.subscribe(t, n), [e, t]), r = () => e.getInstance(t);
|
|
178
|
+
return h(n, r, r);
|
|
179
|
+
}
|
|
180
|
+
var M = class extends s {
|
|
181
|
+
state = { error: null };
|
|
182
|
+
static getDerivedStateFromError(e) {
|
|
183
|
+
return { error: e };
|
|
184
|
+
}
|
|
185
|
+
componentDidCatch(e) {
|
|
186
|
+
this.props.onError(e);
|
|
187
|
+
}
|
|
188
|
+
render() {
|
|
189
|
+
return this.state.error ? l(this.props.errorComponent ?? A, {
|
|
190
|
+
moduleId: this.props.moduleId,
|
|
191
|
+
error: this.state.error
|
|
192
|
+
}) : this.props.children;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/module-tab.tsx
|
|
197
|
+
function N(e) {
|
|
198
|
+
let { module: t, entry: n, input: r, tabId: i, onExit: a } = e, o = t.entryPoints, s = o ? Object.keys(o) : [], c = n, u = null;
|
|
199
|
+
n === void 0 ? s.length === 1 ? c = s[0] : s.length > 1 && (u = `Module "${t.id}" exposes multiple entries (${s.join(", ")}); pass the \`entry\` prop to disambiguate.`) : o && !(n in o) ? u = `Module "${t.id}" has no entry "${n}". Registered: ${s.join(", ") || "(none)"}.` : o || (c = void 0, u = `Module "${t.id}" has no entry points; \`entry="${n}"\` cannot be resolved.`);
|
|
200
|
+
let d = c ? o?.[c] : void 0, f = v(t.id, c ?? "", {
|
|
201
|
+
tabId: i,
|
|
202
|
+
localOnExit: a
|
|
203
|
+
}), p;
|
|
204
|
+
if (u) p = l("div", { style: {
|
|
205
|
+
padding: "1rem",
|
|
206
|
+
color: "#c53030"
|
|
207
|
+
} }, u);
|
|
208
|
+
else if (d) if (r === void 0 && !("input" in e)) p = l("div", { style: {
|
|
209
|
+
padding: "1rem",
|
|
210
|
+
color: "#c53030"
|
|
211
|
+
} }, `Module "${t.id}" entry "${c ?? ""}" was rendered without an \`input\` prop. Pass \`input={undefined}\` explicitly if the entry accepts no input.`);
|
|
212
|
+
else {
|
|
213
|
+
let e = d.component;
|
|
214
|
+
p = l(e, {
|
|
215
|
+
input: r,
|
|
216
|
+
exit: f
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else if (t.component) {
|
|
220
|
+
let e = t.component;
|
|
221
|
+
p = l(e, {
|
|
222
|
+
input: r,
|
|
223
|
+
tabId: i
|
|
224
|
+
});
|
|
225
|
+
} else p = l("div", { style: {
|
|
226
|
+
padding: "1rem",
|
|
227
|
+
color: "#c53030"
|
|
228
|
+
} }, `Module "${t.id}" has no entry points and no component.`);
|
|
229
|
+
return l(g, {
|
|
230
|
+
moduleId: t.id,
|
|
231
|
+
children: p
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/plugin.tsx
|
|
236
|
+
function P(e = {}) {
|
|
237
|
+
let n = [];
|
|
238
|
+
return {
|
|
239
|
+
name: "journeys",
|
|
240
|
+
extend() {
|
|
241
|
+
return { registerJourney(e, r) {
|
|
242
|
+
let i = e, o = a(i);
|
|
243
|
+
if (o.length > 0) throw new t(o);
|
|
244
|
+
n.push({
|
|
245
|
+
definition: i,
|
|
246
|
+
options: r
|
|
247
|
+
});
|
|
248
|
+
} };
|
|
249
|
+
},
|
|
250
|
+
validate({ modules: e }) {
|
|
251
|
+
n.length > 0 && r(n, e);
|
|
252
|
+
},
|
|
253
|
+
onResolve({ moduleDescriptors: t, debug: r }) {
|
|
254
|
+
return o(n, {
|
|
255
|
+
modules: t,
|
|
256
|
+
debug: e.debug ?? r
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
contributeNavigation() {
|
|
260
|
+
let t = [];
|
|
261
|
+
for (let r of n) {
|
|
262
|
+
let n = r.options?.nav;
|
|
263
|
+
if (!n) continue;
|
|
264
|
+
let i = {
|
|
265
|
+
label: n.label,
|
|
266
|
+
to: "",
|
|
267
|
+
...n.icon === void 0 ? {} : { icon: n.icon },
|
|
268
|
+
...n.group === void 0 ? {} : { group: n.group },
|
|
269
|
+
...n.order === void 0 ? {} : { order: n.order },
|
|
270
|
+
...n.hidden === void 0 ? {} : { hidden: n.hidden },
|
|
271
|
+
...n.meta === void 0 ? {} : { meta: n.meta },
|
|
272
|
+
action: {
|
|
273
|
+
kind: "journey-start",
|
|
274
|
+
journeyId: r.definition.id,
|
|
275
|
+
...n.buildInput ? { buildInput: n.buildInput } : {}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
e.buildNavItem ? t.push(e.buildNavItem(i, {
|
|
279
|
+
...n,
|
|
280
|
+
journeyId: r.definition.id
|
|
281
|
+
})) : t.push(i);
|
|
282
|
+
}
|
|
283
|
+
return t;
|
|
284
|
+
},
|
|
285
|
+
providers({ runtime: t }) {
|
|
286
|
+
let n = ({ children: n }) => /* @__PURE__ */ y(T, {
|
|
287
|
+
runtime: t,
|
|
288
|
+
onModuleExit: e.onModuleExit,
|
|
289
|
+
children: n
|
|
290
|
+
});
|
|
291
|
+
return n.displayName = "JourneysPluginProvider", [n];
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/handle.ts
|
|
297
|
+
function F(e) {
|
|
298
|
+
return { id: e.id };
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
export { i as JourneyHydrationError, O as JourneyOutlet, T as JourneyProvider, t as JourneyValidationError, N as ModuleTab, e as UnknownJourneyError, o as createJourneyRuntime, C as createMemoryPersistence, S as createWebStoragePersistence, b as defineJourney, F as defineJourneyHandle, x as defineJourneyPersistence, P as journeysPlugin, E as useJourneyContext, r as validateJourneyContracts, a as validateJourneyDefinition };
|
|
302
|
+
|
|
303
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/define-journey.ts","../src/persistence.ts","../src/provider.tsx","../src/outlet.tsx","../src/module-tab.tsx","../src/plugin.tsx","../src/handle.ts"],"sourcesContent":["import type { JourneyDefinition, ModuleTypeMap } from \"./types.js\";\n\n/**\n * Declare a journey with full type inference on entry/exit contracts,\n * transitions, and the journey's private state.\n *\n * **Why the empty parens?** TypeScript can't partially infer generics: if\n * `defineJourney` took `<TModules, TState, TInput>` in a single call, you'd\n * either have to spell all three — losing the ability to infer `TInput`\n * from `initialState`'s parameter — or spell none, losing the ability to\n * narrow `TModules` / `TState`. The two-call shape splits the generics\n * so `TModules` + `TState` are explicit (first call) while `TInput` is\n * inferred from the definition object (second call).\n *\n * ```ts\n * defineJourney<OnboardingModules, OnboardingState>()({\n * id: \"customer-onboarding\",\n * version: \"1.0.0\",\n * initialState: (input: { customerId: string }) => ({ ... }),\n * // TInput is inferred as { customerId: string } here\n * start: (state) => ({ module: \"profile\", entry: \"review\", input: { customerId: state.customerId } }),\n * transitions: { ... },\n * });\n * ```\n *\n * Zero runtime cost — the definition is returned unchanged.\n */\nexport const defineJourney =\n <TModules extends ModuleTypeMap, TState>() =>\n // `TInput = void` matters: when `initialState` takes no parameter\n // there is no inferable position for TInput, and without a default TS\n // falls back to `unknown`. That silently disables the rest-tuple\n // ergonomics on `runtime.start(handle)` and `simulateJourney(journey)`\n // — callers would still have to pass `undefined`. Defaulting to `void`\n // keeps \"no input\" journeys truly zero-arg.\n <TInput = void>(definition: JourneyDefinition<TModules, TState, TInput>) =>\n definition;\n","import type { JourneyPersistence, SerializedJourney } from \"./types.js\";\n\n/**\n * Narrowed variant of {@link JourneyPersistence} whose methods are\n * guaranteed synchronous — `load` returns `SerializedJourney<TState> | null`\n * (not `MaybePromise<…>`), `save`/`remove` return `void` (not\n * `MaybePromise<void>`). Stock adapters return this shape so direct\n * `.load(key)` callers don't need to discriminate sync vs async or cast\n * away the promise half of the union.\n *\n * Structurally assignable to `JourneyPersistence<TState, TInput>`, so the\n * value can still be passed to `registerJourney({ persistence })` without\n * widening.\n */\nexport interface SyncJourneyPersistence<TState, TInput = unknown> {\n readonly keyFor: (ctx: { journeyId: string; input: TInput }) => string;\n readonly load: (key: string) => SerializedJourney<TState> | null;\n readonly save: (key: string, blob: SerializedJourney<TState>) => void;\n readonly remove: (key: string) => void;\n}\n\n/**\n * Identity helper that ties a persistence adapter's `keyFor` input to a\n * journey's `TInput` so callers get compile-time checking on per-customer /\n * per-session keys. Zero runtime cost — the adapter is returned as-is.\n *\n * The return type preserves both `TInput` and `TState`, so shells calling\n * `persistence.keyFor({ input })` *outside* the runtime (e.g. to probe\n * storage before opening a journey tab) still see the journey's typed\n * input shape — no `input: unknown` erasure at the boundary.\n *\n * ```ts\n * interface CustomerInput { customerId: string }\n *\n * const journeyPersistence = defineJourneyPersistence<CustomerInput, MyState>({\n * keyFor: ({ input }) => `journey:${input.customerId}:onboarding`,\n * load: (k) => backend.load(k),\n * save: (k, b) => backend.save(k, b),\n * remove: (k) => backend.remove(k),\n * });\n *\n * // Outside the runtime — `input` is typed as CustomerInput:\n * const key = journeyPersistence.keyFor({\n * journeyId: \"onboarding\",\n * input: { customerId: \"C-1\" },\n * });\n * ```\n */\nexport function defineJourneyPersistence<TInput, TState>(\n adapter: JourneyPersistence<TState, TInput>,\n): JourneyPersistence<TState, TInput> {\n return adapter;\n}\n\n/**\n * @deprecated Alias kept for source compatibility. Use\n * {@link JourneyPersistence} directly — it now carries a `TInput` generic.\n */\nexport type TypedJourneyPersistenceAdapter<TInput, TState> = JourneyPersistence<TState, TInput>;\n\n// ---------------------------------------------------------------------------\n// Web Storage adapter (localStorage / sessionStorage)\n// ---------------------------------------------------------------------------\n\nexport interface WebStoragePersistenceOptions<TInput> {\n /**\n * Compute the persistence key from the journey id and starting input.\n * Must be deterministic — `runtime.start()` probes this key to find an\n * existing instance and achieve idempotency.\n */\n readonly keyFor: (ctx: { journeyId: string; input: TInput }) => string;\n /**\n * The `Storage` instance to read from and write to. Accepts either a\n * direct reference or a lazy getter; the getter is invoked on every\n * call, which keeps SSR safe (the default returns `null` when\n * `localStorage` is not defined on the global).\n *\n * Defaults to `globalThis.localStorage` (or `null` under SSR) when\n * omitted or explicitly `undefined`. Pass `sessionStorage` for\n * tab-scoped persistence, `null` to force the SSR no-op path, or any\n * `Storage`-shaped stub for custom backends.\n */\n readonly storage?: Storage | null | (() => Storage | null);\n}\n\n/**\n * `JourneyPersistence` backed by the Web Storage API\n * (`localStorage` / `sessionStorage`). Covers the 80% case: a few KB of\n * JSON per journey-per-customer, read on mount, written on every transition.\n *\n * SSR-safe — when `storage` resolves to `null` (server rendering, private\n * modes where storage is disabled) all four methods no-op and `load`\n * returns `null`, so the runtime mints a fresh instance as it would\n * without persistence configured.\n *\n * Corrupt entries (invalid JSON) are removed lazily on `load` so a single\n * bad write doesn't block future loads for the same key.\n *\n * ```ts\n * export const journeyPersistence = createWebStoragePersistence<\n * OnboardingInput,\n * OnboardingState\n * >({\n * keyFor: ({ journeyId, input }) =>\n * `journey:${input.customerId}:${journeyId}`,\n * });\n *\n * // Tab-scoped, cleared when the tab closes:\n * const sessionScoped = createWebStoragePersistence<MyInput, MyState>({\n * keyFor: ({ journeyId, input }) => `s:${input.id}:${journeyId}`,\n * storage: typeof sessionStorage !== \"undefined\" ? sessionStorage : null,\n * });\n * ```\n *\n * **Limits.** `localStorage` is synchronous and capped at ~5 MB per origin.\n * Writes throw `QuotaExceededError` when full; the error bubbles so the\n * app can surface it. If a journey holds large state or offline-first\n * matters, write a custom adapter against IndexedDB.\n */\nexport function createWebStoragePersistence<TInput, TState>(\n options: WebStoragePersistenceOptions<TInput>,\n): SyncJourneyPersistence<TState, TInput> {\n const { keyFor, storage } = options;\n\n const resolve = (): Storage | null => {\n try {\n if (typeof storage === \"function\") return storage();\n if (storage !== undefined) return storage;\n return typeof localStorage !== \"undefined\" ? localStorage : null;\n } catch {\n // `SecurityError` when storage is access-blocked (sandboxed iframe,\n // cookies disabled, strict privacy settings). Degrade to the SSR\n // no-op path instead of crashing the runtime.\n return null;\n }\n };\n\n return {\n keyFor,\n load: (key) => {\n const s = resolve();\n if (!s) return null;\n let raw: string | null;\n try {\n raw = s.getItem(key);\n } catch {\n // Read-side access failure — fall back to \"no existing instance\"\n // so the runtime mints a fresh one rather than wedging the page.\n return null;\n }\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as SerializedJourney<TState>;\n } catch {\n // Don't let a single bad write wedge future loads for this key.\n try {\n s.removeItem(key);\n } catch {\n // Best-effort cleanup; ignore secondary access denials.\n }\n return null;\n }\n },\n save: (key, blob) => {\n const s = resolve();\n if (!s) return;\n s.setItem(key, JSON.stringify(blob));\n },\n remove: (key) => {\n const s = resolve();\n if (!s) return;\n s.removeItem(key);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// In-memory adapter\n// ---------------------------------------------------------------------------\n\nexport interface MemoryPersistenceOptions<TInput, TState> {\n /** Same contract as `WebStoragePersistenceOptions.keyFor`. */\n readonly keyFor: (ctx: { journeyId: string; input: TInput }) => string;\n /**\n * Optional seed entries. Handy for tests that want the runtime to find a\n * pre-persisted journey on first `start()` without walking through the\n * flow to produce the blob.\n */\n readonly initial?: Iterable<readonly [string, SerializedJourney<TState>]>;\n /**\n * When true (default), stored blobs are deep-cloned on both `save` and\n * `load` so callers mutating the returned object can't corrupt the\n * backing store. Set to `false` to skip the clone in hot test loops\n * where you've verified nobody mutates the blob.\n */\n readonly clone?: boolean;\n}\n\n/**\n * `SyncJourneyPersistence` augmented with test-only inspection helpers\n * (`size`, `entries`, `clear`). Methods are sync (no `MaybePromise`) so\n * tests can `expect(store.load(k)).toEqual(blob)` without awaiting, and\n * the value is still assignable to `JourneyPersistence<TState, TInput>`\n * for `registerJourney({ persistence })`.\n */\nexport interface MemoryPersistence<TInput, TState> extends SyncJourneyPersistence<TState, TInput> {\n /** Number of entries currently stored. */\n readonly size: () => number;\n /** Snapshot of all `[key, blob]` pairs. Each blob is cloned if cloning is enabled. */\n readonly entries: () => ReadonlyArray<readonly [string, SerializedJourney<TState>]>;\n /** Drop all entries. */\n readonly clear: () => void;\n}\n\n/**\n * Map-backed `JourneyPersistence` for tests and SSR. Gives tests a\n * canonical isolated store (no bleed between cases, no `localStorage`\n * mocking) and keeps the runtime's persistence code paths exercised.\n *\n * On SSR, it's a safe \"persistence is configured but nothing survives\n * the request\" mode — every start mints a fresh instance, and save /\n * remove are no-ops from the client's perspective.\n *\n * ```ts\n * const store = createMemoryPersistence<MyInput, MyState>({\n * keyFor: ({ journeyId, input }) => `${journeyId}:${input.id}`,\n * });\n *\n * const runtime = createJourneyRuntime(\n * [{ definition: myJourney, options: { persistence: store } }],\n * { modules },\n * );\n *\n * // Tests can assert directly against the store:\n * expect(store.size()).toBe(1);\n * ```\n */\nexport function createMemoryPersistence<TInput, TState>(\n options: MemoryPersistenceOptions<TInput, TState>,\n): MemoryPersistence<TInput, TState> {\n const shouldClone = options.clone !== false;\n\n const copy = (blob: SerializedJourney<TState>): SerializedJourney<TState> =>\n shouldClone ? (JSON.parse(JSON.stringify(blob)) as SerializedJourney<TState>) : blob;\n\n // Seed entries go through the same `copy` as `save` so callers mutating the\n // source array after construction can't corrupt the backing store.\n const store = new Map<string, SerializedJourney<TState>>(\n options.initial ? Array.from(options.initial, ([k, v]) => [k, copy(v)] as const) : undefined,\n );\n\n return {\n keyFor: options.keyFor,\n load: (key) => {\n const blob = store.get(key);\n return blob ? copy(blob) : null;\n },\n save: (key, blob) => {\n store.set(key, copy(blob));\n },\n remove: (key) => {\n store.delete(key);\n },\n size: () => store.size,\n entries: () => Array.from(store, ([k, v]) => [k, copy(v)] as const),\n clear: () => {\n store.clear();\n },\n };\n}\n","import { createContext, createElement, useContext } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { ModuleExitProvider, type ModuleExitEvent } from \"@modular-react/react\";\n\nimport type { JourneyRuntime } from \"./types.js\";\n\n/**\n * Shell-level context read by `<JourneyOutlet>` so callers don't have to\n * thread `runtime` through every container that hosts a journey.\n *\n * `onModuleExit` is still surfaced here for backward compatibility with\n * consumers that introspect the provider value. The actual dispatch now\n * flows through `<ModuleExitProvider>` from `@modular-react/react`, which\n * `<JourneyProvider>` mounts automatically. Prefer consuming\n * `useModuleExit` / `useModuleExitDispatcher` from the react package\n * directly in new code.\n */\nexport interface JourneyProviderValue {\n /** Journey runtime — usually `manifest.journeys`. */\n readonly runtime: JourneyRuntime;\n /**\n * Optional fallback invoked by `<ModuleTab>` / `<ModuleRoute>` after any\n * local `onExit` prop has run. Wiring this at the provider level gives a\n * shell global telemetry / tab-close forwarding without threading the\n * callback through every host.\n */\n readonly onModuleExit?: (event: ModuleExitEvent) => void;\n}\n\nconst JourneyContext = createContext<JourneyProviderValue | null>(null);\n\nexport interface JourneyProviderProps {\n readonly runtime: JourneyRuntime;\n readonly onModuleExit?: JourneyProviderValue[\"onModuleExit\"];\n readonly children: ReactNode;\n}\n\n/**\n * Provides the journey runtime to descendant `<JourneyOutlet>` nodes, and\n * composes over `<ModuleExitProvider>` so module hosts (`<ModuleTab>`,\n * `<ModuleRoute>`, anything using `useModuleExit`) see the shell's\n * `onModuleExit` dispatcher without needing a second provider.\n *\n * Existing journey consumers do not need to change — `onModuleExit` keeps\n * firing for every module exit emitted outside a journey step.\n */\nexport function JourneyProvider(props: JourneyProviderProps): ReactNode {\n const { runtime, onModuleExit, children } = props;\n const value: JourneyProviderValue = { runtime, onModuleExit };\n return createElement(\n JourneyContext.Provider,\n { value },\n createElement(ModuleExitProvider, { onExit: onModuleExit, children }),\n );\n}\n\n/** Read the current provider value, or `null` when none is mounted. */\nexport function useJourneyContext(): JourneyProviderValue | null {\n return useContext(JourneyContext);\n}\n","import {\n Component,\n createElement,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport type { ExitPointMap, ModuleDescriptor, ModuleEntryProps } from \"@modular-react/core\";\n\nimport { getInternals } from \"./runtime.js\";\nimport { useJourneyContext } from \"./provider.js\";\nimport type { InstanceId, JourneyRuntime, JourneyStep, TerminalOutcome } from \"./types.js\";\n\nexport type JourneyStepErrorPolicy = \"abort\" | \"retry\" | \"ignore\";\n\n/** Maximum automatic retries before falling back to `abort`. */\nconst DEFAULT_RETRY_CAP = 2;\n\nexport interface JourneyOutletNotFoundProps {\n readonly moduleId: string;\n readonly entry: string;\n}\n\nexport interface JourneyOutletErrorProps {\n readonly moduleId: string;\n readonly error: unknown;\n}\n\nexport interface JourneyOutletProps {\n /**\n * Runtime to drive the outlet against. Optional when a `<JourneyProvider>`\n * is mounted above — the outlet reads the runtime from context in that\n * case. Explicit prop overrides context, so one outlet can reach a\n * different runtime when needed.\n */\n readonly runtime?: JourneyRuntime;\n readonly instanceId: InstanceId;\n /**\n * Module descriptors the outlet resolves step components against.\n * Optional — when omitted, the outlet pulls the descriptors the runtime\n * was constructed with (the common case).\n */\n readonly modules?: Readonly<Record<string, ModuleDescriptor<any, any, any, any>>>;\n readonly loadingFallback?: ReactNode;\n readonly onFinished?: (outcome: TerminalOutcome) => void;\n readonly onStepError?: (err: unknown, ctx: { step: JourneyStep }) => JourneyStepErrorPolicy;\n /**\n * Cap on `retry` responses before the outlet falls back to `abort`. The\n * counter increments on every retry from `onStepError` and is never reset,\n * so a step that causes a downstream step to also throw cannot bypass the\n * cap by bumping the step token. Default: 2.\n */\n readonly retryLimit?: number;\n /**\n * Rendered when the current step points at a module/entry that is not\n * registered with the runtime. Defaults to a plain red notice.\n */\n readonly notFoundComponent?: ComponentType<JourneyOutletNotFoundProps>;\n /**\n * Rendered when a step component throws. Defaults to a plain red notice\n * with the error message. Receives the raw error so shells can route it\n * through their own reporting.\n */\n readonly errorComponent?: ComponentType<JourneyOutletErrorProps>;\n}\n\n/**\n * Renders the current step of a journey instance. Host-agnostic — works in\n * a tab, modal, route element, or plain `<div>`. On unmount while active,\n * the instance is abandoned (deferred by a microtask so React 18/19\n * StrictMode's simulated mount/unmount/mount cycle does not tear the\n * instance down on its first visit).\n */\nexport function JourneyOutlet(props: JourneyOutletProps): ReactNode {\n const context = useJourneyContext();\n const {\n runtime: runtimeProp,\n instanceId,\n modules: modulesProp,\n loadingFallback,\n onFinished,\n onStepError,\n retryLimit = DEFAULT_RETRY_CAP,\n notFoundComponent,\n errorComponent,\n } = props;\n\n const runtime = runtimeProp ?? context?.runtime;\n if (!runtime) {\n throw new Error(\n \"[@modular-react/journeys] <JourneyOutlet> needs a runtime. Either pass `runtime` or mount a <JourneyProvider>.\",\n );\n }\n\n const instance = useInstanceSnapshot(runtime, instanceId);\n const internals = getInternals(runtime);\n const modules = modulesProp ?? internals.__moduleMap;\n const [retryKey, setRetryKey] = useState(0);\n\n // Abandon on unmount while still active or still loading. Two defenses:\n //\n // 1. StrictMode fires cleanup synchronously and then remounts the same\n // component — deferring the abandon one microtask and re-checking the\n // same `mountedRef` keeps the journey alive through that dance.\n //\n // 2. Two independent outlets rendering the same instance back-to-back\n // (unmount outlet A, mount outlet B) show up as `mountedRef.current\n // === false` because they are different component instances. To keep\n // outlet B's instance alive we also consult `record.listeners.size` —\n // if any subscriber is still attached, another outlet has taken over\n // and we skip the `end()`.\n const mountedRef = useRef(true);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n queueMicrotask(() => {\n if (mountedRef.current) return;\n const record = internals.__getRecord(instanceId);\n if (!record) return;\n if (record.status !== \"active\" && record.status !== \"loading\") return;\n if (record.listeners.size > 0) return;\n runtime.end(instanceId, { reason: \"unmounted\" });\n });\n };\n }, [runtime, instanceId, internals]);\n\n // Fire onFinished exactly once on terminal.\n const finishedFiredRef = useRef(false);\n useEffect(() => {\n if (!instance) return;\n if (instance.status !== \"completed\" && instance.status !== \"aborted\") return;\n if (finishedFiredRef.current) return;\n finishedFiredRef.current = true;\n onFinished?.({\n status: instance.status,\n payload: instance.terminalPayload,\n instanceId: instance.id,\n journeyId: instance.journeyId,\n });\n }, [instance, onFinished]);\n\n if (!instance) return null;\n if (instance.status === \"loading\") return loadingFallback ?? null;\n if (instance.status === \"completed\" || instance.status === \"aborted\") return null;\n\n const step = instance.step;\n if (!step) return null;\n\n const mod = modules[step.moduleId];\n const entry = mod?.entryPoints?.[step.entry];\n if (!mod || !entry) {\n const NotFound = notFoundComponent ?? DefaultNotFound;\n return createElement(NotFound, { moduleId: step.moduleId, entry: step.entry });\n }\n\n // Degrade gracefully if the record/registration was forgotten mid-render\n // (e.g. a sibling effect calling `runtime.forget`) — matches the existing\n // `if (!instance) return null` path above instead of throwing.\n const record = internals.__getRecord(instanceId);\n const reg = internals.__getRegistered(instance.journeyId);\n if (!record || !reg) return null;\n const { exit, goBack } = internals.__bindStepCallbacks(record, reg);\n\n const handleError = (err: unknown): void => {\n // Registration-level onError fires on every component throw — shell\n // telemetry observes the error even when the outlet decides to retry\n // or ignore. Route through the runtime so `fireOnError` stays the\n // single owner of hook firing (including its own try/catch around\n // throwing hooks); the outlet never reads `reg.options.onError`\n // directly.\n internals.__fireComponentError(instanceId, err, step);\n let policy = onStepError?.(err, { step }) ?? \"abort\";\n if (policy === \"retry\") {\n // The retry counter lives on the runtime record (not a ref) so it\n // survives a transition side-effect that advances stepToken mid-retry:\n // a step that throws during render and calls `exit()` in cleanup would\n // otherwise reset the budget on every hop.\n if (record.retryCount >= retryLimit) {\n policy = \"abort\";\n } else {\n record.retryCount += 1;\n }\n }\n if (policy === \"abort\") {\n runtime.end(instanceId, { reason: \"component-error\", error: err });\n return;\n }\n if (policy === \"retry\") {\n setRetryKey((k) => k + 1);\n }\n // 'ignore' — leave the boundary UI in place until the user navigates away\n };\n\n // The step's declared input/exit contract is erased at the module-map\n // boundary (the outlet holds ModuleDescriptor<any, any, any, any>).\n // Narrow to the structural shape every entry component satisfies —\n // `ModuleEntryProps<unknown, ExitPointMap>` — instead of `any`, so the\n // cast site at least documents the prop bag the outlet hands in.\n const StepComponent = entry.component as ComponentType<ModuleEntryProps<unknown, ExitPointMap>>;\n const stepKey = `${record.stepToken}:${retryKey}`;\n\n return createElement(\n StepErrorBoundary,\n {\n moduleId: step.moduleId,\n onError: handleError,\n errorComponent,\n key: stepKey,\n children: null,\n },\n createElement(StepComponent, {\n input: step.input,\n exit,\n goBack,\n }),\n );\n}\n\nfunction DefaultNotFound({ moduleId, entry }: JourneyOutletNotFoundProps): ReactNode {\n return createElement(\n \"div\",\n { style: { padding: \"1rem\", color: \"#c53030\" } },\n `Journey outlet: no entry \"${moduleId}.${entry}\" on the registered modules.`,\n );\n}\n\nfunction DefaultError({ moduleId, error }: JourneyOutletErrorProps): ReactNode {\n const message = error instanceof Error ? error.message : String(error);\n return createElement(\n \"div\",\n {\n style: {\n padding: \"1rem\",\n border: \"1px solid #e53e3e\",\n borderRadius: \"0.5rem\",\n margin: \"1rem\",\n },\n role: \"alert\",\n \"data-journey-step-error\": moduleId,\n },\n createElement(\n \"h3\",\n { style: { color: \"#e53e3e\", margin: \"0 0 0.5rem 0\" } },\n `Module \"${moduleId}\" encountered an error`,\n ),\n createElement(\n \"pre\",\n { style: { fontSize: \"0.875rem\", color: \"#718096\", whiteSpace: \"pre-wrap\" } },\n message,\n ),\n );\n}\n\nfunction useInstanceSnapshot(runtime: JourneyRuntime, instanceId: InstanceId) {\n const subscribe = useMemo(\n () => (listener: () => void) => runtime.subscribe(instanceId, listener),\n [runtime, instanceId],\n );\n const getSnapshot = () => runtime.getInstance(instanceId);\n const getServerSnapshot = getSnapshot;\n const instance = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n return instance;\n}\n\ninterface StepErrorBoundaryProps {\n readonly moduleId: string;\n readonly onError: (err: unknown) => void;\n readonly errorComponent?: ComponentType<JourneyOutletErrorProps>;\n readonly children: ReactNode;\n}\n\ninterface StepErrorBoundaryState {\n readonly error: unknown;\n}\n\nclass StepErrorBoundary extends Component<StepErrorBoundaryProps, StepErrorBoundaryState> {\n override state: StepErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: unknown): StepErrorBoundaryState {\n return { error };\n }\n\n override componentDidCatch(error: unknown) {\n this.props.onError(error);\n }\n\n override render(): ReactNode {\n if (this.state.error) {\n // Render the fallback inline. Wrapping an empty child in\n // `ModuleErrorBoundary` would not show anything: that boundary only\n // renders its fallback when *its own* child throws, and a null child\n // never does — so the outlet used to go blank after a step error.\n const ErrorFallback = this.props.errorComponent ?? DefaultError;\n return createElement(ErrorFallback, {\n moduleId: this.props.moduleId,\n error: this.state.error,\n });\n }\n return this.props.children;\n }\n}\n","import { createElement } from \"react\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport type { ExitPointMap, ModuleDescriptor, ModuleEntryProps } from \"@modular-react/core\";\nimport { ModuleErrorBoundary, useModuleExit, type ModuleExitEvent } from \"@modular-react/react\";\n\n/**\n * Exit event fired by a module rendered inside a `<ModuleTab>`.\n *\n * Alias for {@link ModuleExitEvent} from `@modular-react/react` — kept as a\n * named export for the workspace-tab entry point so existing imports keep\n * compiling. Both types have the same shape.\n */\nexport type ModuleTabExitEvent = ModuleExitEvent;\n\nexport interface ModuleTabProps<TInput = unknown> {\n /** Full module descriptor — the shell looks this up by id. */\n readonly module: ModuleDescriptor<any, any, any, any>;\n /**\n * Entry point name on the module. If omitted and the module exposes\n * exactly one entry, that entry is used automatically. If the module\n * exposes several entries, the name must be supplied — passing an\n * unknown name renders an error notice. If `entry` is omitted and the\n * module has no entry points, the component falls back to the legacy\n * `component` field; passing `entry` to such a module instead renders\n * the error notice so misconfiguration is surfaced.\n */\n readonly entry?: string;\n readonly input?: TInput;\n /** Opaque tab id threaded through to `onExit` for the shell to close it. */\n readonly tabId?: string;\n /**\n * Called when the module emits an exit. Runs *before* the provider's\n * global `onExit` dispatcher (via `<ModuleExitProvider>`, typically\n * composed under `<JourneyProvider>`), so the shell can close the tab\n * first and let the provider hook forward to analytics / routing.\n */\n readonly onExit?: (event: ModuleTabExitEvent) => void;\n}\n\n/**\n * Host for a single module instance rendered outside any route — in a tab,\n * modal, or panel. Default exit behavior delegates to the `onExit` callback\n * provided by the shell; the module itself stays journey-unaware.\n */\nexport function ModuleTab<TInput = unknown>(props: ModuleTabProps<TInput>): ReactNode {\n const { module: mod, entry, input, tabId, onExit } = props;\n\n const entryPoints = mod.entryPoints;\n const entryNames = entryPoints ? Object.keys(entryPoints) : [];\n let resolvedName: string | undefined = entry;\n let missingEntryNotice: string | null = null;\n if (entry === undefined) {\n if (entryNames.length === 1) {\n resolvedName = entryNames[0];\n } else if (entryNames.length > 1) {\n missingEntryNotice = `Module \"${mod.id}\" exposes multiple entries (${entryNames.join(\", \")}); pass the \\`entry\\` prop to disambiguate.`;\n }\n } else if (entryPoints && !(entry in entryPoints)) {\n missingEntryNotice = `Module \"${mod.id}\" has no entry \"${entry}\". Registered: ${entryNames.join(\", \") || \"(none)\"}.`;\n } else if (!entryPoints) {\n // `entry` requested but module exposes no entry points at all — surface\n // the misconfiguration instead of silently falling through to the legacy\n // `component` path.\n resolvedName = undefined;\n missingEntryNotice = `Module \"${mod.id}\" has no entry points; \\`entry=\"${entry}\"\\` cannot be resolved.`;\n }\n\n const entryPoint = resolvedName ? entryPoints?.[resolvedName] : undefined;\n\n // Hook order must stay stable across renders — always call useModuleExit,\n // even if resolvedName is missing. When missing, the returned `exit` is\n // never invoked because we render the error notice instead.\n const exit = useModuleExit(mod.id, resolvedName ?? \"\", {\n tabId,\n localOnExit: onExit,\n });\n\n let content: ReactNode;\n if (missingEntryNotice) {\n content = createElement(\n \"div\",\n { style: { padding: \"1rem\", color: \"#c53030\" } },\n missingEntryNotice,\n );\n } else if (entryPoint) {\n // The entry's declared input schema is the source of truth for whether\n // `input` is required. At runtime we don't reflect on the schema, but\n // we can still refuse to render with a visibly wrong `undefined` when\n // the caller forgot to pass it — that surfaces the misconfiguration\n // instead of letting the component throw deep inside its render with\n // `Cannot read properties of undefined`. Callers whose entry schema\n // is `void` should pass `input={undefined}` explicitly.\n if (input === undefined && !(\"input\" in props)) {\n content = createElement(\n \"div\",\n { style: { padding: \"1rem\", color: \"#c53030\" } },\n `Module \"${mod.id}\" entry \"${resolvedName ?? \"\"}\" was rendered without an \\`input\\` prop. ` +\n `Pass \\`input={undefined}\\` explicitly if the entry accepts no input.`,\n );\n } else {\n const Component = entryPoint.component as ComponentType<\n ModuleEntryProps<TInput, ExitPointMap>\n >;\n content = createElement(Component, { input: input as TInput, exit });\n }\n } else if (mod.component) {\n // Back-compat: render the legacy workspace component when the module\n // exposes no entry points. Entry contracts are opt-in.\n const Component = mod.component as ComponentType<{\n input?: unknown;\n tabId?: string;\n }>;\n content = createElement(Component, { input, tabId });\n } else {\n content = createElement(\n \"div\",\n { style: { padding: \"1rem\", color: \"#c53030\" } },\n `Module \"${mod.id}\" has no entry points and no component.`,\n );\n }\n\n return createElement(ModuleErrorBoundary, { moduleId: mod.id, children: content });\n}\n","import type { ComponentType, ReactNode } from \"react\";\nimport type {\n JourneyRuntime,\n ModuleTypeMap,\n NavigationItemBase,\n RegistryPlugin,\n} from \"@modular-react/core\";\nimport { createJourneyRuntime } from \"./runtime.js\";\nimport {\n JourneyValidationError,\n validateJourneyContracts,\n validateJourneyDefinition,\n} from \"./validation.js\";\nimport { JourneyProvider } from \"./provider.js\";\nimport type {\n AnyJourneyDefinition,\n JourneyDefinition,\n JourneyNavContribution,\n JourneyRegisterOptions,\n RegisteredJourney,\n} from \"./types.js\";\n\n/**\n * Methods the journeys plugin contributes to the registry. Registered\n * plugins type-intersect with the base `ModuleRegistry` so shells call\n * `registry.registerJourney(...)` with full type support.\n */\nexport interface JourneysPluginExtension {\n /**\n * Register a journey definition. The structural shape is validated\n * immediately (missing `id` / `version` / `transitions` etc.);\n * module-level contracts are validated at `resolveManifest()` /\n * `resolve()` time.\n *\n * `options.persistence` is typed against the journey's state, and\n * `options.nav.buildInput` is typed against the journey's input — pass a\n * typed definition and both are checked end-to-end.\n */\n registerJourney<TModules extends ModuleTypeMap, TState, TInput>(\n definition: JourneyDefinition<TModules, TState, TInput>,\n options?: JourneyRegisterOptions<TState, TInput>,\n ): void;\n}\n\n/**\n * Default shape the journeys plugin emits for each `nav`-carrying journey.\n * When {@link JourneysPluginOptions.buildNavItem} is provided, the plugin\n * hands this default (plus the journey's id and buildInput factory) to the\n * adapter so apps can reshape the item into their narrowed `TNavItem`.\n */\nexport interface JourneyDefaultNavItem extends NavigationItemBase {\n readonly label: string;\n /**\n * Always empty for a journey launcher — the dispatchable action lives in\n * {@link JourneyDefaultNavItem.action}, so there is no URL to follow. An\n * empty string keeps the structural `NavigationItemBase.to` satisfied\n * without suggesting the shell should treat this item as a link.\n */\n readonly to: \"\";\n readonly icon?: string | ComponentType<{ className?: string }>;\n readonly group?: string;\n readonly order?: number;\n readonly hidden?: boolean;\n readonly meta?: unknown;\n readonly action: {\n readonly kind: \"journey-start\";\n readonly journeyId: string;\n readonly buildInput?: (ctx?: unknown) => unknown;\n };\n}\n\n/**\n * Signature for the optional typed adapter that reshapes the plugin's\n * default nav item into the app's narrowed `TNavItem`. The adapter is\n * called once per `nav`-carrying journey at manifest time.\n */\nexport type JourneyNavItemBuilder<TNavItem extends NavigationItemBase> = (\n defaults: JourneyDefaultNavItem,\n raw: JourneyNavContribution<unknown> & { readonly journeyId: string },\n) => TNavItem;\n\nexport interface JourneysPluginOptions<\n TNavItem extends NavigationItemBase = JourneyDefaultNavItem,\n> {\n /**\n * Enable verbose transition / rollback logging in the runtime. Defaults to\n * `false`; plugins propagate the registry-level debug flag when set.\n */\n readonly debug?: boolean;\n /**\n * Forwarded onto `<JourneyProvider>` as the shell-wide `onModuleExit`\n * handler. Use it as a default place to close tabs / forward analytics\n * when a module exit isn't consumed by an explicit prop.\n */\n readonly onModuleExit?: (event: {\n readonly moduleId: string;\n readonly entry: string;\n readonly exit: string;\n readonly output: unknown;\n readonly tabId?: string;\n }) => void;\n /**\n * Optional adapter that reshapes the plugin's default nav item into the\n * app's narrowed `TNavItem`. Apps that use a typed `NavigationItem`\n * alias (typed label union, typed action union, typed meta bag) should\n * supply this so contributed items land in `manifest.navigation` with\n * the correct narrowed type. When omitted, the plugin emits items as\n * {@link JourneyDefaultNavItem} and the framework widens them to\n * `TNavItem` at the assembly boundary.\n */\n readonly buildNavItem?: JourneyNavItemBuilder<TNavItem>;\n}\n\n/**\n * Creates the journeys plugin. Pass to `registry.use(journeysPlugin())` to\n * enable journey registration and outlet rendering without the runtime\n * packages depending on `@modular-react/journeys` directly.\n *\n * The plugin:\n * - contributes `registerJourney(...)` onto the registry (type-safe)\n * - validates contracts against registered modules at resolve time\n * - produces a `JourneyRuntime` on `manifest.extensions.journeys` (also\n * surfaced as the `manifest.journeys` convenience alias)\n * - wraps the provider stack in `<JourneyProvider runtime={...} />`\n *\n * **Instantiate per registry.** The returned object closes over a\n * journey-registration list; passing the same instance to two\n * `createRegistry()` calls causes them to share that list. Call\n * `journeysPlugin()` once per registry.\n */\nexport function journeysPlugin<TNavItem extends NavigationItemBase = JourneyDefaultNavItem>(\n options: JourneysPluginOptions<TNavItem> = {},\n): RegistryPlugin<\"journeys\", JourneysPluginExtension, JourneyRuntime> {\n const registered: RegisteredJourney[] = [];\n\n return {\n name: \"journeys\",\n\n extend() {\n return {\n registerJourney<TModules extends ModuleTypeMap, TState, TInput>(\n definition: JourneyDefinition<TModules, TState, TInput>,\n regOpts?: JourneyRegisterOptions<TState, TInput>,\n ): void {\n const def = definition as AnyJourneyDefinition;\n const issues = validateJourneyDefinition(def);\n if (issues.length > 0) {\n throw new JourneyValidationError(issues);\n }\n registered.push({\n definition: def,\n options: regOpts as JourneyRegisterOptions | undefined,\n });\n },\n };\n },\n\n validate({ modules }) {\n if (registered.length > 0) {\n validateJourneyContracts(registered, modules);\n }\n },\n\n onResolve({ moduleDescriptors, debug }) {\n return createJourneyRuntime(registered, {\n modules: moduleDescriptors,\n debug: options.debug ?? debug,\n });\n },\n\n contributeNavigation() {\n const items: NavigationItemBase[] = [];\n for (const reg of registered) {\n const nav = reg.options?.nav;\n if (!nav) continue;\n const defaults: JourneyDefaultNavItem = {\n label: nav.label,\n to: \"\",\n ...(nav.icon !== undefined ? { icon: nav.icon } : {}),\n ...(nav.group !== undefined ? { group: nav.group } : {}),\n ...(nav.order !== undefined ? { order: nav.order } : {}),\n ...(nav.hidden !== undefined ? { hidden: nav.hidden } : {}),\n ...(nav.meta !== undefined ? { meta: nav.meta } : {}),\n action: {\n kind: \"journey-start\",\n journeyId: reg.definition.id,\n ...(nav.buildInput ? { buildInput: nav.buildInput } : {}),\n },\n };\n if (options.buildNavItem) {\n items.push(\n options.buildNavItem(defaults, {\n ...(nav as JourneyNavContribution<unknown>),\n journeyId: reg.definition.id,\n }),\n );\n } else {\n items.push(defaults);\n }\n }\n return items;\n },\n\n providers({ runtime }) {\n const BoundJourneyProvider: ComponentType<{ children: ReactNode }> = ({ children }) => (\n <JourneyProvider runtime={runtime} onModuleExit={options.onModuleExit}>\n {children}\n </JourneyProvider>\n );\n BoundJourneyProvider.displayName = \"JourneysPluginProvider\";\n return [BoundJourneyProvider];\n },\n };\n}\n","import type { JourneyHandleRef } from \"@modular-react/core\";\nimport type { JourneyDefinition, ModuleTypeMap } from \"./types.js\";\n\n/**\n * Lightweight token a journey exports so modules and shells can open it\n * with a typed `input` without pulling in the journey's runtime code.\n * Structurally identical to `JourneyHandleRef` in `@modular-react/core` —\n * re-exported here so authors have a single canonical name to import.\n *\n * The `__input` field is phantom: it never holds a value at runtime, it\n * only carries the input type for the `start(handle, input)` overload.\n */\nexport type JourneyHandle<TId extends string = string, TInput = unknown> = JourneyHandleRef<\n TId,\n TInput\n>;\n\n/**\n * Build a handle from a journey definition. Runtime identity is just\n * `{ id: def.id }`; the returned object is typed so callers get\n * `input`-checking through the `start` overload.\n */\nexport function defineJourneyHandle<TModules extends ModuleTypeMap, TState, TInput>(\n def: JourneyDefinition<TModules, TState, TInput>,\n): JourneyHandle<string, TInput> {\n return { id: def.id };\n}\n"],"mappings":"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"}
|