1b6b91c50SEvan Baconimport { ExpoConfig, getConfig } from '@expo/config';
2b6b91c50SEvan Baconimport type { LoadOptions } from '@expo/metro-config';
30f61c876SCedric van Puttenimport chalk from 'chalk';
4edeec536SEvan Baconimport { Server as ConnectServer } from 'connect';
58d307f52SEvan Baconimport http from 'http';
633643b60SEvan Baconimport type Metro from 'metro';
78d307f52SEvan Baconimport { Terminal } from 'metro-core';
8429dc7fcSEvan Baconimport semver from 'semver';
9429dc7fcSEvan Baconimport { URL } from 'url';
108d307f52SEvan Bacon
118a424bebSJames Ideimport { MetroBundlerDevServer } from './MetroBundlerDevServer';
128a424bebSJames Ideimport { MetroTerminalReporter } from './MetroTerminalReporter';
13edeec536SEvan Baconimport { importCliServerApiFromProject, importExpoMetroConfig } from './resolveFromProject';
1446f023faSEvan Baconimport { getRouterDirectoryModuleIdWithManifest } from './router';
158a424bebSJames Ideimport { runServer } from './runServer-fork';
168a424bebSJames Ideimport { withMetroMultiPlatformAsync } from './withMetroMultiPlatform';
17edeec536SEvan Baconimport { MetroDevServerOptions } from '../../../export/fork-bundleAsync';
180f61c876SCedric van Puttenimport { Log } from '../../../log';
19d42dd5d4SCedric van Puttenimport { getMetroProperties } from '../../../utils/analytics/getMetroProperties';
20d42dd5d4SCedric van Puttenimport { createDebuggerTelemetryMiddleware } from '../../../utils/analytics/metroDebuggerMiddleware';
21d42dd5d4SCedric van Puttenimport { logEventAsync } from '../../../utils/analytics/rudderstackClient';
220f61c876SCedric van Puttenimport { env } from '../../../utils/env';
230a6ddb20SEvan Baconimport { getMetroServerRoot } from '../middleware/ManifestMiddleware';
24edeec536SEvan Baconimport createJsInspectorMiddleware from '../middleware/inspector/createJsInspectorMiddleware';
25edeec536SEvan Baconimport { prependMiddleware, replaceMiddlewareWith } from '../middleware/mutations';
26edeec536SEvan Baconimport { remoteDevtoolsCorsMiddleware } from '../middleware/remoteDevtoolsCorsMiddleware';
27edeec536SEvan Baconimport { remoteDevtoolsSecurityHeadersMiddleware } from '../middleware/remoteDevtoolsSecurityHeadersMiddleware';
28429dc7fcSEvan Baconimport { ServerNext, ServerRequest, ServerResponse } from '../middleware/server.types';
29edeec536SEvan Baconimport { suppressRemoteDebuggingErrorMiddleware } from '../middleware/suppressErrorMiddleware';
306d6b81f9SEvan Baconimport { getPlatformBundlers } from '../platformBundlers';
318d307f52SEvan Bacon
328d307f52SEvan Bacon// From expo/dev-server but with ability to use custom logger.
338d307f52SEvan Bacontype MessageSocket = {
348d307f52SEvan Bacon  broadcast: (method: string, params?: Record<string, any> | undefined) => void;
358d307f52SEvan Bacon};
368d307f52SEvan Bacon
37429dc7fcSEvan Baconfunction gteSdkVersion(exp: Pick<ExpoConfig, 'sdkVersion'>, sdkVersion: string): boolean {
38429dc7fcSEvan Bacon  if (!exp.sdkVersion) {
39429dc7fcSEvan Bacon    return false;
40429dc7fcSEvan Bacon  }
41429dc7fcSEvan Bacon
42429dc7fcSEvan Bacon  if (exp.sdkVersion === 'UNVERSIONED') {
43429dc7fcSEvan Bacon    return true;
44429dc7fcSEvan Bacon  }
45429dc7fcSEvan Bacon
46429dc7fcSEvan Bacon  try {
47429dc7fcSEvan Bacon    return semver.gte(exp.sdkVersion, sdkVersion);
48429dc7fcSEvan Bacon  } catch {
49429dc7fcSEvan Bacon    throw new Error(`${exp.sdkVersion} is not a valid version. Must be in the form of x.y.z`);
50429dc7fcSEvan Bacon  }
51429dc7fcSEvan Bacon}
52429dc7fcSEvan Bacon
53b6b91c50SEvan Baconexport async function loadMetroConfigAsync(
548d307f52SEvan Bacon  projectRoot: string,
55b6b91c50SEvan Bacon  options: LoadOptions,
56b6b91c50SEvan Bacon  {
57b6b91c50SEvan Bacon    exp = getConfig(projectRoot, { skipSDKVersionRequirement: true, skipPlugins: true }).exp,
58429dc7fcSEvan Bacon    isExporting,
59429dc7fcSEvan Bacon  }: { exp?: ExpoConfig; isExporting: boolean }
60b6b91c50SEvan Bacon) {
618d307f52SEvan Bacon  let reportEvent: ((event: any) => void) | undefined;
620a6ddb20SEvan Bacon  const serverRoot = getMetroServerRoot(projectRoot);
638d307f52SEvan Bacon
648d307f52SEvan Bacon  const terminal = new Terminal(process.stdout);
650a6ddb20SEvan Bacon  const terminalReporter = new MetroTerminalReporter(serverRoot, terminal);
668d307f52SEvan Bacon
678d307f52SEvan Bacon  const reporter = {
688d307f52SEvan Bacon    update(event: any) {
698d307f52SEvan Bacon      terminalReporter.update(event);
708d307f52SEvan Bacon      if (reportEvent) {
718d307f52SEvan Bacon        reportEvent(event);
728d307f52SEvan Bacon      }
738d307f52SEvan Bacon    },
748d307f52SEvan Bacon  };
758d307f52SEvan Bacon
76036e9444SEvan Bacon  const ExpoMetroConfig = importExpoMetroConfig(projectRoot);
77b6b91c50SEvan Bacon  let config = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options });
78b6b91c50SEvan Bacon
79429dc7fcSEvan Bacon  if (
80429dc7fcSEvan Bacon    // Requires SDK 50 for expo-assets hashAssetPlugin change.
81429dc7fcSEvan Bacon    !exp.sdkVersion ||
82429dc7fcSEvan Bacon    gteSdkVersion(exp, '50.0.0')
83429dc7fcSEvan Bacon  ) {
84429dc7fcSEvan Bacon    if (isExporting) {
85429dc7fcSEvan Bacon      // This token will be used in the asset plugin to ensure the path is correct for writing locally.
86429dc7fcSEvan Bacon      // @ts-expect-error: typed as readonly.
877c98c357SEvan Bacon      config.transformer.publicPath = `/assets?export_path=${
887c98c357SEvan Bacon        (exp.experiments?.basePath ?? '') + '/assets'
897c98c357SEvan Bacon      }`;
90429dc7fcSEvan Bacon    } else {
91429dc7fcSEvan Bacon      // @ts-expect-error: typed as readonly
92429dc7fcSEvan Bacon      config.transformer.publicPath = '/assets/?unstable_path=.';
93429dc7fcSEvan Bacon    }
947c98c357SEvan Bacon  } else {
957c98c357SEvan Bacon    if (isExporting && exp.experiments?.basePath) {
967c98c357SEvan Bacon      // This token will be used in the asset plugin to ensure the path is correct for writing locally.
977c98c357SEvan Bacon      // @ts-expect-error: typed as readonly.
987c98c357SEvan Bacon      config.transformer.publicPath = exp.experiments?.basePath;
997c98c357SEvan Bacon    }
100429dc7fcSEvan Bacon  }
101429dc7fcSEvan Bacon
102f479be69SEvan Bacon  const platformBundlers = getPlatformBundlers(exp);
103b6b91c50SEvan Bacon
104f479be69SEvan Bacon  config = await withMetroMultiPlatformAsync(projectRoot, {
10546f023faSEvan Bacon    routerDirectory: getRouterDirectoryModuleIdWithManifest(projectRoot, exp),
106f479be69SEvan Bacon    config,
107f479be69SEvan Bacon    platformBundlers,
108*1c8e3ea0SMark Lawlor    isTsconfigPathsEnabled: exp.experiments?.tsconfigPaths ?? true,
1099580591fSEvan Bacon    webOutput: exp.web?.output ?? 'single',
110f479be69SEvan Bacon  });
111b6b91c50SEvan Bacon
112b6b91c50SEvan Bacon  logEventAsync('metro config', getMetroProperties(projectRoot, exp, config));
113b6b91c50SEvan Bacon
114b6b91c50SEvan Bacon  return {
115b6b91c50SEvan Bacon    config,
116b6b91c50SEvan Bacon    setEventReporter: (logger: (event: any) => void) => (reportEvent = logger),
117b6b91c50SEvan Bacon    reporter: terminalReporter,
118b6b91c50SEvan Bacon  };
119b6b91c50SEvan Bacon}
120b6b91c50SEvan Bacon
121b6b91c50SEvan Bacon/** The most generic possible setup for Metro bundler. */
122b6b91c50SEvan Baconexport async function instantiateMetroAsync(
12303d43e7dSCedric van Putten  metroBundler: MetroBundlerDevServer,
124429dc7fcSEvan Bacon  options: Omit<MetroDevServerOptions, 'logger'>,
125429dc7fcSEvan Bacon  { isExporting }: { isExporting: boolean }
126b6b91c50SEvan Bacon): Promise<{
127b6b91c50SEvan Bacon  metro: Metro.Server;
128b6b91c50SEvan Bacon  server: http.Server;
129b6b91c50SEvan Bacon  middleware: any;
130b6b91c50SEvan Bacon  messageSocket: MessageSocket;
131b6b91c50SEvan Bacon}> {
13203d43e7dSCedric van Putten  const projectRoot = metroBundler.projectRoot;
13303d43e7dSCedric van Putten
1346d6b81f9SEvan Bacon  // TODO: When we bring expo/metro-config into the expo/expo repo, then we can upstream this.
135e87e8ea8SEvan Bacon  const { exp } = getConfig(projectRoot, {
136e87e8ea8SEvan Bacon    skipSDKVersionRequirement: true,
137e87e8ea8SEvan Bacon    skipPlugins: true,
138e87e8ea8SEvan Bacon  });
139d42dd5d4SCedric van Putten
140b6b91c50SEvan Bacon  const { config: metroConfig, setEventReporter } = await loadMetroConfigAsync(
141b6b91c50SEvan Bacon    projectRoot,
142b6b91c50SEvan Bacon    options,
143429dc7fcSEvan Bacon    { exp, isExporting }
144b6b91c50SEvan Bacon  );
145d42dd5d4SCedric van Putten
146edeec536SEvan Bacon  const { createDevServerMiddleware, securityHeadersMiddleware } =
147edeec536SEvan Bacon    importCliServerApiFromProject(projectRoot);
148edeec536SEvan Bacon
149edeec536SEvan Bacon  const { middleware, messageSocketEndpoint, eventsSocketEndpoint, websocketEndpoints } =
150edeec536SEvan Bacon    createDevServerMiddleware({
1518d307f52SEvan Bacon      port: metroConfig.server.port,
1528d307f52SEvan Bacon      watchFolders: metroConfig.watchFolders,
1538d307f52SEvan Bacon    });
1548d307f52SEvan Bacon
155edeec536SEvan Bacon  // securityHeadersMiddleware does not support cross-origin requests for remote devtools to get the sourcemap.
156edeec536SEvan Bacon  // We replace with the enhanced version.
157edeec536SEvan Bacon  replaceMiddlewareWith(
158edeec536SEvan Bacon    middleware as ConnectServer,
159edeec536SEvan Bacon    securityHeadersMiddleware,
160edeec536SEvan Bacon    remoteDevtoolsSecurityHeadersMiddleware
161edeec536SEvan Bacon  );
162edeec536SEvan Bacon
163edeec536SEvan Bacon  middleware.use(remoteDevtoolsCorsMiddleware);
164edeec536SEvan Bacon
165edeec536SEvan Bacon  prependMiddleware(middleware, suppressRemoteDebuggingErrorMiddleware);
166edeec536SEvan Bacon
167edeec536SEvan Bacon  middleware.use('/inspector', createJsInspectorMiddleware());
168edeec536SEvan Bacon
169edeec536SEvan Bacon  // TODO: We can probably drop this now.
1708d307f52SEvan Bacon  const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware;
171b6b91c50SEvan Bacon  // @ts-expect-error: can't mutate readonly config
1728d307f52SEvan Bacon  metroConfig.server.enhanceMiddleware = (metroMiddleware: any, server: Metro.Server) => {
1738d307f52SEvan Bacon    if (customEnhanceMiddleware) {
1748d307f52SEvan Bacon      metroMiddleware = customEnhanceMiddleware(metroMiddleware, server);
1758d307f52SEvan Bacon    }
1768d307f52SEvan Bacon    return middleware.use(metroMiddleware);
1778d307f52SEvan Bacon  };
1788d307f52SEvan Bacon
179d42dd5d4SCedric van Putten  middleware.use(createDebuggerTelemetryMiddleware(projectRoot, exp));
180d42dd5d4SCedric van Putten
18103d43e7dSCedric van Putten  const { server, metro } = await runServer(metroBundler, metroConfig, {
1828d307f52SEvan Bacon    hmrEnabled: true,
183da5824c9SKudo Chien    // @ts-expect-error: Inconsistent `websocketEndpoints` type between metro and @react-native-community/cli-server-api
1848a782c0fSEvan Bacon    websocketEndpoints,
1850f61c876SCedric van Putten    watch: isWatchEnabled(),
1868d307f52SEvan Bacon  });
1878d307f52SEvan Bacon
188429dc7fcSEvan Bacon  prependMiddleware(middleware, (req: ServerRequest, res: ServerResponse, next: ServerNext) => {
189429dc7fcSEvan Bacon    // If the URL is a Metro asset request, then we need to skip all other middleware to prevent
190429dc7fcSEvan Bacon    // the community CLI's serve-static from hosting `/assets/index.html` in place of all assets if it exists.
191429dc7fcSEvan Bacon    // /assets/?unstable_path=.
192429dc7fcSEvan Bacon    if (req.url) {
193429dc7fcSEvan Bacon      const url = new URL(req.url!, 'http://localhost:8000');
194429dc7fcSEvan Bacon      if (url.pathname.match(/^\/assets\/?/) && url.searchParams.get('unstable_path') != null) {
195429dc7fcSEvan Bacon        return metro.processRequest(req, res, next);
196429dc7fcSEvan Bacon      }
197429dc7fcSEvan Bacon    }
198429dc7fcSEvan Bacon    return next();
199429dc7fcSEvan Bacon  });
200429dc7fcSEvan Bacon
201b6b91c50SEvan Bacon  setEventReporter(eventsSocketEndpoint.reportEvent);
2028a782c0fSEvan Bacon
2038a782c0fSEvan Bacon  return {
20433643b60SEvan Bacon    metro,
2058a782c0fSEvan Bacon    server,
2068a782c0fSEvan Bacon    middleware,
2078a782c0fSEvan Bacon    messageSocket: messageSocketEndpoint,
2088a782c0fSEvan Bacon  };
2098a782c0fSEvan Bacon}
2100f61c876SCedric van Putten
2110f61c876SCedric van Putten/**
2120f61c876SCedric van Putten * Simplify and communicate if Metro is running without watching file updates,.
2130f61c876SCedric van Putten * Exposed for testing.
2140f61c876SCedric van Putten */
2150f61c876SCedric van Puttenexport function isWatchEnabled() {
2160f61c876SCedric van Putten  if (env.CI) {
2170f61c876SCedric van Putten    Log.log(
2180f61c876SCedric van Putten      chalk`Metro is running in CI mode, reloads are disabled. Remove {bold CI=true} to enable watch mode.`
2190f61c876SCedric van Putten    );
2200f61c876SCedric van Putten  }
2210f61c876SCedric van Putten
2220f61c876SCedric van Putten  return !env.CI;
2230f61c876SCedric van Putten}
224