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