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