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