import type { ActivityBaseParams, ActivityDefinition, Config, InferActivityParams, RegisteredActivityName, } from "@stackflow/config"; import { type CoreStore, makeCoreStore, makeEvent, type PushedEvent, } from "@stackflow/core"; import React, { useMemo } from "react"; import { ActivityComponentMapProvider } from "./ActivityComponentMapProvider"; import type { ActivityComponentType } from "./BaseActivityComponentType"; import { makeActivityId } from "./activity"; import { CoreProvider } from "./core"; import MainRenderer from "./MainRenderer"; import { PluginsProvider } from "./plugins"; import { isBrowser, makeRef } from "./utils"; import type { StackflowReactPlugin } from "./StackflowReactPlugin"; import type { Actions } from "./Actions"; import { ConfigProvider } from "./ConfigProvider"; import { DataLoaderProvider, loaderPlugin } from "./loader"; import { makeActions } from "./makeActions"; import { makePrepare } from "./makePrepare"; import { makeStepActions } from "./makeStepActions"; import type { StackComponentType } from "./StackComponentType"; import type { StepActions } from "./StepActions"; import type { Prepare } from "./Prepare"; export type StackflowPluginsEntry = | StackflowReactPlugin | StackflowPluginsEntry[]; export type StackflowInput< T extends ActivityDefinition, R extends { [activityName in RegisteredActivityName]: ActivityComponentType; }, > = { config: Config; components: R; plugins?: Array; }; export type StackflowOutput = { Stack: StackComponentType; actions: Actions; stepActions: StepActions; prepare: Prepare; }; export function stackflow< T extends ActivityDefinition, R extends { [activityName in RegisteredActivityName]: ActivityComponentType< InferActivityParams >; }, >(input: StackflowInput): StackflowOutput { const loadData = (activityName: string, activityParams: {}) => { const activityConfig = input.config.activities.find( (activity) => activity.name === activityName, ); if (!activityConfig) { throw new Error(`Activity ${activityName} is not registered.`); } const loaderData = activityConfig.loader?.({ params: activityParams, config: input.config, }); return loaderData; }; const plugins = [ ...(input.plugins ?? []) .flat(Number.POSITIVE_INFINITY as 0) .map((p) => p as StackflowReactPlugin), /** * `loaderPlugin()` must be placed after `historySyncPlugin()` */ loaderPlugin(input, loadData), ]; const enoughPastTime = () => new Date().getTime() - input.config.transitionDuration * 2; const staticCoreStore = makeCoreStore({ initialEvents: [ makeEvent("Initialized", { transitionDuration: input.config.transitionDuration, eventDate: enoughPastTime(), }), ...input.config.activities.map((activity) => makeEvent("ActivityRegistered", { activityName: activity.name, eventDate: enoughPastTime(), }), ), ], plugins: [], }); const [getCoreStore, setCoreStore] = makeRef(); const Stack: StackComponentType = React.memo((props) => { const initialContext = useMemo( () => ({ ...props.initialContext, ...(props.initialLoaderData ? { initialLoaderData: props.initialLoaderData, } : null), }), [], ); const coreStore = useMemo(() => { const prevCoreStore = getCoreStore(); /** * In a browser environment, * memoize `coreStore` so that only one `coreStore` exists throughout the entire app. */ if (isBrowser() && prevCoreStore) { return prevCoreStore; } const initialPushedEventsByOption = input.config.initialActivity ? [ makeEvent("Pushed", { activityId: makeActivityId(), activityName: input.config.initialActivity(), activityParams: {}, eventDate: enoughPastTime(), skipEnterActiveState: false, }), ] : []; const store = makeCoreStore({ initialEvents: [ ...staticCoreStore.pullEvents(), ...initialPushedEventsByOption, ], initialContext, plugins, handlers: { onInitialActivityIgnored: (initialPushedEvents) => { if (isBrowser()) { console.warn( `Stackflow - Some plugin overrides an "initialActivity" option. The "initialActivity" option you set to "${ (initialPushedEvents[0] as PushedEvent).activityName }" in the "stackflow" is ignored.`, ); } }, onInitialActivityNotFound: () => { if (isBrowser()) { console.warn( "Stackflow -" + " There is no initial activity." + " If you want to set the initial activity," + " add the `initialActivity` option of the `stackflow()` function or" + " add a plugin that sets the initial activity. (e.g. `@stackflow/plugin-history-sync`)", ); } }, }, }); if (isBrowser()) { store.init(); setCoreStore(store); } return store; }, []); return ( ); }); Stack.displayName = "Stack"; return { Stack, actions: makeActions(() => getCoreStore()?.actions), stepActions: makeStepActions(() => getCoreStore()?.actions), prepare: makePrepare({ config: input.config, loadData, activityComponentMap: input.components, }), }; }