import React, { JSXElementConstructor } from 'react'; import { Action, createLocation, Location } from 'history'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { catchError, mergeMap } from 'rxjs/operators'; import { RouterProvider } from './Router'; import { ResolvedPath } from './PageRouter'; import { MwFn } from './middleware'; import { processRoute } from './processRoute'; export interface PageModule { default: JSXElementConstructor | any; // the page to be rendered by the router middleware?: MwFn[]; loadComponent?(resolved: ResolvedPath, ctx: AppContext): Observable | Promise; // a function that can alter the component loaded } export type Store = BehaviorSubject; export const Ctx = React.createContext<{ Cmp: JSXElementConstructor; resolved: ResolvedPath; appContext: AppContext; location: Location; action: Action; store: BehaviorSubject; pages: string[]; }>({ store: new BehaviorSubject({}), location: createLocation('/'), appContext: { componentLoader: (resolved: ResolvedPath) => Promise.resolve({ default: () => { // eslint-disable-next-line no-console console.log('not implemented'); }, }), }, action: 'POP', Cmp: function NotImplemented() { return null; }, pages: [], resolved: { id: -1, pageName: 'None', pathname: '/', chunkName: '' }, }); export type PageResponse = | { kind: 'success'; } | { kind: 'error'; statusCode: number; message: string; } | { kind: 'redirect'; statusCode: number; location: string; } | { kind: 'not-found'; statusCode: number; }; export interface AppOutput { app?: React.ReactElement; appContext: { [index: string]: any }; response: PageResponse; } export interface AppContext { componentLoader(resolved: ResolvedPath): Promise; [index: string]: any; } export type PluginFn = (params: PluginParams, appCtx: AppContext) => AppContext; export type PluginParams = { initialGQLState: any }; export function app( pathname: string, search: string, initialGQLState = {}, pages: string[] = [], storeState = {}, el: React.ReactElement, plugins: PluginFn[] = [], ): Observable { const store = new BehaviorSubject(storeState); const location = createLocation(pathname); location.search = search; const context: AppContext = { componentLoader: (resolved: ResolvedPath) => Promise.resolve({ default: () => { // eslint-disable-next-line no-console console.log('not implemented'); }, }), }; const nextContext = plugins.reduce((ctx, plugin) => { return plugin({ initialGQLState }, ctx); }, context); return processRoute(pathname, location, store, pages, nextContext).pipe( mergeMap(({ mod, resolved, response }) => { return of({ response, appContext: nextContext, app: ( {el} ), }); }), catchError((e) => { console.error('error from process', e); return of({ appContext: nextContext, response: { kind: 'error' as const, message: e.message, statusCode: 500 }, }); }), ); }