import { stringify } from 'qs'; import { isRouteArgsWithParams, RouteFnArgs } from './routeArgs'; import { isRouteNodeWithParams, RouteNode, RouteNodeBase, RouteNodeWithoutParams, RouteNodeWithParams, } from './routeNode'; import { RawParams, RouteOptions, SerializedParams, TemplateParserMap, } from './types'; import { paramsParser, parseRoute, stringifyParams, stringifyRoute, } from './utils/routeUtils'; export type ChildRouteMap = Record< string, RouteNodeWithParams | RouteNodeWithoutParams >; export type RouteFnContext = { previousQueryParams: SerializedParams; previousPath: string; previousOptions: Required; }; export type RouteFn = < T extends string, TPM extends TemplateParserMap, CRM extends ChildRouteMap >( templateWithQuery: T, args: RouteFnArgs, children?: ChildrenRouterFn ) => RouteNode; export function routeFn< T extends string, TPM extends TemplateParserMap, CRM extends ChildRouteMap, RO extends RouteOptions >( this: RouteFnContext, templateWithQuery: T, args: RouteFnArgs, children?: ChildrenRouterFn ): RouteNode { var params: TemplateParserMap | undefined; if (isRouteArgsWithParams(args)) { params = args.params; } const parsedRoute = parseRoute(templateWithQuery, params); const fullTemplate = this.previousPath + '/' + parsedRoute.pathTemplate; const options: Required = { ...this.previousOptions, ...args.options, }; const _children = typeof children === 'function' ? children( childRouteFn({ previousPath: fullTemplate, previousQueryParams: { ...this.previousQueryParams }, previousOptions: options, }) ) : typeof children === 'object' ? children : undefined; const fn = (rawParams: RawParams) => new Proxy( {}, { get: (target, next, receiver) => { const pathParams = stringifyParams( parsedRoute.pathParamParsers, rawParams ); const queryParams = { ...this.previousQueryParams, ...stringifyParams(parsedRoute.queryParamParsers, rawParams), }; const path = stringifyRoute( parsedRoute.pathTokens, pathParams, this.previousPath ); if (next === '$') { return path + stringify(queryParams, { addQueryPrefix: true }); } else if (next === Symbol.toPrimitive) { return () => path + stringify(queryParams, { addQueryPrefix: true }); } else if (typeof next == 'string' && _children && _children[next]) { const nextChild = _children[next]; if (isRouteNodeWithParams(nextChild)) { return routeFn.call, [any, any, any], any>( { previousPath: path, previousQueryParams: queryParams, previousOptions: options, }, nextChild.templateWithQuery, { component: nextChild.element, options: nextChild.options, params: nextChild.paramsMap, }, nextChild.children ); } else { return routeFn.call, [any, any, any], any>( { previousPath: path, previousQueryParams: queryParams, previousOptions: options, }, nextChild.templateWithQuery, { component: nextChild.element, options: nextChild.options, }, nextChild.children ); } } return Reflect.get(target, next, receiver); }, } ); var node: any = { fullTemplate: fullTemplate, templateWithQuery: templateWithQuery, template: parsedRoute.pathTemplate, children: _children, options: options, caseSensitive: args.caseSensitive, loader: args.loader, element: args.element, action: args.action, errorElement: args.errorElement, shouldRevalidate: args.shouldRevalidate, index: args.index, layout: args.layout, } as RouteNodeBase; if (isRouteArgsWithParams(args)) { node.paramsMap = params; node.parseParams = paramsParser(parsedRoute); } return Object.assign(fn, node); } export type ChildrenRouterFn< RO extends RouteOptions, CRM extends ChildRouteMap > = (r: RouteFn) => { [K in keyof CRM]: CRM[K] }; const childRouteFn = ( context: RouteFnContext ): RouteFn => < T extends string, TPM extends TemplateParserMap, CRM extends ChildRouteMap, CRF extends ChildrenRouterFn >( templateWithQuery: T, args: RouteFnArgs, children?: CRF ): RouteNode => routeFn.call< RouteFnContext, [T, typeof args, CRF | undefined], RouteNode >(context, templateWithQuery, args, children);