1/**
2 * Copyright © 2022 650 Industries.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7import { ExpoResponse } from '@expo/server';
8import { createRequestHandler } from '@expo/server/build/vendor/http';
9import requireString from 'require-from-string';
10import resolve from 'resolve';
11import { promisify } from 'util';
12
13import { ForwardHtmlError } from './MetroBundlerDevServer';
14import { bundleApiRoute } from './bundleApiRoutes';
15import { fetchManifest } from './fetchRouterManifest';
16import { getErrorOverlayHtmlAsync, logMetroError, logMetroErrorAsync } from './metroErrorInterface';
17import { Log } from '../../../log';
18
19const debug = require('debug')('expo:start:server:metro') as typeof console.log;
20
21const resolveAsync = promisify(resolve) as any as (
22  id: string,
23  opts: resolve.AsyncOpts
24) => Promise<string | null>;
25
26export function createRouteHandlerMiddleware(
27  projectRoot: string,
28  options: {
29    mode?: string;
30    appDir: string;
31    port?: number;
32    getWebBundleUrl: () => string;
33    getStaticPageAsync: (pathname: string) => Promise<{ content: string }>;
34  }
35) {
36  return createRequestHandler(
37    { build: '' },
38    {
39      async getRoutesManifest() {
40        const manifest = await fetchManifest<RegExp>(projectRoot, options);
41        debug('manifest', manifest);
42        // NOTE: no app dir if null
43        // TODO: Redirect to 404 page
44        return manifest;
45      },
46      async getHtml(request) {
47        try {
48          const { content } = await options.getStaticPageAsync(request.url);
49          return content;
50        } catch (error: any) {
51          // Forward the Metro server response as-is. It won't be pretty, but at least it will be accurate.
52          if (error instanceof ForwardHtmlError) {
53            return new ExpoResponse(error.html, {
54              status: error.statusCode,
55              headers: {
56                'Content-Type': 'text/html',
57              },
58            });
59          }
60
61          try {
62            return new ExpoResponse(
63              await getErrorOverlayHtmlAsync({
64                error,
65                projectRoot,
66              }),
67              {
68                status: 500,
69                headers: {
70                  'Content-Type': 'text/html',
71                },
72              }
73            );
74          } catch (staticError: any) {
75            // Fallback error for when Expo Router is misconfigured in the project.
76            return new ExpoResponse(
77              '<span><h3>Internal Error:</h3><b>Project is not setup correctly for static rendering (check terminal for more info):</b><br/>' +
78                error.message +
79                '<br/><br/>' +
80                staticError.message +
81                '</span>',
82              {
83                status: 500,
84                headers: {
85                  'Content-Type': 'text/html',
86                },
87              }
88            );
89          }
90        }
91      },
92      logApiRouteExecutionError(error) {
93        logMetroError(projectRoot, { error });
94      },
95      async getApiRoute(route) {
96        const resolvedFunctionPath = await resolveAsync(route.page, {
97          extensions: ['.js', '.jsx', '.ts', '.tsx'],
98          basedir: options.appDir,
99        });
100
101        const middlewareContents = await bundleApiRoute(
102          projectRoot,
103          resolvedFunctionPath!,
104          options
105        );
106        if (!middlewareContents) {
107          // TODO: Error handling
108          return null;
109        }
110
111        try {
112          debug(`Bundling middleware at: ${resolvedFunctionPath}`);
113          return requireString(middlewareContents);
114        } catch (error: any) {
115          if (error instanceof Error) {
116            await logMetroErrorAsync({ projectRoot, error });
117          } else {
118            Log.error('Failed to load middleware: ' + error);
119          }
120          return new ExpoResponse(
121            'Failed to load middleware: ' + resolvedFunctionPath + '\n\n' + error.message,
122            {
123              status: 500,
124              headers: {
125                'Content-Type': 'text/html',
126              },
127            }
128          );
129        }
130      },
131    }
132  );
133}
134