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 path from 'node:path';
8*46f023faSEvan Bacon
9*46f023faSEvan Baconimport { logMetroErrorAsync } from './metroErrorInterface';
10*46f023faSEvan Baconimport { getApiRoutesForDirectory } from './router';
11*46f023faSEvan Baconimport { requireFileContentsWithMetro } from '../getStaticRenderFunctions';
12*46f023faSEvan Bacon
13*46f023faSEvan Baconconst debug = require('debug')('expo:server-routes') as typeof console.log;
14*46f023faSEvan Bacon
15*46f023faSEvan Baconconst pendingRouteOperations = new Map<string, Promise<string | null>>();
16*46f023faSEvan Bacon
17*46f023faSEvan Baconexport type ApiRouteOptions = {
18*46f023faSEvan Bacon  mode?: string;
19*46f023faSEvan Bacon  appDir: string;
20*46f023faSEvan Bacon  port?: number;
21*46f023faSEvan Bacon  shouldThrow?: boolean;
22*46f023faSEvan Bacon};
23*46f023faSEvan Bacon
24*46f023faSEvan Bacon// Bundle the API Route with Metro and return the string contents to be evaluated in the server.
25*46f023faSEvan Baconexport async function bundleApiRoute(
26*46f023faSEvan Bacon  projectRoot: string,
27*46f023faSEvan Bacon  filepath: string,
28*46f023faSEvan Bacon  options: ApiRouteOptions
29*46f023faSEvan Bacon): Promise<string | null | undefined> {
30*46f023faSEvan Bacon  if (pendingRouteOperations.has(filepath)) {
31*46f023faSEvan Bacon    return pendingRouteOperations.get(filepath);
32*46f023faSEvan Bacon  }
33*46f023faSEvan Bacon
34*46f023faSEvan Bacon  const devServerUrl = `http://localhost:${options.port}`;
35*46f023faSEvan Bacon
36*46f023faSEvan Bacon  async function bundleAsync() {
37*46f023faSEvan Bacon    try {
38*46f023faSEvan Bacon      debug('Check API route:', options.appDir, filepath);
39*46f023faSEvan Bacon
40*46f023faSEvan Bacon      const middleware = await requireFileContentsWithMetro(projectRoot, devServerUrl, filepath, {
41*46f023faSEvan Bacon        minify: options.mode === 'production',
42*46f023faSEvan Bacon        dev: options.mode !== 'production',
43*46f023faSEvan Bacon        // Ensure Node.js
44*46f023faSEvan Bacon        environment: 'node',
45*46f023faSEvan Bacon      });
46*46f023faSEvan Bacon
47*46f023faSEvan Bacon      return middleware;
48*46f023faSEvan Bacon    } catch (error: any) {
49*46f023faSEvan Bacon      if (error instanceof Error) {
50*46f023faSEvan Bacon        await logMetroErrorAsync({ error, projectRoot });
51*46f023faSEvan Bacon      }
52*46f023faSEvan Bacon      if (options.shouldThrow) {
53*46f023faSEvan Bacon        throw error;
54*46f023faSEvan Bacon      }
55*46f023faSEvan Bacon      // TODO: improve error handling, maybe have this be a mock function which returns the static error html
56*46f023faSEvan Bacon      return null;
57*46f023faSEvan Bacon    } finally {
58*46f023faSEvan Bacon      // pendingRouteOperations.delete(filepath);
59*46f023faSEvan Bacon    }
60*46f023faSEvan Bacon  }
61*46f023faSEvan Bacon  const route = bundleAsync();
62*46f023faSEvan Bacon
63*46f023faSEvan Bacon  pendingRouteOperations.set(filepath, route);
64*46f023faSEvan Bacon  return route;
65*46f023faSEvan Bacon}
66*46f023faSEvan Bacon
67*46f023faSEvan Baconexport async function rebundleApiRoute(
68*46f023faSEvan Bacon  projectRoot: string,
69*46f023faSEvan Bacon  filepath: string,
70*46f023faSEvan Bacon  options: ApiRouteOptions
71*46f023faSEvan Bacon) {
72*46f023faSEvan Bacon  pendingRouteOperations.delete(filepath);
73*46f023faSEvan Bacon  return bundleApiRoute(projectRoot, filepath, options);
74*46f023faSEvan Bacon}
75*46f023faSEvan Bacon
76*46f023faSEvan Baconexport async function exportAllApiRoutesAsync(projectRoot: string, options: ApiRouteOptions) {
77*46f023faSEvan Bacon  const files: Map<string, string> = new Map();
78*46f023faSEvan Bacon
79*46f023faSEvan Bacon  await Promise.all(
80*46f023faSEvan Bacon    getApiRoutesForDirectory(options.appDir).map(async (filepath) => {
81*46f023faSEvan Bacon      const contents = await bundleApiRoute(projectRoot, filepath, options);
82*46f023faSEvan Bacon      files.set(path.relative(options.appDir, filepath.replace(/\.[tj]sx?$/, '.js')), contents!);
83*46f023faSEvan Bacon    })
84*46f023faSEvan Bacon  );
85*46f023faSEvan Bacon
86*46f023faSEvan Bacon  return files;
87*46f023faSEvan Bacon}
88