1import type { DynamicConvention, RouteNode } from './Route'; 2 3async function recurseAndFlattenNodes< 4 T, 5 TProps, 6 TProcess extends (node: T, props: any) => Promise<T[]>, 7>(nodes: T[], props: TProps, func: TProcess): Promise<T[]> { 8 const tarr = await Promise.all(nodes.map((node) => func(node, props)).flat()); 9 return tarr.filter(Boolean) as T[]; 10} 11 12export async function loadStaticParamsAsync(route: RouteNode): Promise<RouteNode> { 13 const processed = ( 14 await Promise.all( 15 route.children.map((route) => loadStaticParamsRecursive(route, { parentParams: {} })) 16 ) 17 ).flat(); 18 19 route.children = processed; 20 return route; 21} 22 23function assertStaticParams(route: RouteNode, params: Record<string, string | string[]>) { 24 const matches = route.dynamic!.every((dynamic) => { 25 const value = params[dynamic.name]; 26 return value !== undefined && value !== null; 27 }); 28 if (!matches) { 29 throw new Error( 30 `generateStaticParams() must return an array of params that match the dynamic route. Received ${JSON.stringify( 31 params 32 )}` 33 ); 34 } 35 36 const validateSingleParam = ( 37 dynamic: DynamicConvention, 38 value: any, 39 allowMultipleSegments?: boolean 40 ) => { 41 if (typeof value !== 'string') { 42 throw new Error( 43 `generateStaticParams() for route "${route.contextKey}" expected param "${ 44 dynamic.name 45 }" to be of type string, instead found "${typeof value}" while parsing "${value}".` 46 ); 47 } 48 const parts = value.split('/').filter(Boolean); 49 if (parts.length > 1 && !allowMultipleSegments) { 50 throw new Error( 51 `generateStaticParams() for route "${route.contextKey}" expected param "${dynamic.name}" to not contain "/" (multiple segments) while parsing "${value}".` 52 ); 53 } 54 if (parts.length === 0) { 55 throw new Error( 56 `generateStaticParams() for route "${route.contextKey}" expected param "${dynamic.name}" not to be empty while parsing "${value}".` 57 ); 58 } 59 }; 60 61 route.dynamic!.forEach((dynamic) => { 62 const value = params[dynamic.name]; 63 if (dynamic.deep) { 64 // TODO: We could split strings by `/` and use that too. 65 if (!Array.isArray(value)) { 66 validateSingleParam(dynamic, value, true); 67 } else { 68 validateSingleParam(dynamic, value.filter(Boolean).join('/'), true); 69 } 70 } else { 71 validateSingleParam(dynamic, value); 72 } 73 return value !== undefined && value !== null; 74 }); 75} 76 77/** lodash.uniqBy */ 78function uniqBy<T>(array: T[], key: (item: T) => string): T[] { 79 const seen: { [key: string]: boolean } = {}; 80 return array.filter((item) => { 81 const k = key(item); 82 if (seen[k]) { 83 return false; 84 } 85 seen[k] = true; 86 return true; 87 }); 88} 89 90async function loadStaticParamsRecursive( 91 route: RouteNode, 92 props: { parentParams: any } 93): Promise<RouteNode[]> { 94 if (!route?.dynamic && !route?.children?.length) { 95 return [route]; 96 } 97 98 const loaded = await route.loadRoute(); 99 100 let staticParams: Record<string, string | string[]>[] = []; 101 102 if (loaded.generateStaticParams) { 103 staticParams = await loaded.generateStaticParams({ 104 params: props.parentParams || {}, 105 }); 106 if (!Array.isArray(staticParams)) { 107 throw new Error( 108 `generateStaticParams() must return an array of params, received ${staticParams}` 109 ); 110 } 111 112 // Assert that at least one param from each matches the dynamic route. 113 staticParams.forEach((params) => assertStaticParams(route, params)); 114 } 115 116 route.children = uniqBy( 117 ( 118 await recurseAndFlattenNodes( 119 [...route.children], 120 { 121 ...props, 122 parentParams: { 123 ...props.parentParams, 124 ...staticParams, 125 }, 126 }, 127 loadStaticParamsRecursive 128 ) 129 ).flat(), 130 (i) => i.route 131 ); 132 133 const createParsedRouteName = (input: string, params: any) => { 134 let parsedRouteName = input; 135 route.dynamic?.map((query) => { 136 const param = params[query.name]; 137 const formattedParameter = Array.isArray(param) ? param.join('/') : param; 138 if (query.deep) { 139 parsedRouteName = parsedRouteName.replace(`[...${query.name}]`, formattedParameter); 140 } else { 141 parsedRouteName = parsedRouteName.replace(`[${query.name}]`, param); 142 } 143 }); 144 145 return parsedRouteName; 146 }; 147 148 const generatedRoutes = await Promise.all( 149 staticParams.map(async (params) => { 150 const parsedRoute = createParsedRouteName(route.route, params); 151 const generatedContextKey = createParsedRouteName(route.contextKey, params); 152 153 return { 154 ...route, 155 // TODO: Add a new field for this 156 contextKey: generatedContextKey, 157 // Convert the dynamic route to a static route. 158 dynamic: null, 159 route: parsedRoute, 160 children: uniqBy( 161 ( 162 await recurseAndFlattenNodes( 163 [...route.children], 164 { 165 ...props, 166 parentParams: { 167 ...props.parentParams, 168 ...staticParams, 169 }, 170 }, 171 loadStaticParamsRecursive 172 ) 173 ).flat(), 174 (i) => i.route 175 ), 176 }; 177 }) 178 ); 179 180 return [route, ...generatedRoutes]; 181} 182