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