18d307f52SEvan Baconimport { ExpoConfig, getConfig } from '@expo/config';
28d307f52SEvan Baconimport assert from 'assert';
3fdf34e39SEvan Baconimport chalk from 'chalk';
48d307f52SEvan Bacon
58a424bebSJames Ideimport { BundlerDevServer, BundlerStartOptions } from './BundlerDevServer';
68a424bebSJames Ideimport { getPlatformBundlers } from './platformBundlers';
7a7e47f4dSEvan Baconimport { Log } from '../../log';
88d307f52SEvan Baconimport { FileNotifier } from '../../utils/FileNotifier';
9ea99eec9SEvan Baconimport { logEventAsync } from '../../utils/analytics/rudderstackClient';
101117330aSMark Lawlorimport { env } from '../../utils/env';
118d307f52SEvan Baconimport { ProjectPrerequisite } from '../doctor/Prerequisite';
1233643b60SEvan Baconimport { TypeScriptProjectPrerequisite } from '../doctor/typescript/TypeScriptProjectPrerequisite';
13a7e47f4dSEvan Baconimport { printItem } from '../interface/commandsTable';
148d307f52SEvan Baconimport * as AndroidDebugBridge from '../platforms/android/adb';
15a7e47f4dSEvan Baconimport { resolveSchemeAsync } from '../resolveOptions';
168d307f52SEvan Bacon
17474a7a4bSEvan Baconconst debug = require('debug')('expo:start:server:devServerManager') as typeof console.log;
18474a7a4bSEvan Bacon
198d307f52SEvan Baconexport type MultiBundlerStartOptions = {
208d307f52SEvan Bacon  type: keyof typeof BUNDLERS;
218d307f52SEvan Bacon  options?: BundlerStartOptions;
228d307f52SEvan Bacon}[];
238d307f52SEvan Bacon
248d307f52SEvan Baconconst devServers: BundlerDevServer[] = [];
258d307f52SEvan Bacon
268d307f52SEvan Baconconst BUNDLERS = {
278d307f52SEvan Bacon  webpack: () =>
288d307f52SEvan Bacon    require('./webpack/WebpackBundlerDevServer')
298d307f52SEvan Bacon      .WebpackBundlerDevServer as typeof import('./webpack/WebpackBundlerDevServer').WebpackBundlerDevServer,
308d307f52SEvan Bacon  metro: () =>
318d307f52SEvan Bacon    require('./metro/MetroBundlerDevServer')
328d307f52SEvan Bacon      .MetroBundlerDevServer as typeof import('./metro/MetroBundlerDevServer').MetroBundlerDevServer,
338d307f52SEvan Bacon};
348d307f52SEvan Bacon
358d307f52SEvan Bacon/** Manages interacting with multiple dev servers. */
368d307f52SEvan Baconexport class DevServerManager {
3733643b60SEvan Bacon  private projectPrerequisites: ProjectPrerequisite<any, void>[] = [];
388d307f52SEvan Bacon
395404abc1SEvan Bacon  private notifier: FileNotifier | null = null;
405404abc1SEvan Bacon
418d307f52SEvan Bacon  constructor(
428d307f52SEvan Bacon    public projectRoot: string,
438d307f52SEvan Bacon    /** Keep track of the original CLI options for bundlers that are started interactively. */
448d307f52SEvan Bacon    public options: BundlerStartOptions
458d307f52SEvan Bacon  ) {
465404abc1SEvan Bacon    this.notifier = this.watchBabelConfig();
478d307f52SEvan Bacon  }
488d307f52SEvan Bacon
498d307f52SEvan Bacon  private watchBabelConfig() {
50fdf34e39SEvan Bacon    const notifier = new FileNotifier(
51fdf34e39SEvan Bacon      this.projectRoot,
52fdf34e39SEvan Bacon      [
538d307f52SEvan Bacon        './babel.config.js',
548d307f52SEvan Bacon        './babel.config.json',
558d307f52SEvan Bacon        './.babelrc.json',
568d307f52SEvan Bacon        './.babelrc',
578d307f52SEvan Bacon        './.babelrc.js',
58fdf34e39SEvan Bacon      ],
59fdf34e39SEvan Bacon      {
60fdf34e39SEvan Bacon        additionalWarning: chalk` You may need to clear the bundler cache with the {bold --clear} flag for your changes to take effect.`,
61fdf34e39SEvan Bacon      }
62fdf34e39SEvan Bacon    );
638d307f52SEvan Bacon
648d307f52SEvan Bacon    notifier.startObserving();
658d307f52SEvan Bacon
668d307f52SEvan Bacon    return notifier;
678d307f52SEvan Bacon  }
688d307f52SEvan Bacon
698d307f52SEvan Bacon  /** Lazily load and assert a project-level prerequisite. */
7033643b60SEvan Bacon  async ensureProjectPrerequisiteAsync(PrerequisiteClass: typeof ProjectPrerequisite<any, any>) {
718d307f52SEvan Bacon    let prerequisite = this.projectPrerequisites.find(
728d307f52SEvan Bacon      (prerequisite) => prerequisite instanceof PrerequisiteClass
738d307f52SEvan Bacon    );
748d307f52SEvan Bacon    if (!prerequisite) {
758d307f52SEvan Bacon      prerequisite = new PrerequisiteClass(this.projectRoot);
768d307f52SEvan Bacon      this.projectPrerequisites.push(prerequisite);
778d307f52SEvan Bacon    }
7833643b60SEvan Bacon    return await prerequisite.assertAsync();
798d307f52SEvan Bacon  }
808d307f52SEvan Bacon
818d307f52SEvan Bacon  /**
828d307f52SEvan Bacon   * Sends a message over web sockets to all connected devices,
838d307f52SEvan Bacon   * does nothing when the dev server is not running.
848d307f52SEvan Bacon   *
858d307f52SEvan Bacon   * @param method name of the command. In RN projects `reload`, and `devMenu` are available. In Expo Go, `sendDevCommand` is available.
868d307f52SEvan Bacon   * @param params extra event info to send over the socket.
878d307f52SEvan Bacon   */
888d307f52SEvan Bacon  broadcastMessage(method: 'reload' | 'devMenu' | 'sendDevCommand', params?: Record<string, any>) {
898d307f52SEvan Bacon    devServers.forEach((server) => {
908d307f52SEvan Bacon      server.broadcastMessage(method, params);
918d307f52SEvan Bacon    });
928d307f52SEvan Bacon  }
938d307f52SEvan Bacon
948d307f52SEvan Bacon  /** Get the port for the dev server (either Webpack or Metro) that is hosting code for React Native runtimes. */
958d307f52SEvan Bacon  getNativeDevServerPort() {
968d307f52SEvan Bacon    const server = devServers.find((server) => server.isTargetingNative());
978d307f52SEvan Bacon    return server?.getInstance()?.location.port ?? null;
988d307f52SEvan Bacon  }
998d307f52SEvan Bacon
1008d307f52SEvan Bacon  /** Get the first server that targets web. */
1018d307f52SEvan Bacon  getWebDevServer() {
1028d307f52SEvan Bacon    const server = devServers.find((server) => server.isTargetingWeb());
1038d307f52SEvan Bacon    return server ?? null;
1048d307f52SEvan Bacon  }
1058d307f52SEvan Bacon
1068d307f52SEvan Bacon  getDefaultDevServer(): BundlerDevServer {
1078d307f52SEvan Bacon    // Return the first native dev server otherwise return the first dev server.
1088d307f52SEvan Bacon    const server = devServers.find((server) => server.isTargetingNative());
1098d307f52SEvan Bacon    const defaultServer = server ?? devServers[0];
1108d307f52SEvan Bacon    assert(defaultServer, 'No dev servers are running');
1118d307f52SEvan Bacon    return defaultServer;
1128d307f52SEvan Bacon  }
1138d307f52SEvan Bacon
1148d307f52SEvan Bacon  async ensureWebDevServerRunningAsync() {
1158d307f52SEvan Bacon    const [server] = devServers.filter((server) => server.isTargetingWeb());
1168d307f52SEvan Bacon    if (server) {
1178d307f52SEvan Bacon      return;
1188d307f52SEvan Bacon    }
1196d6b81f9SEvan Bacon    const { exp } = getConfig(this.projectRoot, {
1206d6b81f9SEvan Bacon      skipPlugins: true,
1216d6b81f9SEvan Bacon      skipSDKVersionRequirement: true,
1226d6b81f9SEvan Bacon    });
1236d6b81f9SEvan Bacon    const bundler = getPlatformBundlers(exp).web;
1246d6b81f9SEvan Bacon    debug(`Starting ${bundler} dev server for web`);
1258d307f52SEvan Bacon    return this.startAsync([
1268d307f52SEvan Bacon      {
1276d6b81f9SEvan Bacon        type: bundler,
1288d307f52SEvan Bacon        options: this.options,
1298d307f52SEvan Bacon      },
1308d307f52SEvan Bacon    ]);
1318d307f52SEvan Bacon  }
1328d307f52SEvan Bacon
133a7e47f4dSEvan Bacon  /** Switch between Expo Go and Expo Dev Clients. */
134a7e47f4dSEvan Bacon  async toggleRuntimeMode(isUsingDevClient: boolean = !this.options.devClient): Promise<boolean> {
135a7e47f4dSEvan Bacon    const nextMode = isUsingDevClient ? '--dev-client' : '--go';
136a7e47f4dSEvan Bacon    Log.log(printItem(chalk`Switching to {bold ${nextMode}}`));
137a7e47f4dSEvan Bacon
138a7e47f4dSEvan Bacon    const nextScheme = await resolveSchemeAsync(this.projectRoot, {
139a7e47f4dSEvan Bacon      devClient: isUsingDevClient,
140a7e47f4dSEvan Bacon      // NOTE: The custom `--scheme` argument is lost from this point on.
141a7e47f4dSEvan Bacon    });
142a7e47f4dSEvan Bacon
143a7e47f4dSEvan Bacon    this.options.location.scheme = nextScheme;
144a7e47f4dSEvan Bacon    this.options.devClient = isUsingDevClient;
145a7e47f4dSEvan Bacon    for (const devServer of devServers) {
146a7e47f4dSEvan Bacon      devServer.isDevClient = isUsingDevClient;
147a7e47f4dSEvan Bacon      const urlCreator = devServer.getUrlCreator();
148a7e47f4dSEvan Bacon      urlCreator.defaults ??= {};
149a7e47f4dSEvan Bacon      urlCreator.defaults.scheme = nextScheme;
150a7e47f4dSEvan Bacon    }
151a7e47f4dSEvan Bacon
152a7e47f4dSEvan Bacon    debug(`New runtime options (runtime: ${nextMode}):`, this.options);
153a7e47f4dSEvan Bacon    return true;
154a7e47f4dSEvan Bacon  }
155a7e47f4dSEvan Bacon
1568d307f52SEvan Bacon  /** Start all dev servers. */
1578d307f52SEvan Bacon  async startAsync(startOptions: MultiBundlerStartOptions): Promise<ExpoConfig> {
1589580591fSEvan Bacon    const { exp } = getConfig(this.projectRoot, { skipSDKVersionRequirement: true });
1598d307f52SEvan Bacon
160ea99eec9SEvan Bacon    await logEventAsync('Start Project', {
1618d307f52SEvan Bacon      sdkVersion: exp.sdkVersion ?? null,
1628d307f52SEvan Bacon    });
1638d307f52SEvan Bacon
1646d6b81f9SEvan Bacon    const platformBundlers = getPlatformBundlers(exp);
1656d6b81f9SEvan Bacon
1668d307f52SEvan Bacon    // Start all dev servers...
1678d307f52SEvan Bacon    for (const { type, options } of startOptions) {
1688d307f52SEvan Bacon      const BundlerDevServerClass = await BUNDLERS[type]();
1696d6b81f9SEvan Bacon      const server = new BundlerDevServerClass(
1706d6b81f9SEvan Bacon        this.projectRoot,
1716d6b81f9SEvan Bacon        platformBundlers,
1726d6b81f9SEvan Bacon        !!options?.devClient
1736d6b81f9SEvan Bacon      );
1748d307f52SEvan Bacon      await server.startAsync(options ?? this.options);
1758d307f52SEvan Bacon      devServers.push(server);
1768d307f52SEvan Bacon    }
1778d307f52SEvan Bacon
1788d307f52SEvan Bacon    return exp;
1798d307f52SEvan Bacon  }
1808d307f52SEvan Bacon
18133643b60SEvan Bacon  async bootstrapTypeScriptAsync() {
1821117330aSMark Lawlor    const typescriptPrerequisite = await this.ensureProjectPrerequisiteAsync(
1831117330aSMark Lawlor      TypeScriptProjectPrerequisite
1841117330aSMark Lawlor    );
1851117330aSMark Lawlor
1861117330aSMark Lawlor    if (env.EXPO_NO_TYPESCRIPT_SETUP) {
18733643b60SEvan Bacon      return;
18833643b60SEvan Bacon    }
1891117330aSMark Lawlor
19033643b60SEvan Bacon    // Optionally, wait for the user to add TypeScript during the
19133643b60SEvan Bacon    // development cycle.
19233643b60SEvan Bacon    const server = devServers.find((server) => server.name === 'metro');
19333643b60SEvan Bacon    if (!server) {
19433643b60SEvan Bacon      return;
19533643b60SEvan Bacon    }
1961117330aSMark Lawlor
197*87669a95SMark Lawlor    // The dev server shouldn't wait for the typescript services
1981117330aSMark Lawlor    if (!typescriptPrerequisite) {
1991117330aSMark Lawlor      server.waitForTypeScriptAsync().then(async (success) => {
2001117330aSMark Lawlor        if (success) {
201*87669a95SMark Lawlor          server.startTypeScriptServices();
2021117330aSMark Lawlor        }
2031117330aSMark Lawlor      });
2041117330aSMark Lawlor    } else {
2051117330aSMark Lawlor      server.startTypeScriptServices();
2061117330aSMark Lawlor    }
20733643b60SEvan Bacon  }
20833643b60SEvan Bacon
2096a750d06SEvan Bacon  async watchEnvironmentVariables() {
2106a750d06SEvan Bacon    await devServers.find((server) => server.name === 'metro')?.watchEnvironmentVariables();
2116a750d06SEvan Bacon  }
2126a750d06SEvan Bacon
2138d307f52SEvan Bacon  /** Stop all servers including ADB. */
2148d307f52SEvan Bacon  async stopAsync(): Promise<void> {
21511a5a4d2SEvan Bacon    await Promise.allSettled([
2165404abc1SEvan Bacon      this.notifier?.stopObserving(),
2178d307f52SEvan Bacon      // Stop all dev servers
2188d307f52SEvan Bacon      ...devServers.map((server) => server.stopAsync()),
2198d307f52SEvan Bacon      // Stop ADB
2208d307f52SEvan Bacon      AndroidDebugBridge.getServer().stopAsync(),
22111a5a4d2SEvan Bacon    ]);
2228d307f52SEvan Bacon  }
2238d307f52SEvan Bacon}
224