@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.d.ts
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import { AbandonCtx } from '@modular-react/core';
|
|
2
|
+
import { ComponentType } from 'react';
|
|
3
|
+
import { EntryInputOf } from '@modular-react/core';
|
|
4
|
+
import { EntryNamesOf } from '@modular-react/core';
|
|
5
|
+
import { EntryTransitions } from '@modular-react/core';
|
|
6
|
+
import { ExitCtx } from '@modular-react/core';
|
|
7
|
+
import { ExitNamesOf } from '@modular-react/core';
|
|
8
|
+
import { ExitOutputOf } from '@modular-react/core';
|
|
9
|
+
import { InstanceId } from '@modular-react/core';
|
|
10
|
+
import { JourneyDefinitionSummary } from '@modular-react/core';
|
|
11
|
+
import { JourneyHandleRef } from '@modular-react/core';
|
|
12
|
+
import { JourneyInstance } from '@modular-react/core';
|
|
13
|
+
import { JourneyPersistence } from '@modular-react/core';
|
|
14
|
+
import { JourneyRuntime } from '@modular-react/core';
|
|
15
|
+
import { JourneyStatus } from '@modular-react/core';
|
|
16
|
+
import { JourneyStep } from '@modular-react/core';
|
|
17
|
+
import { MaybePromise } from '@modular-react/core';
|
|
18
|
+
import { ModuleDescriptor } from '@modular-react/core';
|
|
19
|
+
import { ModuleExitEvent } from '@modular-react/react';
|
|
20
|
+
import { ModuleTypeMap } from '@modular-react/core';
|
|
21
|
+
import { NavigationItemBase } from '@modular-react/core';
|
|
22
|
+
import { ReactNode } from 'react';
|
|
23
|
+
import { RegistryPlugin } from '@modular-react/core';
|
|
24
|
+
import { SerializedJourney } from '@modular-react/core';
|
|
25
|
+
import { StepSpec } from '@modular-react/core';
|
|
26
|
+
import { TerminalCtx } from '@modular-react/core';
|
|
27
|
+
import { TerminalOutcome } from '@modular-react/core';
|
|
28
|
+
import { TransitionEvent as TransitionEvent_2 } from '@modular-react/core';
|
|
29
|
+
import { TransitionMap } from '@modular-react/core';
|
|
30
|
+
import { TransitionResult } from '@modular-react/core';
|
|
31
|
+
|
|
32
|
+
export { AbandonCtx }
|
|
33
|
+
|
|
34
|
+
/** Erased shape used by the registry — `any` on the generics lets the
|
|
35
|
+
* registry store definitions from different journeys side-by-side.
|
|
36
|
+
* Tightening to `unknown` breaks variance: `initialState: (input: TInput)
|
|
37
|
+
* => TState` for a specific journey is not assignable to
|
|
38
|
+
* `(input: unknown) => unknown` because function parameters are
|
|
39
|
+
* contravariant, so the registry would reject any concrete definition. */
|
|
40
|
+
export declare type AnyJourneyDefinition = JourneyDefinition<ModuleTypeMap, any, any>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a journey runtime bound to a set of registered journeys. The
|
|
44
|
+
* registry integration assembles this once at resolve time; the runtime is
|
|
45
|
+
* owned by the manifest and exposed as `manifest.journeys`.
|
|
46
|
+
*
|
|
47
|
+
* Passing an empty `registered` array yields a no-op runtime: every public
|
|
48
|
+
* method is safe to call, and `start()` will throw "unknown journey id" —
|
|
49
|
+
* matching the normal "not registered" failure mode and letting shells skip
|
|
50
|
+
* null-guards on `manifest.journeys`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createJourneyRuntime(registered: readonly RegisteredJourney[], options?: JourneyRuntimeOptions): JourneyRuntime;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Map-backed `JourneyPersistence` for tests and SSR. Gives tests a
|
|
56
|
+
* canonical isolated store (no bleed between cases, no `localStorage`
|
|
57
|
+
* mocking) and keeps the runtime's persistence code paths exercised.
|
|
58
|
+
*
|
|
59
|
+
* On SSR, it's a safe "persistence is configured but nothing survives
|
|
60
|
+
* the request" mode — every start mints a fresh instance, and save /
|
|
61
|
+
* remove are no-ops from the client's perspective.
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* const store = createMemoryPersistence<MyInput, MyState>({
|
|
65
|
+
* keyFor: ({ journeyId, input }) => `${journeyId}:${input.id}`,
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* const runtime = createJourneyRuntime(
|
|
69
|
+
* [{ definition: myJourney, options: { persistence: store } }],
|
|
70
|
+
* { modules },
|
|
71
|
+
* );
|
|
72
|
+
*
|
|
73
|
+
* // Tests can assert directly against the store:
|
|
74
|
+
* expect(store.size()).toBe(1);
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function createMemoryPersistence<TInput, TState>(options: MemoryPersistenceOptions<TInput, TState>): MemoryPersistence<TInput, TState>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `JourneyPersistence` backed by the Web Storage API
|
|
81
|
+
* (`localStorage` / `sessionStorage`). Covers the 80% case: a few KB of
|
|
82
|
+
* JSON per journey-per-customer, read on mount, written on every transition.
|
|
83
|
+
*
|
|
84
|
+
* SSR-safe — when `storage` resolves to `null` (server rendering, private
|
|
85
|
+
* modes where storage is disabled) all four methods no-op and `load`
|
|
86
|
+
* returns `null`, so the runtime mints a fresh instance as it would
|
|
87
|
+
* without persistence configured.
|
|
88
|
+
*
|
|
89
|
+
* Corrupt entries (invalid JSON) are removed lazily on `load` so a single
|
|
90
|
+
* bad write doesn't block future loads for the same key.
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* export const journeyPersistence = createWebStoragePersistence<
|
|
94
|
+
* OnboardingInput,
|
|
95
|
+
* OnboardingState
|
|
96
|
+
* >({
|
|
97
|
+
* keyFor: ({ journeyId, input }) =>
|
|
98
|
+
* `journey:${input.customerId}:${journeyId}`,
|
|
99
|
+
* });
|
|
100
|
+
*
|
|
101
|
+
* // Tab-scoped, cleared when the tab closes:
|
|
102
|
+
* const sessionScoped = createWebStoragePersistence<MyInput, MyState>({
|
|
103
|
+
* keyFor: ({ journeyId, input }) => `s:${input.id}:${journeyId}`,
|
|
104
|
+
* storage: typeof sessionStorage !== "undefined" ? sessionStorage : null,
|
|
105
|
+
* });
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* **Limits.** `localStorage` is synchronous and capped at ~5 MB per origin.
|
|
109
|
+
* Writes throw `QuotaExceededError` when full; the error bubbles so the
|
|
110
|
+
* app can surface it. If a journey holds large state or offline-first
|
|
111
|
+
* matters, write a custom adapter against IndexedDB.
|
|
112
|
+
*/
|
|
113
|
+
export declare function createWebStoragePersistence<TInput, TState>(options: WebStoragePersistenceOptions<TInput>): SyncJourneyPersistence<TState, TInput>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Declare a journey with full type inference on entry/exit contracts,
|
|
117
|
+
* transitions, and the journey's private state.
|
|
118
|
+
*
|
|
119
|
+
* **Why the empty parens?** TypeScript can't partially infer generics: if
|
|
120
|
+
* `defineJourney` took `<TModules, TState, TInput>` in a single call, you'd
|
|
121
|
+
* either have to spell all three — losing the ability to infer `TInput`
|
|
122
|
+
* from `initialState`'s parameter — or spell none, losing the ability to
|
|
123
|
+
* narrow `TModules` / `TState`. The two-call shape splits the generics
|
|
124
|
+
* so `TModules` + `TState` are explicit (first call) while `TInput` is
|
|
125
|
+
* inferred from the definition object (second call).
|
|
126
|
+
*
|
|
127
|
+
* ```ts
|
|
128
|
+
* defineJourney<OnboardingModules, OnboardingState>()({
|
|
129
|
+
* id: "customer-onboarding",
|
|
130
|
+
* version: "1.0.0",
|
|
131
|
+
* initialState: (input: { customerId: string }) => ({ ... }),
|
|
132
|
+
* // TInput is inferred as { customerId: string } here
|
|
133
|
+
* start: (state) => ({ module: "profile", entry: "review", input: { customerId: state.customerId } }),
|
|
134
|
+
* transitions: { ... },
|
|
135
|
+
* });
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* Zero runtime cost — the definition is returned unchanged.
|
|
139
|
+
*/
|
|
140
|
+
export declare const defineJourney: <TModules extends ModuleTypeMap, TState>() => <TInput = void>(definition: JourneyDefinition<TModules, TState, TInput>) => JourneyDefinition<TModules, TState, TInput>;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build a handle from a journey definition. Runtime identity is just
|
|
144
|
+
* `{ id: def.id }`; the returned object is typed so callers get
|
|
145
|
+
* `input`-checking through the `start` overload.
|
|
146
|
+
*/
|
|
147
|
+
export declare function defineJourneyHandle<TModules extends ModuleTypeMap, TState, TInput>(def: JourneyDefinition<TModules, TState, TInput>): JourneyHandle<string, TInput>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Identity helper that ties a persistence adapter's `keyFor` input to a
|
|
151
|
+
* journey's `TInput` so callers get compile-time checking on per-customer /
|
|
152
|
+
* per-session keys. Zero runtime cost — the adapter is returned as-is.
|
|
153
|
+
*
|
|
154
|
+
* The return type preserves both `TInput` and `TState`, so shells calling
|
|
155
|
+
* `persistence.keyFor({ input })` *outside* the runtime (e.g. to probe
|
|
156
|
+
* storage before opening a journey tab) still see the journey's typed
|
|
157
|
+
* input shape — no `input: unknown` erasure at the boundary.
|
|
158
|
+
*
|
|
159
|
+
* ```ts
|
|
160
|
+
* interface CustomerInput { customerId: string }
|
|
161
|
+
*
|
|
162
|
+
* const journeyPersistence = defineJourneyPersistence<CustomerInput, MyState>({
|
|
163
|
+
* keyFor: ({ input }) => `journey:${input.customerId}:onboarding`,
|
|
164
|
+
* load: (k) => backend.load(k),
|
|
165
|
+
* save: (k, b) => backend.save(k, b),
|
|
166
|
+
* remove: (k) => backend.remove(k),
|
|
167
|
+
* });
|
|
168
|
+
*
|
|
169
|
+
* // Outside the runtime — `input` is typed as CustomerInput:
|
|
170
|
+
* const key = journeyPersistence.keyFor({
|
|
171
|
+
* journeyId: "onboarding",
|
|
172
|
+
* input: { customerId: "C-1" },
|
|
173
|
+
* });
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export declare function defineJourneyPersistence<TInput, TState>(adapter: JourneyPersistence<TState, TInput>): JourneyPersistence<TState, TInput>;
|
|
177
|
+
|
|
178
|
+
export { EntryInputOf }
|
|
179
|
+
|
|
180
|
+
export { EntryNamesOf }
|
|
181
|
+
|
|
182
|
+
export { EntryTransitions }
|
|
183
|
+
|
|
184
|
+
export { ExitCtx }
|
|
185
|
+
|
|
186
|
+
export { ExitNamesOf }
|
|
187
|
+
|
|
188
|
+
export { ExitOutputOf }
|
|
189
|
+
|
|
190
|
+
export { InstanceId }
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Default shape the journeys plugin emits for each `nav`-carrying journey.
|
|
194
|
+
* When {@link JourneysPluginOptions.buildNavItem} is provided, the plugin
|
|
195
|
+
* hands this default (plus the journey's id and buildInput factory) to the
|
|
196
|
+
* adapter so apps can reshape the item into their narrowed `TNavItem`.
|
|
197
|
+
*/
|
|
198
|
+
export declare interface JourneyDefaultNavItem extends NavigationItemBase {
|
|
199
|
+
readonly label: string;
|
|
200
|
+
/**
|
|
201
|
+
* Always empty for a journey launcher — the dispatchable action lives in
|
|
202
|
+
* {@link JourneyDefaultNavItem.action}, so there is no URL to follow. An
|
|
203
|
+
* empty string keeps the structural `NavigationItemBase.to` satisfied
|
|
204
|
+
* without suggesting the shell should treat this item as a link.
|
|
205
|
+
*/
|
|
206
|
+
readonly to: "";
|
|
207
|
+
readonly icon?: string | ComponentType<{
|
|
208
|
+
className?: string;
|
|
209
|
+
}>;
|
|
210
|
+
readonly group?: string;
|
|
211
|
+
readonly order?: number;
|
|
212
|
+
readonly hidden?: boolean;
|
|
213
|
+
readonly meta?: unknown;
|
|
214
|
+
readonly action: {
|
|
215
|
+
readonly kind: "journey-start";
|
|
216
|
+
readonly journeyId: string;
|
|
217
|
+
readonly buildInput?: (ctx?: unknown) => unknown;
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export declare interface JourneyDefinition<TModules extends ModuleTypeMap, TState, TInput = void> {
|
|
222
|
+
readonly id: string;
|
|
223
|
+
readonly version: string;
|
|
224
|
+
readonly meta?: Readonly<Record<string, unknown>>;
|
|
225
|
+
readonly initialState: (input: TInput) => TState;
|
|
226
|
+
readonly start: (state: TState, input: TInput) => StepSpec<TModules>;
|
|
227
|
+
readonly transitions: TransitionMap<TModules, TState>;
|
|
228
|
+
readonly onTransition?: (ev: TransitionEvent_2<TModules, TState>) => void;
|
|
229
|
+
readonly onAbandon?: (ctx: AbandonCtx<TModules, TState>) => TransitionResult<TModules, TState>;
|
|
230
|
+
readonly onComplete?: (ctx: TerminalCtx<TState>, result: unknown) => void;
|
|
231
|
+
readonly onAbort?: (ctx: TerminalCtx<TState>, reason: unknown) => void;
|
|
232
|
+
readonly onHydrate?: (blob: SerializedJourney<TState>) => SerializedJourney<TState>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export { JourneyDefinitionSummary }
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Lightweight token a journey exports so modules and shells can open it
|
|
239
|
+
* with a typed `input` without pulling in the journey's runtime code.
|
|
240
|
+
* Structurally identical to `JourneyHandleRef` in `@modular-react/core` —
|
|
241
|
+
* re-exported here so authors have a single canonical name to import.
|
|
242
|
+
*
|
|
243
|
+
* The `__input` field is phantom: it never holds a value at runtime, it
|
|
244
|
+
* only carries the input type for the `start(handle, input)` overload.
|
|
245
|
+
*/
|
|
246
|
+
export declare type JourneyHandle<TId extends string = string, TInput = unknown> = JourneyHandleRef<TId, TInput>;
|
|
247
|
+
|
|
248
|
+
export declare class JourneyHydrationError extends Error {
|
|
249
|
+
constructor(message: string, options?: ErrorOptions);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export { JourneyInstance }
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Nav contribution attached to a `registerJourney` call. The journeys plugin
|
|
256
|
+
* collects these at manifest time and emits them as navigation items tagged
|
|
257
|
+
* with an `action.kind = "journey-start"` so the shell's navbar dispatcher
|
|
258
|
+
* can start the journey instead of following a URL.
|
|
259
|
+
*
|
|
260
|
+
* `TInput` is the journey's input type — `buildInput` produces that shape
|
|
261
|
+
* from whatever context the dispatcher passes at click time. Keeping the
|
|
262
|
+
* context loosely typed (`unknown`) matches how the journeys plugin surfaces
|
|
263
|
+
* contributions to the framework; apps that want to narrow the context can
|
|
264
|
+
* provide a typed `buildNavItem` adapter (see
|
|
265
|
+
* {@link JourneysPluginOptions}.`buildNavItem`).
|
|
266
|
+
*/
|
|
267
|
+
export declare interface JourneyNavContribution<TInput = unknown> {
|
|
268
|
+
/** Display label. Apps that type-narrow labels should reshape via `buildNavItem`. */
|
|
269
|
+
readonly label: string;
|
|
270
|
+
/** Icon — string identifier or React component (matches `NavigationItem.icon`). */
|
|
271
|
+
readonly icon?: string | React.ComponentType<{
|
|
272
|
+
className?: string;
|
|
273
|
+
}>;
|
|
274
|
+
/** Grouping key for the navbar, same semantics as `NavigationItem.group`. */
|
|
275
|
+
readonly group?: string;
|
|
276
|
+
/** Sort order within the group (lower wins). */
|
|
277
|
+
readonly order?: number;
|
|
278
|
+
/** If true, registered but hidden from default navbar rendering. */
|
|
279
|
+
readonly hidden?: boolean;
|
|
280
|
+
/** App-owned metadata, opaque to the library. */
|
|
281
|
+
readonly meta?: unknown;
|
|
282
|
+
/**
|
|
283
|
+
* Optional factory that builds the journey's `input` at click time. The
|
|
284
|
+
* shell's navbar dispatcher calls this with whatever nav context the host
|
|
285
|
+
* provides (workspace id, current selection, etc.) and hands the result
|
|
286
|
+
* back to `runtime.start(handle, input)`. Omit when the journey has no
|
|
287
|
+
* input; pass a pure factory when it does.
|
|
288
|
+
*/
|
|
289
|
+
readonly buildInput?: (ctx?: unknown) => TInput;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Signature for the optional typed adapter that reshapes the plugin's
|
|
294
|
+
* default nav item into the app's narrowed `TNavItem`. The adapter is
|
|
295
|
+
* called once per `nav`-carrying journey at manifest time.
|
|
296
|
+
*/
|
|
297
|
+
export declare type JourneyNavItemBuilder<TNavItem extends NavigationItemBase> = (defaults: JourneyDefaultNavItem, raw: JourneyNavContribution<unknown> & {
|
|
298
|
+
readonly journeyId: string;
|
|
299
|
+
}) => TNavItem;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Renders the current step of a journey instance. Host-agnostic — works in
|
|
303
|
+
* a tab, modal, route element, or plain `<div>`. On unmount while active,
|
|
304
|
+
* the instance is abandoned (deferred by a microtask so React 18/19
|
|
305
|
+
* StrictMode's simulated mount/unmount/mount cycle does not tear the
|
|
306
|
+
* instance down on its first visit).
|
|
307
|
+
*/
|
|
308
|
+
export declare function JourneyOutlet(props: JourneyOutletProps): ReactNode;
|
|
309
|
+
|
|
310
|
+
export declare interface JourneyOutletErrorProps {
|
|
311
|
+
readonly moduleId: string;
|
|
312
|
+
readonly error: unknown;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export declare interface JourneyOutletNotFoundProps {
|
|
316
|
+
readonly moduleId: string;
|
|
317
|
+
readonly entry: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export declare interface JourneyOutletProps {
|
|
321
|
+
/**
|
|
322
|
+
* Runtime to drive the outlet against. Optional when a `<JourneyProvider>`
|
|
323
|
+
* is mounted above — the outlet reads the runtime from context in that
|
|
324
|
+
* case. Explicit prop overrides context, so one outlet can reach a
|
|
325
|
+
* different runtime when needed.
|
|
326
|
+
*/
|
|
327
|
+
readonly runtime?: JourneyRuntime;
|
|
328
|
+
readonly instanceId: InstanceId;
|
|
329
|
+
/**
|
|
330
|
+
* Module descriptors the outlet resolves step components against.
|
|
331
|
+
* Optional — when omitted, the outlet pulls the descriptors the runtime
|
|
332
|
+
* was constructed with (the common case).
|
|
333
|
+
*/
|
|
334
|
+
readonly modules?: Readonly<Record<string, ModuleDescriptor<any, any, any, any>>>;
|
|
335
|
+
readonly loadingFallback?: ReactNode;
|
|
336
|
+
readonly onFinished?: (outcome: TerminalOutcome) => void;
|
|
337
|
+
readonly onStepError?: (err: unknown, ctx: {
|
|
338
|
+
step: JourneyStep;
|
|
339
|
+
}) => JourneyStepErrorPolicy;
|
|
340
|
+
/**
|
|
341
|
+
* Cap on `retry` responses before the outlet falls back to `abort`. The
|
|
342
|
+
* counter increments on every retry from `onStepError` and is never reset,
|
|
343
|
+
* so a step that causes a downstream step to also throw cannot bypass the
|
|
344
|
+
* cap by bumping the step token. Default: 2.
|
|
345
|
+
*/
|
|
346
|
+
readonly retryLimit?: number;
|
|
347
|
+
/**
|
|
348
|
+
* Rendered when the current step points at a module/entry that is not
|
|
349
|
+
* registered with the runtime. Defaults to a plain red notice.
|
|
350
|
+
*/
|
|
351
|
+
readonly notFoundComponent?: ComponentType<JourneyOutletNotFoundProps>;
|
|
352
|
+
/**
|
|
353
|
+
* Rendered when a step component throws. Defaults to a plain red notice
|
|
354
|
+
* with the error message. Receives the raw error so shells can route it
|
|
355
|
+
* through their own reporting.
|
|
356
|
+
*/
|
|
357
|
+
readonly errorComponent?: ComponentType<JourneyOutletErrorProps>;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export { JourneyPersistence }
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Provides the journey runtime to descendant `<JourneyOutlet>` nodes, and
|
|
364
|
+
* composes over `<ModuleExitProvider>` so module hosts (`<ModuleTab>`,
|
|
365
|
+
* `<ModuleRoute>`, anything using `useModuleExit`) see the shell's
|
|
366
|
+
* `onModuleExit` dispatcher without needing a second provider.
|
|
367
|
+
*
|
|
368
|
+
* Existing journey consumers do not need to change — `onModuleExit` keeps
|
|
369
|
+
* firing for every module exit emitted outside a journey step.
|
|
370
|
+
*/
|
|
371
|
+
export declare function JourneyProvider(props: JourneyProviderProps): ReactNode;
|
|
372
|
+
|
|
373
|
+
export declare interface JourneyProviderProps {
|
|
374
|
+
readonly runtime: JourneyRuntime;
|
|
375
|
+
readonly onModuleExit?: JourneyProviderValue["onModuleExit"];
|
|
376
|
+
readonly children: ReactNode;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Shell-level context read by `<JourneyOutlet>` so callers don't have to
|
|
381
|
+
* thread `runtime` through every container that hosts a journey.
|
|
382
|
+
*
|
|
383
|
+
* `onModuleExit` is still surfaced here for backward compatibility with
|
|
384
|
+
* consumers that introspect the provider value. The actual dispatch now
|
|
385
|
+
* flows through `<ModuleExitProvider>` from `@modular-react/react`, which
|
|
386
|
+
* `<JourneyProvider>` mounts automatically. Prefer consuming
|
|
387
|
+
* `useModuleExit` / `useModuleExitDispatcher` from the react package
|
|
388
|
+
* directly in new code.
|
|
389
|
+
*/
|
|
390
|
+
export declare interface JourneyProviderValue {
|
|
391
|
+
/** Journey runtime — usually `manifest.journeys`. */
|
|
392
|
+
readonly runtime: JourneyRuntime;
|
|
393
|
+
/**
|
|
394
|
+
* Optional fallback invoked by `<ModuleTab>` / `<ModuleRoute>` after any
|
|
395
|
+
* local `onExit` prop has run. Wiring this at the provider level gives a
|
|
396
|
+
* shell global telemetry / tab-close forwarding without threading the
|
|
397
|
+
* callback through every host.
|
|
398
|
+
*/
|
|
399
|
+
readonly onModuleExit?: (event: ModuleExitEvent) => void;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export declare interface JourneyRegisterOptions<TState = unknown, TInput = unknown> {
|
|
403
|
+
/**
|
|
404
|
+
* Fires after every transition. Registration-level hook runs after the
|
|
405
|
+
* definition-level `onTransition`. Useful for shell telemetry that
|
|
406
|
+
* doesn't belong in journey authoring code.
|
|
407
|
+
*/
|
|
408
|
+
onTransition?: (ev: TransitionEvent_2) => void;
|
|
409
|
+
/**
|
|
410
|
+
* Fires when the journey reaches a `{ complete }` transition. Runs after
|
|
411
|
+
* the definition-level `onComplete` (both fire). Use for shell-level
|
|
412
|
+
* completion analytics.
|
|
413
|
+
*/
|
|
414
|
+
onComplete?: (ctx: TerminalCtx<TState>, result: unknown) => void;
|
|
415
|
+
/**
|
|
416
|
+
* Fires when the journey aborts — either via a `{ abort }` transition, a
|
|
417
|
+
* thrown transition handler, or `runtime.end(id)`. Runs after the
|
|
418
|
+
* definition-level `onAbort`.
|
|
419
|
+
*/
|
|
420
|
+
onAbort?: (ctx: TerminalCtx<TState>, reason: unknown) => void;
|
|
421
|
+
/**
|
|
422
|
+
* Overrides the definition's `onAbandon` when `runtime.end(id)` is called
|
|
423
|
+
* on an active instance. When set, this handler supplies the transition
|
|
424
|
+
* result (typically `{ abort }`). When absent, the definition's handler
|
|
425
|
+
* is used. Use to swap out abandon behaviour for a specific deployment
|
|
426
|
+
* (e.g. a tab-close workflow that completes instead of aborting).
|
|
427
|
+
*/
|
|
428
|
+
onAbandon?: (ctx: AbandonCtx<ModuleTypeMap, TState>) => TransitionResult<ModuleTypeMap, TState>;
|
|
429
|
+
/**
|
|
430
|
+
* Layered on top of the definition-level `onHydrate` — runs **after** the
|
|
431
|
+
* definition transforms the blob. Useful for shell-level migration of
|
|
432
|
+
* fields that the journey author doesn't know about (e.g. redacting
|
|
433
|
+
* environment-specific identifiers on load).
|
|
434
|
+
*/
|
|
435
|
+
onHydrate?: (blob: SerializedJourney<TState>) => SerializedJourney<TState>;
|
|
436
|
+
/**
|
|
437
|
+
* Fires when a step component throws or a transition handler throws for
|
|
438
|
+
* an instance of this journey. Observation-only — the runtime still
|
|
439
|
+
* aborts / retries according to the outlet's `onStepError` policy.
|
|
440
|
+
*/
|
|
441
|
+
onError?: (err: unknown, ctx: {
|
|
442
|
+
step: JourneyStep | null;
|
|
443
|
+
}) => void;
|
|
444
|
+
/**
|
|
445
|
+
* Optional. Without it, journeys live in memory only — every
|
|
446
|
+
* `runtime.start()` mints a fresh instance and nothing is written to
|
|
447
|
+
* storage. Add an adapter when you want reload recovery or idempotent
|
|
448
|
+
* `start` (same input → same `instanceId`).
|
|
449
|
+
*
|
|
450
|
+
* Typed against both `TState` (for `load` / `save` payloads) and the
|
|
451
|
+
* journey's `TInput` (for `keyFor`) — pass a typed adapter built with
|
|
452
|
+
* {@link defineJourneyPersistence} to get end-to-end checking.
|
|
453
|
+
*/
|
|
454
|
+
persistence?: JourneyPersistence<TState, TInput>;
|
|
455
|
+
/**
|
|
456
|
+
* Maximum number of entries to keep in `history` (and the matching
|
|
457
|
+
* `rollbackSnapshots`). Oldest entries are dropped once the cap is
|
|
458
|
+
* exceeded. Omit — or pass `0` or a negative number — for unbounded
|
|
459
|
+
* history (the default).
|
|
460
|
+
*
|
|
461
|
+
* **Caveat with `allowBack`.** A cap smaller than the deepest
|
|
462
|
+
* back-enabled chain will silently break `goBack` past the trim
|
|
463
|
+
* point — the rollback snapshot that `goBack` would restore is among
|
|
464
|
+
* the dropped entries. Treat `maxHistory` as "prune old entries that
|
|
465
|
+
* no one will ever navigate back to" rather than a hard window on
|
|
466
|
+
* `goBack` distance. If an app needs both back-navigation and a tight
|
|
467
|
+
* cap, size the cap to at least the longest user-reachable back chain.
|
|
468
|
+
*/
|
|
469
|
+
maxHistory?: number;
|
|
470
|
+
/**
|
|
471
|
+
* Optional nav contribution. When set, the journeys plugin emits a
|
|
472
|
+
* navigation item for this journey so pure launchers don't need a
|
|
473
|
+
* shadow module to host them. The contributed item is tagged with
|
|
474
|
+
* `action: { kind: "journey-start", journeyId, buildInput }`; the
|
|
475
|
+
* shell's navbar dispatcher starts the journey on click.
|
|
476
|
+
*
|
|
477
|
+
* Typed against the journey's `TInput` so `buildInput` returns the
|
|
478
|
+
* right shape end-to-end. Apps with a narrowed `TNavItem` should also
|
|
479
|
+
* pass a `buildNavItem` adapter on `journeysPlugin` to reshape the
|
|
480
|
+
* default item into the app's narrowed type.
|
|
481
|
+
*/
|
|
482
|
+
nav?: JourneyNavContribution<TInput>;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export { JourneyRuntime }
|
|
486
|
+
|
|
487
|
+
export declare interface JourneyRuntimeOptions {
|
|
488
|
+
readonly debug?: boolean;
|
|
489
|
+
/**
|
|
490
|
+
* Module descriptors keyed by id — the runtime needs them to resolve
|
|
491
|
+
* `allowBack` mode ('preserve-state' | 'rollback' | false) at goBack time.
|
|
492
|
+
* When omitted, `goBack` falls back to 'preserve-state' for any journey
|
|
493
|
+
* transition that opts in via `allowBack: true`.
|
|
494
|
+
*/
|
|
495
|
+
readonly modules?: Readonly<Record<string, ModuleDescriptor<any, any, any, any>>>;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Creates the journeys plugin. Pass to `registry.use(journeysPlugin())` to
|
|
500
|
+
* enable journey registration and outlet rendering without the runtime
|
|
501
|
+
* packages depending on `@modular-react/journeys` directly.
|
|
502
|
+
*
|
|
503
|
+
* The plugin:
|
|
504
|
+
* - contributes `registerJourney(...)` onto the registry (type-safe)
|
|
505
|
+
* - validates contracts against registered modules at resolve time
|
|
506
|
+
* - produces a `JourneyRuntime` on `manifest.extensions.journeys` (also
|
|
507
|
+
* surfaced as the `manifest.journeys` convenience alias)
|
|
508
|
+
* - wraps the provider stack in `<JourneyProvider runtime={...} />`
|
|
509
|
+
*
|
|
510
|
+
* **Instantiate per registry.** The returned object closes over a
|
|
511
|
+
* journey-registration list; passing the same instance to two
|
|
512
|
+
* `createRegistry()` calls causes them to share that list. Call
|
|
513
|
+
* `journeysPlugin()` once per registry.
|
|
514
|
+
*/
|
|
515
|
+
export declare function journeysPlugin<TNavItem extends NavigationItemBase = JourneyDefaultNavItem>(options?: JourneysPluginOptions<TNavItem>): RegistryPlugin<"journeys", JourneysPluginExtension, JourneyRuntime>;
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Methods the journeys plugin contributes to the registry. Registered
|
|
519
|
+
* plugins type-intersect with the base `ModuleRegistry` so shells call
|
|
520
|
+
* `registry.registerJourney(...)` with full type support.
|
|
521
|
+
*/
|
|
522
|
+
export declare interface JourneysPluginExtension {
|
|
523
|
+
/**
|
|
524
|
+
* Register a journey definition. The structural shape is validated
|
|
525
|
+
* immediately (missing `id` / `version` / `transitions` etc.);
|
|
526
|
+
* module-level contracts are validated at `resolveManifest()` /
|
|
527
|
+
* `resolve()` time.
|
|
528
|
+
*
|
|
529
|
+
* `options.persistence` is typed against the journey's state, and
|
|
530
|
+
* `options.nav.buildInput` is typed against the journey's input — pass a
|
|
531
|
+
* typed definition and both are checked end-to-end.
|
|
532
|
+
*/
|
|
533
|
+
registerJourney<TModules extends ModuleTypeMap, TState, TInput>(definition: JourneyDefinition<TModules, TState, TInput>, options?: JourneyRegisterOptions<TState, TInput>): void;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export declare interface JourneysPluginOptions<TNavItem extends NavigationItemBase = JourneyDefaultNavItem> {
|
|
537
|
+
/**
|
|
538
|
+
* Enable verbose transition / rollback logging in the runtime. Defaults to
|
|
539
|
+
* `false`; plugins propagate the registry-level debug flag when set.
|
|
540
|
+
*/
|
|
541
|
+
readonly debug?: boolean;
|
|
542
|
+
/**
|
|
543
|
+
* Forwarded onto `<JourneyProvider>` as the shell-wide `onModuleExit`
|
|
544
|
+
* handler. Use it as a default place to close tabs / forward analytics
|
|
545
|
+
* when a module exit isn't consumed by an explicit prop.
|
|
546
|
+
*/
|
|
547
|
+
readonly onModuleExit?: (event: {
|
|
548
|
+
readonly moduleId: string;
|
|
549
|
+
readonly entry: string;
|
|
550
|
+
readonly exit: string;
|
|
551
|
+
readonly output: unknown;
|
|
552
|
+
readonly tabId?: string;
|
|
553
|
+
}) => void;
|
|
554
|
+
/**
|
|
555
|
+
* Optional adapter that reshapes the plugin's default nav item into the
|
|
556
|
+
* app's narrowed `TNavItem`. Apps that use a typed `NavigationItem`
|
|
557
|
+
* alias (typed label union, typed action union, typed meta bag) should
|
|
558
|
+
* supply this so contributed items land in `manifest.navigation` with
|
|
559
|
+
* the correct narrowed type. When omitted, the plugin emits items as
|
|
560
|
+
* {@link JourneyDefaultNavItem} and the framework widens them to
|
|
561
|
+
* `TNavItem` at the assembly boundary.
|
|
562
|
+
*/
|
|
563
|
+
readonly buildNavItem?: JourneyNavItemBuilder<TNavItem>;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export { JourneyStatus }
|
|
567
|
+
|
|
568
|
+
export { JourneyStep }
|
|
569
|
+
|
|
570
|
+
export declare type JourneyStepErrorPolicy = "abort" | "retry" | "ignore";
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Aggregated error thrown when one or more registered journeys reference
|
|
574
|
+
* module ids, entry names, or exit names that do not exist (or that
|
|
575
|
+
* disagree on `allowBack`). Mirrors the style of core's
|
|
576
|
+
* `validateDependencies` — accumulate all issues, throw once.
|
|
577
|
+
*/
|
|
578
|
+
export declare class JourneyValidationError extends Error {
|
|
579
|
+
readonly issues: readonly string[];
|
|
580
|
+
constructor(issues: readonly string[]);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export { MaybePromise }
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* `SyncJourneyPersistence` augmented with test-only inspection helpers
|
|
587
|
+
* (`size`, `entries`, `clear`). Methods are sync (no `MaybePromise`) so
|
|
588
|
+
* tests can `expect(store.load(k)).toEqual(blob)` without awaiting, and
|
|
589
|
+
* the value is still assignable to `JourneyPersistence<TState, TInput>`
|
|
590
|
+
* for `registerJourney({ persistence })`.
|
|
591
|
+
*/
|
|
592
|
+
export declare interface MemoryPersistence<TInput, TState> extends SyncJourneyPersistence<TState, TInput> {
|
|
593
|
+
/** Number of entries currently stored. */
|
|
594
|
+
readonly size: () => number;
|
|
595
|
+
/** Snapshot of all `[key, blob]` pairs. Each blob is cloned if cloning is enabled. */
|
|
596
|
+
readonly entries: () => ReadonlyArray<readonly [string, SerializedJourney<TState>]>;
|
|
597
|
+
/** Drop all entries. */
|
|
598
|
+
readonly clear: () => void;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export declare interface MemoryPersistenceOptions<TInput, TState> {
|
|
602
|
+
/** Same contract as `WebStoragePersistenceOptions.keyFor`. */
|
|
603
|
+
readonly keyFor: (ctx: {
|
|
604
|
+
journeyId: string;
|
|
605
|
+
input: TInput;
|
|
606
|
+
}) => string;
|
|
607
|
+
/**
|
|
608
|
+
* Optional seed entries. Handy for tests that want the runtime to find a
|
|
609
|
+
* pre-persisted journey on first `start()` without walking through the
|
|
610
|
+
* flow to produce the blob.
|
|
611
|
+
*/
|
|
612
|
+
readonly initial?: Iterable<readonly [string, SerializedJourney<TState>]>;
|
|
613
|
+
/**
|
|
614
|
+
* When true (default), stored blobs are deep-cloned on both `save` and
|
|
615
|
+
* `load` so callers mutating the returned object can't corrupt the
|
|
616
|
+
* backing store. Set to `false` to skip the clone in hot test loops
|
|
617
|
+
* where you've verified nobody mutates the blob.
|
|
618
|
+
*/
|
|
619
|
+
readonly clone?: boolean;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Host for a single module instance rendered outside any route — in a tab,
|
|
624
|
+
* modal, or panel. Default exit behavior delegates to the `onExit` callback
|
|
625
|
+
* provided by the shell; the module itself stays journey-unaware.
|
|
626
|
+
*/
|
|
627
|
+
export declare function ModuleTab<TInput = unknown>(props: ModuleTabProps<TInput>): ReactNode;
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Exit event fired by a module rendered inside a `<ModuleTab>`.
|
|
631
|
+
*
|
|
632
|
+
* Alias for {@link ModuleExitEvent} from `@modular-react/react` — kept as a
|
|
633
|
+
* named export for the workspace-tab entry point so existing imports keep
|
|
634
|
+
* compiling. Both types have the same shape.
|
|
635
|
+
*/
|
|
636
|
+
export declare type ModuleTabExitEvent = ModuleExitEvent;
|
|
637
|
+
|
|
638
|
+
export declare interface ModuleTabProps<TInput = unknown> {
|
|
639
|
+
/** Full module descriptor — the shell looks this up by id. */
|
|
640
|
+
readonly module: ModuleDescriptor<any, any, any, any>;
|
|
641
|
+
/**
|
|
642
|
+
* Entry point name on the module. If omitted and the module exposes
|
|
643
|
+
* exactly one entry, that entry is used automatically. If the module
|
|
644
|
+
* exposes several entries, the name must be supplied — passing an
|
|
645
|
+
* unknown name renders an error notice. If `entry` is omitted and the
|
|
646
|
+
* module has no entry points, the component falls back to the legacy
|
|
647
|
+
* `component` field; passing `entry` to such a module instead renders
|
|
648
|
+
* the error notice so misconfiguration is surfaced.
|
|
649
|
+
*/
|
|
650
|
+
readonly entry?: string;
|
|
651
|
+
readonly input?: TInput;
|
|
652
|
+
/** Opaque tab id threaded through to `onExit` for the shell to close it. */
|
|
653
|
+
readonly tabId?: string;
|
|
654
|
+
/**
|
|
655
|
+
* Called when the module emits an exit. Runs *before* the provider's
|
|
656
|
+
* global `onExit` dispatcher (via `<ModuleExitProvider>`, typically
|
|
657
|
+
* composed under `<JourneyProvider>`), so the shell can close the tab
|
|
658
|
+
* first and let the provider hook forward to analytics / routing.
|
|
659
|
+
*/
|
|
660
|
+
readonly onExit?: (event: ModuleTabExitEvent) => void;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export { ModuleTypeMap }
|
|
664
|
+
|
|
665
|
+
/** Internal registration record — definition + options kept together. */
|
|
666
|
+
export declare interface RegisteredJourney<TState = unknown, TInput = unknown> {
|
|
667
|
+
readonly definition: AnyJourneyDefinition;
|
|
668
|
+
readonly options: JourneyRegisterOptions<TState, TInput> | undefined;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export { SerializedJourney }
|
|
672
|
+
|
|
673
|
+
export { StepSpec }
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Narrowed variant of {@link JourneyPersistence} whose methods are
|
|
677
|
+
* guaranteed synchronous — `load` returns `SerializedJourney<TState> | null`
|
|
678
|
+
* (not `MaybePromise<…>`), `save`/`remove` return `void` (not
|
|
679
|
+
* `MaybePromise<void>`). Stock adapters return this shape so direct
|
|
680
|
+
* `.load(key)` callers don't need to discriminate sync vs async or cast
|
|
681
|
+
* away the promise half of the union.
|
|
682
|
+
*
|
|
683
|
+
* Structurally assignable to `JourneyPersistence<TState, TInput>`, so the
|
|
684
|
+
* value can still be passed to `registerJourney({ persistence })` without
|
|
685
|
+
* widening.
|
|
686
|
+
*/
|
|
687
|
+
export declare interface SyncJourneyPersistence<TState, TInput = unknown> {
|
|
688
|
+
readonly keyFor: (ctx: {
|
|
689
|
+
journeyId: string;
|
|
690
|
+
input: TInput;
|
|
691
|
+
}) => string;
|
|
692
|
+
readonly load: (key: string) => SerializedJourney<TState> | null;
|
|
693
|
+
readonly save: (key: string, blob: SerializedJourney<TState>) => void;
|
|
694
|
+
readonly remove: (key: string) => void;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export { TerminalCtx }
|
|
698
|
+
|
|
699
|
+
export { TerminalOutcome }
|
|
700
|
+
|
|
701
|
+
export { TransitionEvent_2 as TransitionEvent }
|
|
702
|
+
|
|
703
|
+
export { TransitionMap }
|
|
704
|
+
|
|
705
|
+
export { TransitionResult }
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Thrown when `runtime.start()` / `runtime.hydrate()` is called with a
|
|
709
|
+
* journey id that is not registered. Distinct class so shells can
|
|
710
|
+
* discriminate "this journey is gone after an upgrade, drop the tab"
|
|
711
|
+
* from transient or validation failures.
|
|
712
|
+
*/
|
|
713
|
+
export declare class UnknownJourneyError extends Error {
|
|
714
|
+
readonly journeyId: string;
|
|
715
|
+
constructor(journeyId: string, registered: readonly string[]);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Read the current provider value, or `null` when none is mounted. */
|
|
719
|
+
export declare function useJourneyContext(): JourneyProviderValue | null;
|
|
720
|
+
|
|
721
|
+
export declare function validateJourneyContracts(journeys: readonly RegisteredJourney[], modules: readonly ModuleDescriptor<any, any, any, any>[]): void;
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Shallow sanity check on a journey definition's own shape. Use this for
|
|
725
|
+
* authoring ergonomics; structural contract checks live in
|
|
726
|
+
* {@link validateJourneyContracts}.
|
|
727
|
+
*/
|
|
728
|
+
export declare function validateJourneyDefinition(def: AnyJourneyDefinition): readonly string[];
|
|
729
|
+
|
|
730
|
+
export declare interface WebStoragePersistenceOptions<TInput> {
|
|
731
|
+
/**
|
|
732
|
+
* Compute the persistence key from the journey id and starting input.
|
|
733
|
+
* Must be deterministic — `runtime.start()` probes this key to find an
|
|
734
|
+
* existing instance and achieve idempotency.
|
|
735
|
+
*/
|
|
736
|
+
readonly keyFor: (ctx: {
|
|
737
|
+
journeyId: string;
|
|
738
|
+
input: TInput;
|
|
739
|
+
}) => string;
|
|
740
|
+
/**
|
|
741
|
+
* The `Storage` instance to read from and write to. Accepts either a
|
|
742
|
+
* direct reference or a lazy getter; the getter is invoked on every
|
|
743
|
+
* call, which keeps SSR safe (the default returns `null` when
|
|
744
|
+
* `localStorage` is not defined on the global).
|
|
745
|
+
*
|
|
746
|
+
* Defaults to `globalThis.localStorage` (or `null` under SSR) when
|
|
747
|
+
* omitted or explicitly `undefined`. Pass `sessionStorage` for
|
|
748
|
+
* tab-scoped persistence, `null` to force the SSR no-op path, or any
|
|
749
|
+
* `Storage`-shaped stub for custom backends.
|
|
750
|
+
*/
|
|
751
|
+
readonly storage?: Storage | null | (() => Storage | null);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export { }
|