1import {
2  AppJSONConfig,
3  ExpoConfig,
4  getConfig,
5  getProjectConfigDescriptionWithPaths,
6  ProjectConfig,
7} from '@expo/config';
8import chalk from 'chalk';
9
10import * as Log from '../../../log';
11import { env } from '../../../utils/env';
12import { PrerequisiteCommandError, ProjectPrerequisite } from '../Prerequisite';
13import { ensureDependenciesAsync } from '../dependencies/ensureDependenciesAsync';
14
15const debug = require('debug')('expo:doctor:webSupport') as typeof console.log;
16
17/** Ensure the project has the required web support settings. */
18export class WebSupportProjectPrerequisite extends ProjectPrerequisite {
19  /** Ensure a project that hasn't explicitly disabled web support has all the required packages for running in the browser. */
20  async assertImplementation(): Promise<void> {
21    if (env.EXPO_NO_WEB_SETUP) {
22      Log.warn('Skipping web setup: EXPO_NO_WEB_SETUP is enabled.');
23      return;
24    }
25    debug('Ensuring web support is setup');
26
27    const result = await this._shouldSetupWebSupportAsync();
28
29    // Ensure web packages are installed
30    await this._ensureWebDependenciesInstalledAsync({ exp: result.exp });
31  }
32
33  /** Exposed for testing. */
34  async _shouldSetupWebSupportAsync(): Promise<ProjectConfig> {
35    const config = getConfig(this.projectRoot);
36
37    // Detect if the 'web' string is purposefully missing from the platforms array.
38    if (isWebPlatformExcluded(config.rootConfig)) {
39      // Get exact config description with paths.
40      const configName = getProjectConfigDescriptionWithPaths(this.projectRoot, config);
41      throw new PrerequisiteCommandError(
42        'WEB_SUPPORT',
43        chalk`Skipping web setup: {bold "web"} is not included in the project ${configName} {bold "platforms"} array.`
44      );
45    }
46
47    return config;
48  }
49
50  /** Exposed for testing. */
51  async _ensureWebDependenciesInstalledAsync({ exp }: { exp: ExpoConfig }): Promise<boolean> {
52    try {
53      return await ensureDependenciesAsync(this.projectRoot, {
54        // This never seems to work when prompting, installing, and running -- instead just inform the user to run the install command and try again.
55        skipPrompt: true,
56        exp,
57        installMessage: `It looks like you're trying to use web support but don't have the required dependencies installed.`,
58        warningMessage: chalk`If you're not using web, please ensure you remove the {bold "web"} string from the platforms array in the project Expo config.`,
59        requiredPackages: [
60          // use react-native-web/package.json to skip node module cache issues when the user installs
61          // the package and attempts to resolve the module in the same process.
62          { file: 'react-native-web/package.json', pkg: 'react-native-web', version: '~0.17.1' },
63          { file: 'react-dom/package.json', pkg: 'react-dom', version: '^17.0.1' },
64          // `webpack` and `webpack-dev-server` should be installed in the `@expo/webpack-config`
65          {
66            file: '@expo/webpack-config/package.json',
67            pkg: '@expo/webpack-config',
68            version: '~0.16.2',
69            dev: true,
70          },
71        ],
72      });
73    } catch (error) {
74      // Reset the cached check so we can re-run the check if the user re-runs the command by pressing 'w' in the Terminal UI.
75      this.resetAssertion();
76      throw error;
77    }
78  }
79}
80
81/** Return `true` if the `web` platform is purposefully excluded from the project Expo config. */
82export function isWebPlatformExcluded(rootConfig: AppJSONConfig): boolean {
83  // Detect if the 'web' string is purposefully missing from the platforms array.
84  const isWebExcluded =
85    Array.isArray(rootConfig.expo?.platforms) &&
86    !!rootConfig.expo?.platforms.length &&
87    !rootConfig.expo?.platforms.includes('web');
88  return isWebExcluded;
89}
90