@modular-react/journeys 0.1.0 → 1.0.1

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/dist/index.js CHANGED
Binary file
package/dist/index.js.map CHANGED
@@ -1 +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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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/define-journey.ts","../src/persistence.ts","../src/provider.tsx","../src/define-transition.ts","../src/outlet.tsx","../src/module-tab.tsx","../src/plugin.tsx","../src/handle.ts","../src/select-module.ts"],"sourcesContent":["import type { CatalogMeta } from \"@modular-react/core\";\nimport 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, TOutput>` in a single\n * call, you'd either have to spell all four — losing the ability to infer\n * `TInput` from `initialState`'s parameter — or spell none, losing the\n * ability to narrow `TModules` / `TState`. The two-call shape splits the\n * generics so `TModules` + `TState` (+ optional `TOutput`) are explicit\n * (first call) while `TInput` is inferred from the definition object\n * (second call).\n *\n * ```ts\n * defineJourney<OnboardingModules, OnboardingState>()({ ... });\n * defineJourney<OnboardingModules, OnboardingState, { token: string }>()({\n * id: \"customer-onboarding\",\n * version: \"1.0.0\",\n * initialState: (input: { customerId: string }) => ({ ... }),\n * start: (state) => ({ module: \"profile\", entry: \"review\", input: { customerId: state.customerId } }),\n * transitions: {\n * billing: {\n * collect: {\n * done: ({ output }) => ({ complete: { token: output.token } }),\n * // ^ checked against { token: string }\n * },\n * },\n * },\n * });\n * ```\n *\n * Zero runtime cost — the definition is returned unchanged.\n */\nexport const defineJourney =\n // `TOutput = unknown` keeps existing two-generic call sites compiling —\n // `complete: { ...arbitrary }` assigns to `unknown`. New journeys that\n // want their terminal payload type-checked (and surfaced to a parent's\n // resume handler) explicitly pass it as the third generic.\n <\n TModules extends ModuleTypeMap,\n TState,\n TOutput = unknown,\n TMeta extends { [K in keyof TMeta]: unknown } = Record<string, unknown>,\n >() =>\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>(\n definition: JourneyDefinition<TModules, TState, TInput, TOutput, CatalogMeta & TMeta>,\n ) =>\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 type {\n EntryInputOf,\n EntryNamesOf,\n ExitCtx,\n InvokeSpec,\n ModuleTypeMap,\n TransitionResult,\n} from \"@modular-react/core\";\n\n/**\n * Sentinel value declaring a non-`next` outcome on a wrapped transition\n * handler. Mixed into the `targets:` array alongside `{ module, entry }`\n * step refs so a single declaration captures every branch the handler\n * may take.\n *\n * - `\"complete\"` — handler may return `{ complete: ... }` (terminates).\n * - `\"abort\"` — handler may return `{ abort: ... }` (terminates with abort).\n * - `\"invoke\"` — handler may return `{ invoke: { handle, input, resume } }`\n * (suspends the parent, runs a child journey). The specific handle is\n * not type-narrowed here — the journey definition's `invokes` field\n * remains the closed-set declaration the runtime cycle / undeclared-\n * child guards check against.\n */\nexport type TerminalSentinel = \"complete\" | \"abort\" | \"invoke\";\n\n/**\n * Reference to one possible outcome of a transition handler. Used by\n * {@link defineTransition} to declare every branch the handler may take;\n * the host's auto-preloader reads the `{ module, entry }` entries to warm\n * chunks for next-step candidates from the current step, and the catalog\n * harvester reads the sentinels to derive `aborts` / `completes` flags\n * without an AST walk over the handler body.\n *\n * The `{ module, entry }` shape mirrors the `next:` field handlers return —\n * a step ref is just `StepSpec` without the runtime-computed `input` —\n * so authors don't flip between two notations for the same idea.\n *\n * When `TModules` is bound (via the curried `defineTransition<TModules>()`\n * binder), `module` narrows to `keyof TModules` and `entry` narrows to that\n * module's `entryPoints` keys.\n */\nexport type StepRef<TModules extends ModuleTypeMap> =\n | {\n [M in keyof TModules & string]: {\n [E in EntryNamesOf<TModules[M]> & string]: {\n readonly module: M;\n readonly entry: E;\n };\n }[EntryNamesOf<TModules[M]> & string];\n }[keyof TModules & string]\n | TerminalSentinel;\n\n/**\n * Type predicate that splits a `StepRef` into its step-ref vs sentinel arms.\n * Object refs land in the `next:` arm; sentinels gate the terminal arms.\n */\ntype StepObjectRef = { readonly module: string; readonly entry: string };\n\n/**\n * Build the `next.{ module, entry, input }` shape for one declared step ref.\n * Distributes over a union of refs so multiple targets produce a union of\n * step specs under a single `next:` key (rather than separate `{ next: A }`\n * vs `{ next: B }` arms — the latter would reject conditional handler\n * returns like `next: cond ? planRef : billingRef`).\n */\ntype StepSpecFromRef<TModules extends ModuleTypeMap, TRef> = TRef extends {\n readonly module: infer M;\n readonly entry: infer E;\n}\n ? M extends keyof TModules & string\n ? E extends EntryNamesOf<TModules[M]> & string\n ? {\n readonly module: M;\n readonly entry: E;\n readonly input: EntryInputOf<TModules[M], E>;\n }\n : never\n : never\n : never;\n\n/**\n * Narrow the handler return type to only the arms whose targets are declared.\n * - `{ module, entry }` in targets → `next:` arm allowed (with `input` typed\n * against the chosen entry). Multiple refs collapse into one `next:` key\n * whose value is the union of step specs.\n * - `\"complete\"` in targets → `complete:` arm allowed.\n * - `\"abort\"` in targets → `abort:` arm allowed.\n * - `\"invoke\"` in targets → `invoke:` arm allowed.\n *\n * Declaring an arm in targets but never returning it is fine (over-declaring\n * is conservative for preload). Returning an arm that wasn't declared is a\n * compile error — the wrapped handler can't drift past the declaration.\n */\ntype NarrowedTransitionResult<\n TModules extends ModuleTypeMap,\n TState,\n TOutput,\n TTargets extends readonly StepRef<TModules>[],\n> =\n | (Extract<TTargets[number], StepObjectRef> extends never\n ? never\n : {\n readonly next: StepSpecFromRef<TModules, Extract<TTargets[number], StepObjectRef>>;\n readonly state?: TState;\n })\n | (Extract<TTargets[number], \"complete\"> extends never\n ? never\n : { readonly complete: TOutput; readonly state?: TState })\n | (Extract<TTargets[number], \"abort\"> extends never\n ? never\n : { readonly abort: unknown; readonly state?: TState })\n | (Extract<TTargets[number], \"invoke\"> extends never\n ? never\n : { readonly invoke: InvokeSpec<unknown, unknown>; readonly state?: TState });\n\n/**\n * A transition handler with declared `targets` metadata attached. Functionally\n * identical to the bare handler the journey runtime expects — the call\n * signature is preserved verbatim, so the value drops directly into a\n * `transitions[mod][entry][exit]` slot. The host's preloader walks\n * `Object.values(perEntry)` and reads each handler's `.targets` to schedule\n * speculative imports.\n */\nexport type AnnotatedTransitionHandler<\n THandler extends (ctx: any) => any,\n TTargets extends readonly (StepObjectRef | TerminalSentinel)[],\n> = THandler & { readonly targets: TTargets };\n\ninterface DefineTransitionSpec<\n THandler extends (ctx: any) => any,\n TTargets extends readonly (StepObjectRef | TerminalSentinel)[],\n> {\n readonly targets: TTargets;\n readonly handle: THandler;\n}\n\nfunction attach<\n THandler extends (ctx: any) => any,\n TTargets extends readonly (StepObjectRef | TerminalSentinel)[],\n>(spec: DefineTransitionSpec<THandler, TTargets>): AnnotatedTransitionHandler<THandler, TTargets> {\n const handler = spec.handle as AnnotatedTransitionHandler<THandler, TTargets>;\n // Reusing the same function reference across two `defineTransition` calls\n // would crash on the second `Object.defineProperty` with a cryptic\n // `TypeError: Cannot redefine property: targets` (we set the property\n // non-configurable so frozen targets can't be silently replaced). Detect\n // the reuse explicitly and surface an actionable message instead.\n if (Object.getOwnPropertyDescriptor(handler, \"targets\") !== undefined) {\n throw new TypeError(\n \"[@modular-react/journeys] defineTransition: the same handler function was passed to defineTransition twice. \" +\n \"Each transition needs its own handler — pass an inline arrow / function literal per `defineTransition({ ... })` call.\",\n );\n }\n // Non-enumerable so structural iteration (Object.entries on the transitions\n // map, JSON serialization of journey snapshots) does not surface this field;\n // the preloader reads it via direct property access.\n Object.defineProperty(handler, \"targets\", {\n value: Object.freeze(\n spec.targets.map((t) => (typeof t === \"string\" ? t : Object.freeze({ ...t }))),\n ) as TTargets,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n return handler;\n}\n\n/**\n * Curried binder used by {@link defineTransition} to thread the journey's\n * `TModules` / `TState` / `TOutput` into the handler's contextual return\n * type — `next.module` / `next.entry` and the choice of arms (`next` vs\n * `complete` vs `abort` vs `invoke`) check against the bound generics\n * instead of widening to plain `string` / accepting any arm.\n */\nexport interface TypedTransitionBinder<TModules extends ModuleTypeMap, TState, TOutput> {\n <\n const TTargets extends readonly StepRef<TModules>[],\n TEntryInput = unknown,\n TExitOutput = unknown,\n >(spec: {\n readonly targets: TTargets;\n readonly handle: (\n ctx: ExitCtx<TState, TExitOutput, TEntryInput>,\n ) => NarrowedTransitionResult<TModules, TState, TOutput, TTargets>;\n }): AnnotatedTransitionHandler<\n (ctx: ExitCtx<TState, TExitOutput, TEntryInput>) => TransitionResult<TModules, TState, TOutput>,\n TTargets\n >;\n}\n\n/**\n * Wrap a transition handler with a static declaration of every outcome it may\n * take. Two effects:\n *\n * 1. **Runtime — preload precision.** `<JourneyOutlet>`'s default\n * `preload=\"precise\"` mode reads `targets` and warms exactly those\n * entries' chunks during idle time, so navigating Next finds the\n * chunk already cached. Bare-function handlers contribute nothing\n * to precise mode (they fall back to `preload=\"aggressive\"` if set).\n * Sentinel targets (`\"complete\"`, `\"abort\"`, `\"invoke\"`) carry no\n * chunk to preload — they are skipped.\n *\n * 2. **Type-level — the handler's return is constrained to the declared\n * arms.** Declaring `targets: [{ module: \"plan\", entry: \"choose\" }]`\n * means the handler may only return `{ next: ... }`; declaring\n * `targets: [\"abort\"]` means only `{ abort: ... }`; mixing both\n * allows either. Returning an undeclared arm is a compile error.\n *\n * **`targets` is mandatory.** A wrapped handler must enumerate every\n * outcome it may take. If you don't want a declaration, use a bare\n * function — the runtime invocation path is identical, and bare handlers\n * sit out of precise-mode preload.\n *\n * Two call shapes:\n *\n * ```ts\n * // Curried (recommended): bind the journey's generics once, get full\n * // contextual typing on every wrapped handler. Naming convention mirrors\n * // `selectModule` (a descriptive verb for the binder, not an\n * // abbreviation — `tx` reads as \"transaction\" in most codebases).\n * const transition = defineTransition<OnboardingModules, OnboardingState>();\n *\n * profileComplete: transition({\n * targets: [{ module: \"plan\", entry: \"choose\" }],\n * handle: ({ output, state }) => ({\n * state: { ...state, hint: output.hint },\n * next: { module: \"plan\", entry: \"choose\", input: ... },\n * }),\n * }),\n *\n * // Mix step refs with sentinels for handlers that branch between\n * // next and a terminal arm:\n * checkout: transition({\n * targets: [{ module: \"plan\", entry: \"choose\" }, \"abort\"],\n * handle: ({ output }) =>\n * output.kind === \"ok\"\n * ? { next: { module: \"plan\", entry: \"choose\", input: ... } }\n * : { abort: { reason: \"user-cancelled\" } },\n * }),\n *\n * // Bare: zero-config, no contextual narrowing. Targets accept any\n * // `{ module: string; entry: string } | \"complete\" | \"abort\" | \"invoke\"`;\n * // useful for one-off handlers or for journeys whose return literals\n * // are already typed by an outer annotation.\n * cancelled: defineTransition({\n * targets: [\"abort\"],\n * handle: () => ({ abort: { reason: \"user-cancelled\" } }),\n * }),\n * ```\n */\nexport function defineTransition<\n TModules extends ModuleTypeMap,\n TState = unknown,\n TOutput = unknown,\n>(): TypedTransitionBinder<TModules, TState, TOutput>;\nexport function defineTransition<\n THandler extends (ctx: any) => any,\n const TTargets extends readonly (StepObjectRef | TerminalSentinel)[],\n>(spec: DefineTransitionSpec<THandler, TTargets>): AnnotatedTransitionHandler<THandler, TTargets>;\nexport function defineTransition(specOrNothing?: DefineTransitionSpec<any, any>): unknown {\n if (specOrNothing === undefined) {\n // Curried form — return the binder. The binder reuses `attach` so the\n // metadata-stamping logic stays in one place.\n return ((spec: DefineTransitionSpec<any, any>) => attach(spec)) as TypedTransitionBinder<\n ModuleTypeMap,\n unknown,\n unknown\n >;\n }\n return attach(specOrNothing);\n}\n\n/**\n * Narrow a value to the annotated form. Used by the auto-preloader to read\n * `targets` from a handler without trusting structural lookups on `unknown`.\n * Each target must be either a `{ module, entry }` string-pair or one of\n * the recognized sentinel strings.\n */\nexport function isAnnotatedTransition(\n value: unknown,\n): value is AnnotatedTransitionHandler<\n (ctx: any) => any,\n readonly (StepObjectRef | TerminalSentinel)[]\n> {\n if (typeof value !== \"function\") return false;\n const targets = (value as { targets?: unknown }).targets;\n if (!Array.isArray(targets)) return false;\n return targets.every(\n (t) =>\n isTerminalSentinel(t) ||\n (typeof t === \"object\" &&\n t !== null &&\n typeof (t as { module?: unknown }).module === \"string\" &&\n typeof (t as { entry?: unknown }).entry === \"string\"),\n );\n}\n\nconst TERMINAL_SENTINELS = new Set<TerminalSentinel>([\"complete\", \"abort\", \"invoke\"]);\n\n/**\n * Narrow a value to one of the recognized {@link TerminalSentinel} strings.\n * Exposed for hosts that introspect a wrapped handler's targets and want to\n * separate step refs from terminal arms without a string-equality dance.\n */\nexport function isTerminalSentinel(value: unknown): value is TerminalSentinel {\n return typeof value === \"string\" && TERMINAL_SENTINELS.has(value as TerminalSentinel);\n}\n","import {\n Component,\n Suspense,\n createElement,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport type { ModuleDescriptor, ModuleEntryPoint } from \"@modular-react/core\";\nimport { resolveEntryComponent } from \"@modular-react/react\";\n\nimport { getInternals } from \"./runtime.js\";\nimport { useJourneyContext } from \"./provider.js\";\nimport { isAnnotatedTransition } from \"./define-transition.js\";\nimport type {\n AnyJourneyDefinition,\n InstanceId,\n JourneyRuntime,\n JourneyStep,\n TerminalOutcome,\n} 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 * When `false`, the outlet renders the instance you handed it directly,\n * even if it has a child journey in flight. Set this when you compose\n * two outlets to render parent and child side-by-side or in a modal —\n * the parent outlet stays on the parent's step, and a sibling outlet\n * keyed off `instance.activeChildId` renders the child.\n *\n * When `true` (the default), the outlet walks the active call chain\n * from the supplied `instanceId` down to the leaf and renders the leaf,\n * matching the subroutine intuition: a child takes over the same\n * outlet for the duration of its run.\n */\n readonly leafOnly?: boolean;\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 * Speculatively prefetch the chunks for entries reachable from the\n * current step during idle time after mount, so the next click finds\n * its bundle hot.\n *\n * `\"precise\"` (default, alias `true`) — read declared `targets` from\n * `defineTransition({ targets, handle })`-annotated handlers on the\n * current step's transitions. Preload exactly those entries.\n * Bare-function handlers contribute nothing (this is the precise\n * mode's whole point — no guessing).\n *\n * `\"aggressive\"` — preload every entry that appears as a transition\n * source OR as a declared `target` of any annotated handler in the\n * journey's `transitions` map. The destination-side pass catches\n * terminal-only steps that have no outbound transitions of their\n * own (e.g. a freshly-added receipt screen reachable from `next:`\n * but not yet wired with its own exits). A step reachable only\n * via `definition.start` AND with no outbound transitions of its\n * own is the one remaining static gap — but such a step can only\n * be the current step on first mount (no exits → no advance), and\n * the skip-current logic already excludes it. Useful when handlers\n * are not annotated and the journey is small enough that warming\n * all candidates is cheap.\n *\n * `false` — opt out entirely.\n *\n * Has no effect for eager (`component:`) entries — their import is\n * already resolved. Effects only fire in the browser; SSR is a no-op.\n */\n readonly preload?: boolean | \"precise\" | \"aggressive\";\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 leafOnly = true,\n preload = \"precise\",\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 // Subscribe to the originally-supplied (root) instance so onFinished and\n // abandon-on-unmount resolve against THAT instance — even when leaf-walk\n // is rendering a different (child/grandchild) record. The leaf is also\n // subscribed-to via `useLeafId` so this component re-renders when the\n // call chain shifts (parent invokes a child, child terminates, etc.).\n const rootInstance = useInstanceSnapshot(runtime, instanceId);\n const leafId = useLeafId(runtime, instanceId, leafOnly);\n // Subscribe to the leaf separately so leaf-internal transitions trigger\n // re-renders. When the leaf id equals the root, useInstanceSnapshot\n // dedupes naturally (same subscribe call to the same record).\n const leafInstance = useInstanceSnapshot(runtime, leafId);\n const instance = leafId === instanceId ? rootInstance : leafInstance;\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 //\n // Targets the ROOT — `runtime.end` cascades to any active child, so a\n // single call cleans the whole chain.\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 — bound to the ROOT, since\n // the caller asked to be notified when the journey they mounted finishes.\n // Child terminations are observed via the parent's resume handler and\n // are not reported through this hook.\n const finishedFiredRef = useRef(false);\n useEffect(() => {\n if (!rootInstance) return;\n if (rootInstance.status !== \"completed\" && rootInstance.status !== \"aborted\") return;\n if (finishedFiredRef.current) return;\n finishedFiredRef.current = true;\n onFinished?.({\n status: rootInstance.status,\n payload: rootInstance.terminalPayload,\n instanceId: rootInstance.id,\n journeyId: rootInstance.journeyId,\n });\n }, [rootInstance, onFinished]);\n\n // Speculative preload of reachable entries' chunks. Runs after the current\n // step is settled in-DOM; cancels on step change so a fast advance does\n // not race with the previous step's preload set. Deps are deliberately\n // narrow — `instance` itself changes reference on every snapshot bump\n // (timestamps, child-id shifts), and re-running preload on those is wasted\n // work. We re-key the effect on (status, module, entry, journey) instead.\n const isActive = instance?.status === \"active\";\n const stepModuleId = instance?.step?.moduleId;\n const stepEntryName = instance?.step?.entry;\n const journeyId = instance?.journeyId;\n useEffect(() => {\n if (preload === false || !isActive) return;\n if (!stepModuleId || !stepEntryName || !journeyId) return;\n const reg = internals.__getRegistered(journeyId);\n if (!reg) return;\n const mode = preload === \"aggressive\" ? \"aggressive\" : \"precise\";\n const targets = collectPreloadTargets(\n reg.definition,\n modules,\n stepModuleId,\n stepEntryName,\n mode,\n );\n if (targets.length === 0) return;\n\n let cancelled = false;\n const run = (): void => {\n if (cancelled) return;\n for (const entry of targets) {\n try {\n resolveEntryComponent(entry).preload();\n } catch {\n // Best-effort: a malformed entry would have failed validation\n // upstream. Swallow here so one bad entry never hides the rest.\n }\n }\n };\n\n const ricFn = (\n globalThis as {\n requestIdleCallback?: (cb: () => void, opts?: { timeout?: number }) => number;\n }\n ).requestIdleCallback;\n const cicFn = (globalThis as { cancelIdleCallback?: (handle: number) => void })\n .cancelIdleCallback;\n let idleHandle: number | undefined;\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n if (typeof ricFn === \"function\") {\n idleHandle = ricFn(run, { timeout: 2000 });\n } else {\n timeoutHandle = setTimeout(run, 0);\n }\n return () => {\n cancelled = true;\n if (idleHandle !== undefined && typeof cicFn === \"function\") cicFn(idleHandle);\n if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);\n };\n }, [preload, isActive, stepModuleId, stepEntryName, journeyId, internals, modules]);\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 // Resolve the *leaf's* record and registration so the step callbacks\n // (`exit`, `goBack`) drive the leaf's transitions. Using the root's\n // record here would cross wires — exits dispatched by the leaf module\n // would land on the parent's step.\n const record = internals.__getRecord(instance.id);\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. Bound to the LEAF's instance id since the throw came from\n // the leaf step's component.\n internals.__fireComponentError(instance.id, err, step);\n let policy = onStepError?.(err, { step }) ?? \"abort\";\n if (policy === \"retry\") {\n // Defer the budget check to the runtime so the counter is owned in\n // one place and survives transition side-effects that advance\n // stepToken mid-retry.\n if (!internals.__consumeRetry(instance.id, retryLimit)) {\n policy = \"abort\";\n }\n }\n if (policy === \"abort\") {\n // End the LEAF — its abort cascades via the parent's resume handler\n // (which sees `outcome.status === \"aborted\"` and decides what to do),\n // matching the runtime's normal \"child aborted\" path. Calling end on\n // the root would skip the parent's chance to recover.\n runtime.end(instance.id, { 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 // Resolve eager (`component:`) and lazy (`lazy:`) entries through the\n // shared helper. Lazy entries get a memoized `React.lazy` wrapper plus an\n // idempotent `preload()`; eager entries pass through as-is. Both render\n // sites (here and `ModuleTab`) call this so the per-descriptor cache\n // is shared.\n const { Component: StepComponent } = resolveEntryComponent(entry);\n const stepKey = `${record.stepToken}:${retryKey}`;\n // For eager entries `entry.fallback` is typed `never` (and is always\n // `undefined` at runtime); for lazy entries it's the optional Suspense\n // fallback. Either way, fall through to the outlet-level `loadingFallback`.\n const suspenseFallback = entry.fallback ?? loadingFallback ?? null;\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(\n Suspense,\n { fallback: suspenseFallback },\n createElement(StepComponent, {\n input: step.input,\n exit,\n goBack,\n }),\n ),\n );\n}\n\n/**\n * Walk `definition.transitions` to assemble the set of entry-point\n * descriptors to preload. In `\"precise\"` mode we look at the current\n * step's transitions only and read each handler's declared `targets`;\n * in `\"aggressive\"` mode we walk every entry referenced anywhere in the\n * map. Both skip the current step (it's already mounted).\n */\nfunction collectPreloadTargets(\n definition: AnyJourneyDefinition,\n modules: Readonly<Record<string, ModuleDescriptor<any, any, any, any>>>,\n currentModuleId: string,\n currentEntry: string,\n mode: \"precise\" | \"aggressive\",\n): readonly ModuleEntryPoint<any>[] {\n const seen = new Set<string>();\n const out: ModuleEntryPoint<any>[] = [];\n const transitions = definition.transitions as\n | Record<string, Record<string, Record<string, unknown>> | undefined>\n | undefined;\n if (!transitions) return out;\n\n const collectPair = (moduleId: string, entryName: string): void => {\n if (!moduleId || !entryName) return;\n if (moduleId === currentModuleId && entryName === currentEntry) return;\n // Composite key only used to dedupe. Using `\u0000` as the separator\n // sidesteps any collision risk with module ids that legitimately\n // contain `/` (npm-style scopes) or other punctuation.\n const seenKey = `${moduleId}\u0000${entryName}`;\n if (seen.has(seenKey)) return;\n seen.add(seenKey);\n const entry = modules[moduleId]?.entryPoints?.[entryName];\n if (entry) out.push(entry);\n };\n\n if (mode === \"precise\") {\n const perEntry = transitions[currentModuleId]?.[currentEntry];\n if (!perEntry) return out;\n for (const value of Object.values(perEntry)) {\n if (!isAnnotatedTransition(value)) continue;\n for (const target of value.targets) {\n // Sentinel targets (`\"complete\"` / `\"abort\"` / `\"invoke\"`) carry\n // no chunk to preload — they're terminal-arm declarations for the\n // type system and the catalog harvester. Skip them here.\n if (typeof target === \"string\") continue;\n collectPair(target.module, target.entry);\n }\n }\n return out;\n }\n\n // Aggressive — every (module, entry) the journey could plausibly navigate\n // to: source-side keys (covers bare-function handlers and every step that\n // has outbound transitions wired) UNIONED with the destinations declared\n // by every annotated handler (covers terminal-only destination steps —\n // entries reachable from a `next:` arm that themselves have no outbound\n // transitions yet, e.g. a freshly-added receipt screen).\n //\n // The remaining static gap — a step reachable only via `definition.start`\n // AND with no outbound transitions of its own — is left uncovered. Such a\n // step can only be the current step on first mount (you can't advance\n // away from a step with no exits), in which case the skip-current logic\n // excludes it anyway. `definition.start` is a function and we\n // deliberately don't run it speculatively.\n for (const [moduleId, perModule] of Object.entries(transitions)) {\n if (!perModule) continue;\n for (const [entryName, perExit] of Object.entries(perModule)) {\n collectPair(moduleId, entryName);\n if (!perExit) continue;\n for (const value of Object.values(perExit)) {\n if (!isAnnotatedTransition(value)) continue;\n for (const target of value.targets) {\n if (typeof target === \"string\") continue;\n collectPair(target.module, target.entry);\n }\n }\n }\n }\n return out;\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\n/**\n * Walk the active call chain from a root instance down to the leaf. The\n * leaf is the first instance in the chain that does not itself have an\n * `activeChildId`. Walks `activeChildId` greedily; bounded by a sanity\n * cap so a corrupted cycle (which the runtime should prevent) cannot\n * loop forever.\n *\n * Subscribes to every instance along the chain — when any link changes\n * (parent invokes a child, child resumes the parent, grandchild starts),\n * the consumer re-renders with a fresh leaf id.\n */\nfunction useLeafId(runtime: JourneyRuntime, rootId: InstanceId, enabled: boolean): InstanceId {\n // Chain of instance ids root → … → leaf. Recomputed on every render so\n // changes to any link mid-chain take effect on the next snapshot read.\n const chain = useCallChain(runtime, rootId, enabled);\n return chain[chain.length - 1] ?? rootId;\n}\n\n/**\n * Returns the call stack for an outlet's instance — root at index 0, the\n * active leaf at the end, intermediate parents in between. Useful for\n * shells that render layered presentations (e.g. parent visible\n * underneath, child in a modal): mount the parent outlet with\n * `leafOnly={false}` and the child outlet against `chain[chain.length - 1]`.\n *\n * Subscribes to every instance in the chain so the array re-resolves\n * when the chain shifts. Length is at least 1 (the root) for any\n * registered instance.\n */\nexport function useJourneyCallStack(\n runtime: JourneyRuntime,\n rootId: InstanceId,\n): readonly InstanceId[] {\n return useCallChain(runtime, rootId, true);\n}\n\n/**\n * Shared chain-walker used by both `useLeafId` (which takes the last\n * element) and `useJourneyCallStack` (which takes the whole array).\n * Subscribes to every instance along the way and re-subscribes\n * whenever the chain shifts so deep transitions still trigger snapshot\n * reads.\n */\n// Sanity bound to break a corrupted cycle in the activeChild graph; legitimate\n// invoke nesting is not expected to approach this depth. If a real product\n// stacks deeper, surface this through `JourneyRuntimeOptions` rather than\n// raising the constant blindly.\nconst MAX_CHAIN_DEPTH = 64;\n\nfunction useCallChain(\n runtime: JourneyRuntime,\n rootId: InstanceId,\n enabled: boolean,\n): readonly InstanceId[] {\n // useSyncExternalStore over a virtual \"chain\" store: subscribe to each\n // instance the chain currently traverses, return a frozen-per-render\n // array on snapshot read. The chain can shift while we're subscribed\n // (a leaf invokes a grandchild, mid-chain instance terminates) — when\n // that happens, `fire` re-walks the chain and tops up subscriptions\n // for any newly-reachable instance, so deep transitions still surface.\n const subscribe = useMemo(\n () => (listener: () => void) => {\n const unsubs = new Map<InstanceId, () => void>();\n let stopped = false;\n const fire = () => {\n if (stopped) return;\n // The chain may have grown — top up subscriptions before notifying.\n // This keeps `useJourneyCallStack` correct for chains beyond depth\n // 1 without paying for unnecessary work: only newly-reachable ids\n // call into `runtime.subscribe`.\n rewire();\n listener();\n };\n const rewire = () => {\n const seen = new Set<InstanceId>();\n let id: InstanceId | null = rootId;\n let depth = 0;\n while (id && depth < MAX_CHAIN_DEPTH) {\n if (seen.has(id)) break;\n seen.add(id);\n if (!unsubs.has(id)) {\n unsubs.set(id, runtime.subscribe(id, fire));\n }\n const inst = runtime.getInstance(id);\n id = enabled && inst ? inst.activeChildId : null;\n depth += 1;\n }\n // Drop subscriptions for ids no longer in the chain.\n for (const [subscribedId, unsub] of unsubs) {\n if (!seen.has(subscribedId)) {\n unsub();\n unsubs.delete(subscribedId);\n }\n }\n };\n rewire();\n return () => {\n stopped = true;\n for (const unsub of unsubs.values()) unsub();\n unsubs.clear();\n };\n },\n [runtime, rootId, enabled],\n );\n const getSnapshot = () => resolveChain(runtime, rootId, enabled);\n // External-store snapshots must be referentially stable across reads\n // when nothing has changed. `resolveChain` returns a fresh array every\n // call, so we cache by rootId+enabled and re-issue when the joined-id\n // signature changes — the same trick the runtime uses for instance\n // snapshots via `revision`.\n const cacheRef = useRef<{ key: string; chain: readonly InstanceId[] } | null>(null);\n const getStableSnapshot = () => {\n const fresh = getSnapshot();\n const key = fresh.join(\">\");\n if (cacheRef.current && cacheRef.current.key === key) return cacheRef.current.chain;\n cacheRef.current = { key, chain: fresh };\n return fresh;\n };\n return useSyncExternalStore(subscribe, getStableSnapshot, getStableSnapshot);\n}\n\nfunction resolveChain(\n runtime: JourneyRuntime,\n rootId: InstanceId,\n enabled: boolean,\n): readonly InstanceId[] {\n const chain: InstanceId[] = [];\n let id: InstanceId | null = rootId;\n let depth = 0;\n const visited = new Set<InstanceId>();\n while (id && depth < MAX_CHAIN_DEPTH) {\n if (visited.has(id)) break; // Defensive: bail on cycle.\n visited.add(id);\n chain.push(id);\n const inst = runtime.getInstance(id);\n id = enabled && inst ? inst.activeChildId : null;\n depth += 1;\n }\n return chain;\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 { Suspense, createElement } from \"react\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport type { ModuleDescriptor } from \"@modular-react/core\";\nimport {\n ModuleErrorBoundary,\n resolveEntryComponent,\n useModuleExit,\n type ModuleExitEvent,\n} 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 } = resolveEntryComponent(entryPoint);\n // `fallback` is typed `never` on eager entries (always `undefined` at\n // runtime), `ReactNode | undefined` on lazy entries.\n const fallback = entryPoint.fallback ?? null;\n content = createElement(\n Suspense,\n { fallback },\n createElement(Component, { input: input as TInput, exit }),\n );\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, TOutput = unknown>(\n definition: JourneyDefinition<TModules, TState, TInput, TOutput>,\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, TOutput = unknown>(\n definition: JourneyDefinition<TModules, TState, TInput, TOutput>,\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 { InvokeSpec, 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` (and a typed `outcome.payload` when invoked from a\n * parent journey) 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` and `__output` fields are phantom: they never hold values\n * at runtime, they only carry types for the `start(handle, input)`\n * overload and a parent journey's resume handler signature.\n */\nexport type JourneyHandle<\n TId extends string = string,\n TInput = unknown,\n TOutput = unknown,\n> = JourneyHandleRef<TId, TInput, TOutput>;\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 and `outcome.payload`\n * narrowing through a parent journey's resume handler.\n */\nexport function defineJourneyHandle<TModules extends ModuleTypeMap, TState, TInput, TOutput>(\n def: JourneyDefinition<TModules, TState, TInput, TOutput>,\n): JourneyHandle<string, TInput, TOutput> {\n return { id: def.id };\n}\n\n/**\n * Typed builder for the `{ invoke }` arm of `TransitionResult`. The union\n * arm itself declares `InvokeSpec<unknown, unknown>` — bare object literals\n * like `{ invoke: { handle, input, resume } }` therefore won't catch a\n * mismatch between `input` and the handle's `TInput`. Going through this\n * helper threads both `TInput` and `TOutput` from the handle into `input`\n * (which must be assignable) and the returned spec, so authors get\n * end-to-end checking at the call site:\n *\n * ```ts\n * confirmAge: ({ state }) =>\n * invoke({\n * handle: verifyIdentityHandle, // TInput = { customerId: string }\n * input: { customerId: state.id }, // checked against TInput\n * resume: \"afterAgeVerified\",\n * }),\n * ```\n *\n * The runtime treats the result identically to a hand-built literal — the\n * only purpose is the compile-time check.\n */\nexport function invoke<TInput, TOutput>(spec: {\n readonly handle: JourneyHandleRef<string, TInput, TOutput>;\n readonly input: TInput;\n readonly resume: string;\n}): { readonly invoke: InvokeSpec<TInput, TOutput> } {\n return { invoke: spec };\n}\n","import type { EntryInputOf, EntryNamesOf, ModuleTypeMap, StepSpec } from \"@modular-react/core\";\n\n/**\n * One case in a `selectModule` map: an entry name on module `M` plus the\n * matching input shape. The mapped object union is collapsed via the\n * indexed-access trick at the end so the resulting type is a discriminated\n * union over the module's entries (the same shape `StepSpec` uses).\n */\ntype StepCaseFor<TModules extends ModuleTypeMap, M extends keyof TModules & string> = {\n [E in EntryNamesOf<TModules[M]> & string]: {\n readonly entry: E;\n readonly input: EntryInputOf<TModules[M], E>;\n };\n}[EntryNamesOf<TModules[M]> & string];\n\n/**\n * Cases map for the exhaustive form `selectModule(key, cases)` — one case\n * per discriminator value. Each case's `entry` is narrowed against that\n * module's `entryPoints`; `input` is checked against that entry. Missing a\n * key is a compile error, so a journey gains exhaustiveness for free when\n * the discriminator is a union literal.\n */\nexport type SelectModuleCases<\n TModules extends ModuleTypeMap,\n TKey extends keyof TModules & string,\n> = {\n readonly [M in TKey]: StepCaseFor<TModules, M>;\n};\n\n/**\n * Cases map for the fallback form `selectModuleOrDefault(key, cases,\n * fallback)`. Every case is optional and any discriminator value not\n * present in `cases` falls through to the explicit fallback `StepSpec`.\n * `TKey` is intentionally widened to `string` so callers can pass a\n * discriminator that includes values outside the module map (the fallback\n * handles them).\n *\n * Keys are constrained to `Extract<TKey, keyof TModules>` so a typo on a\n * module id still errors — the looseness is only on TKey itself, not on\n * which module ids the cases object accepts.\n */\nexport type SelectModuleCasesPartial<TModules extends ModuleTypeMap, TKey extends string> = {\n readonly [M in Extract<TKey, keyof TModules & string>]?: StepCaseFor<TModules, M>;\n};\n\n/**\n * Curried helper for state-driven module dispatch in a transition handler.\n *\n * `selectModule<TModules>()` binds the journey's module map; the inner call\n * takes a discriminator (`key`) whose value names the next module and a\n * cases object that supplies the entry + input for each branch. Returns a\n * `StepSpec` ready to drop into `{ next }`.\n *\n * **Why curry on `TModules`?** Same partial-inference reason as\n * `defineJourney`: TypeScript can't infer `TKey` while we're also forcing\n * `TModules` to be specified — splitting the calls lets us spell the module\n * map once and let the discriminator's union flow naturally into `TKey`.\n *\n * **Exhaustive by design.** The cases object is `Record<TKey, …>`, so when\n * `key` is a union literal (`\"github\" | \"strapi\" | \"contentful\"`),\n * forgetting a branch is a compile error. When you want a default-everything\n * fallback instead of branch-by-branch coverage, use\n * {@link selectModuleOrDefault}.\n *\n * **What you get:**\n * - **Per-branch input checking** — each case's `entry` and `input` are\n * typed against the module the branch dispatches to. You can't paste a\n * `strapi`-shaped input under the `github` key.\n * - **One state-spread instead of N** — call sites no longer repeat\n * `{ state, next: { … } }` per branch.\n *\n * **Limit:** the discriminator key must equal the target module id. When\n * the discriminator differs from the id (e.g. `tier: \"free\" | \"paid\"`\n * dispatching to module ids `trial-onboarding` / `billing-onboarding`),\n * fall back to a `switch` returning `next` per branch — that case isn't\n * common enough yet to justify a second helper.\n *\n * @example\n * ```ts\n * import { selectModule } from \"@modular-react/journeys\";\n *\n * const select = selectModule<IntegrationModules>();\n *\n * chosen: ({ output, state }) => ({\n * state: { ...state, selected: output.kind },\n * next: select(output.kind, {\n * github: { entry: \"configure\", input: { workspaceId: state.workspaceId } },\n * strapi: { entry: \"configure\", input: { workspaceId: state.workspaceId } },\n * contentful: { entry: \"configure\", input: { workspaceId: state.workspaceId } },\n * }),\n * }),\n * ```\n *\n * Zero runtime cost beyond an object lookup.\n */\nexport const selectModule =\n <TModules extends ModuleTypeMap>() =>\n <TKey extends keyof TModules & string>(\n key: TKey,\n cases: SelectModuleCases<TModules, TKey>,\n ): StepSpec<TModules> => {\n // `hasOwn`-gate the lookup so prototype-chain keys (`__proto__`,\n // `toString`, …) can't masquerade as a valid branch when types are\n // bypassed at runtime — `cases[\"__proto__\"]` would otherwise return\n // Object.prototype and produce a malformed StepSpec. With the gate,\n // the no-match path falls into the throw below.\n if (!hasOwnCase(cases, key)) {\n // Reachable only when types are bypassed (a runtime value escaped the\n // discriminator's union, e.g. via a serialized blob). Throw with the\n // offending key in the message — silently producing an invalid\n // StepSpec would fail later inside the runtime with a far less\n // actionable error.\n throw new Error(\n `[@modular-react/journeys] selectModule: no case for key \"${String(key)}\". ` +\n `Use selectModuleOrDefault if a fallback is intentional.`,\n );\n }\n return moduleStep(key, cases[key]);\n };\n\n/**\n * Sibling of {@link selectModule} that allows partial cases plus an\n * explicit fallback `StepSpec`. Use when most discriminator values funnel\n * into a generic module and only a few warrant their own specific\n * dispatch.\n *\n * Kept as a separate function (rather than a third argument on\n * `selectModule`) so that the *exhaustive* call site is visually\n * distinct from the *fallback-allowed* one — losing exhaustiveness in\n * `selectModule` by accidentally adding a third argument later would\n * silently disable the missing-branch compile error.\n *\n * The cases object is `Partial<Record<TKey, …>>`; any discriminator value\n * not present uses `fallback`. The fallback is a full `StepSpec`\n * (carrying its own `module`) since it isn't keyed by the discriminator.\n *\n * @example\n * ```ts\n * import { selectModuleOrDefault } from \"@modular-react/journeys\";\n *\n * const select = selectModuleOrDefault<IntegrationModules>();\n *\n * chosen: ({ output, state }) => ({\n * state: { ...state, selected: output.kind },\n * next: select(\n * output.kind,\n * {\n * github: { entry: \"configure\", input: { workspaceId: state.workspaceId, repo: output.repo } },\n * },\n * { module: \"generic\", entry: \"configure\", input: { workspaceId: state.workspaceId, kind: output.kind } },\n * ),\n * }),\n * ```\n */\nexport const selectModuleOrDefault =\n <TModules extends ModuleTypeMap>() =>\n <TKey extends string>(\n key: TKey,\n cases: SelectModuleCasesPartial<TModules, TKey>,\n fallback: StepSpec<TModules>,\n ): StepSpec<TModules> => {\n // `hasOwn`-gate (see selectModule) so prototype-chain keys can't slip\n // a malformed branch past the fallback path.\n if (!hasOwnCase(cases, key)) return fallback;\n const branch = (cases as Record<string, StepCaseFor<TModules, never>>)[key];\n return moduleStep(key, branch);\n };\n\n/**\n * Shared step-builder used by both helpers. Lifted out so the runtime\n * behaviour is identical and any future tweak (e.g. dev-mode freezing)\n * lives in one place.\n *\n * The cast is necessary because TS can't see that\n * `{ module: key, entry: branch.entry, input: branch.input }` aligns with\n * the discriminated `StepSpec` union — but the cases-object constraint\n * (StepCaseFor<TModules, M>) guarantees the alignment per branch.\n */\nfunction moduleStep<TModules extends ModuleTypeMap>(\n key: string,\n branch: { readonly entry: string; readonly input: unknown },\n): StepSpec<TModules> {\n return {\n module: key,\n entry: branch.entry,\n input: branch.input,\n } as StepSpec<TModules>;\n}\n\n/**\n * Own-property check used by both helpers before indexing into `cases`.\n * Without this gate, a runtime key like `\"__proto__\"` or `\"toString\"`\n * (reachable when the discriminator's typing has been bypassed) would\n * resolve to `Object.prototype` and produce a malformed StepSpec instead\n * of falling into the throw / fallback path.\n */\nfunction hasOwnCase(cases: object, key: PropertyKey): boolean {\n return Object.prototype.hasOwnProperty.call(cases, key);\n}\n"],"mappings":"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"}