1import { ExpoConfig, getConfig } from '@expo/config';
2import assert from 'assert';
3
4import * as Log from '../../log';
5import { FileNotifier } from '../../utils/FileNotifier';
6import { logEvent } from '../../utils/analytics/rudderstackClient';
7import { ProjectPrerequisite } from '../doctor/Prerequisite';
8import * as AndroidDebugBridge from '../platforms/android/adb';
9import { BundlerDevServer, BundlerStartOptions } from './BundlerDevServer';
10
11export type MultiBundlerStartOptions = {
12  type: keyof typeof BUNDLERS;
13  options?: BundlerStartOptions;
14}[];
15
16const devServers: BundlerDevServer[] = [];
17
18const BUNDLERS = {
19  webpack: () =>
20    require('./webpack/WebpackBundlerDevServer')
21      .WebpackBundlerDevServer as typeof import('./webpack/WebpackBundlerDevServer').WebpackBundlerDevServer,
22  metro: () =>
23    require('./metro/MetroBundlerDevServer')
24      .MetroBundlerDevServer as typeof import('./metro/MetroBundlerDevServer').MetroBundlerDevServer,
25};
26
27/** Manages interacting with multiple dev servers. */
28export class DevServerManager {
29  private projectPrerequisites: ProjectPrerequisite[] = [];
30
31  constructor(
32    public projectRoot: string,
33    /** Keep track of the original CLI options for bundlers that are started interactively. */
34    public options: BundlerStartOptions
35  ) {
36    this.watchBabelConfig();
37  }
38
39  private watchBabelConfig() {
40    const notifier = new FileNotifier(this.projectRoot, [
41      './babel.config.js',
42      './babel.config.json',
43      './.babelrc.json',
44      './.babelrc',
45      './.babelrc.js',
46    ]);
47
48    notifier.startObserving();
49
50    return notifier;
51  }
52
53  /** Lazily load and assert a project-level prerequisite. */
54  async ensureProjectPrerequisiteAsync(PrerequisiteClass: typeof ProjectPrerequisite) {
55    let prerequisite = this.projectPrerequisites.find(
56      (prerequisite) => prerequisite instanceof PrerequisiteClass
57    );
58    if (!prerequisite) {
59      prerequisite = new PrerequisiteClass(this.projectRoot);
60      this.projectPrerequisites.push(prerequisite);
61    }
62    await prerequisite.assertAsync();
63  }
64
65  /**
66   * Sends a message over web sockets to all connected devices,
67   * does nothing when the dev server is not running.
68   *
69   * @param method name of the command. In RN projects `reload`, and `devMenu` are available. In Expo Go, `sendDevCommand` is available.
70   * @param params extra event info to send over the socket.
71   */
72  broadcastMessage(method: 'reload' | 'devMenu' | 'sendDevCommand', params?: Record<string, any>) {
73    devServers.forEach((server) => {
74      server.broadcastMessage(method, params);
75    });
76  }
77
78  /** Get the port for the dev server (either Webpack or Metro) that is hosting code for React Native runtimes. */
79  getNativeDevServerPort() {
80    const server = devServers.find((server) => server.isTargetingNative());
81    return server?.getInstance()?.location.port ?? null;
82  }
83
84  /** Get the first server that targets web. */
85  getWebDevServer() {
86    const server = devServers.find((server) => server.isTargetingWeb());
87    return server ?? null;
88  }
89
90  getDefaultDevServer(): BundlerDevServer {
91    // Return the first native dev server otherwise return the first dev server.
92    const server = devServers.find((server) => server.isTargetingNative());
93    const defaultServer = server ?? devServers[0];
94    assert(defaultServer, 'No dev servers are running');
95    return defaultServer;
96  }
97
98  async ensureWebDevServerRunningAsync() {
99    const [server] = devServers.filter((server) => server.isTargetingWeb());
100    if (server) {
101      return;
102    }
103    Log.debug('Starting webpack dev server');
104    return this.startAsync([
105      {
106        type: 'webpack',
107        options: this.options,
108      },
109    ]);
110  }
111
112  /** Start all dev servers. */
113  async startAsync(startOptions: MultiBundlerStartOptions): Promise<ExpoConfig> {
114    const { exp } = getConfig(this.projectRoot);
115
116    logEvent('Start Project', {
117      sdkVersion: exp.sdkVersion ?? null,
118    });
119
120    // Start all dev servers...
121    for (const { type, options } of startOptions) {
122      const BundlerDevServerClass = await BUNDLERS[type]();
123      const server = new BundlerDevServerClass(this.projectRoot, !!options?.devClient);
124      await server.startAsync(options ?? this.options);
125      devServers.push(server);
126    }
127
128    return exp;
129  }
130
131  /** Stop all servers including ADB. */
132  async stopAsync(): Promise<void> {
133    await Promise.allSettled([
134      // Stop all dev servers
135      ...devServers.map((server) => server.stopAsync()),
136      // Stop ADB
137      AndroidDebugBridge.getServer().stopAsync(),
138    ]);
139  }
140}
141