18d307f52SEvan Baconimport {
28d307f52SEvan Bacon  AppJSONConfig,
38d307f52SEvan Bacon  ExpoConfig,
48d307f52SEvan Bacon  getConfig,
58d307f52SEvan Bacon  getProjectConfigDescriptionWithPaths,
68d307f52SEvan Bacon  ProjectConfig,
78d307f52SEvan Bacon} from '@expo/config';
88d307f52SEvan Baconimport chalk from 'chalk';
98d307f52SEvan Bacon
108d307f52SEvan Baconimport * as Log from '../../../log';
118d307f52SEvan Baconimport { env } from '../../../utils/env';
126d6b81f9SEvan Baconimport { getPlatformBundlers } from '../../server/platformBundlers';
138d307f52SEvan Baconimport { PrerequisiteCommandError, ProjectPrerequisite } from '../Prerequisite';
148d307f52SEvan Baconimport { ensureDependenciesAsync } from '../dependencies/ensureDependenciesAsync';
156d6b81f9SEvan Baconimport { ResolvedPackage } from '../dependencies/getMissingPackages';
168d307f52SEvan Bacon
17474a7a4bSEvan Baconconst debug = require('debug')('expo:doctor:webSupport') as typeof console.log;
18474a7a4bSEvan Bacon
198d307f52SEvan Bacon/** Ensure the project has the required web support settings. */
208d307f52SEvan Baconexport class WebSupportProjectPrerequisite extends ProjectPrerequisite {
218d307f52SEvan Bacon  /** Ensure a project that hasn't explicitly disabled web support has all the required packages for running in the browser. */
228d307f52SEvan Bacon  async assertImplementation(): Promise<void> {
238d307f52SEvan Bacon    if (env.EXPO_NO_WEB_SETUP) {
248d307f52SEvan Bacon      Log.warn('Skipping web setup: EXPO_NO_WEB_SETUP is enabled.');
258d307f52SEvan Bacon      return;
268d307f52SEvan Bacon    }
27474a7a4bSEvan Bacon    debug('Ensuring web support is setup');
288d307f52SEvan Bacon
298d307f52SEvan Bacon    const result = await this._shouldSetupWebSupportAsync();
308d307f52SEvan Bacon
318d307f52SEvan Bacon    // Ensure web packages are installed
328d307f52SEvan Bacon    await this._ensureWebDependenciesInstalledAsync({ exp: result.exp });
338d307f52SEvan Bacon  }
348d307f52SEvan Bacon
358d307f52SEvan Bacon  /** Exposed for testing. */
368d307f52SEvan Bacon  async _shouldSetupWebSupportAsync(): Promise<ProjectConfig> {
378d307f52SEvan Bacon    const config = getConfig(this.projectRoot);
388d307f52SEvan Bacon
398d307f52SEvan Bacon    // Detect if the 'web' string is purposefully missing from the platforms array.
408d307f52SEvan Bacon    if (isWebPlatformExcluded(config.rootConfig)) {
418d307f52SEvan Bacon      // Get exact config description with paths.
428d307f52SEvan Bacon      const configName = getProjectConfigDescriptionWithPaths(this.projectRoot, config);
438d307f52SEvan Bacon      throw new PrerequisiteCommandError(
448d307f52SEvan Bacon        'WEB_SUPPORT',
458d307f52SEvan Bacon        chalk`Skipping web setup: {bold "web"} is not included in the project ${configName} {bold "platforms"} array.`
468d307f52SEvan Bacon      );
478d307f52SEvan Bacon    }
488d307f52SEvan Bacon
498d307f52SEvan Bacon    return config;
508d307f52SEvan Bacon  }
518d307f52SEvan Bacon
528d307f52SEvan Bacon  /** Exposed for testing. */
538d307f52SEvan Bacon  async _ensureWebDependenciesInstalledAsync({ exp }: { exp: ExpoConfig }): Promise<boolean> {
546d6b81f9SEvan Bacon    const requiredPackages: ResolvedPackage[] = [
556d6b81f9SEvan Bacon      // use react-native-web/package.json to skip node module cache issues when the user installs
566d6b81f9SEvan Bacon      // the package and attempts to resolve the module in the same process.
5771c2e4a7SEvan Bacon      { file: 'react-native-web/package.json', pkg: 'react-native-web' },
5871c2e4a7SEvan Bacon      { file: 'react-dom/package.json', pkg: 'react-dom' },
596d6b81f9SEvan Bacon    ];
606d6b81f9SEvan Bacon
616d6b81f9SEvan Bacon    const bundler = getPlatformBundlers(exp).web;
626d6b81f9SEvan Bacon    // Only include webpack-config if bundler is webpack.
636d6b81f9SEvan Bacon    if (bundler === 'webpack') {
646d6b81f9SEvan Bacon      requiredPackages.push(
656d6b81f9SEvan Bacon        // `webpack` and `webpack-dev-server` should be installed in the `@expo/webpack-config`
666d6b81f9SEvan Bacon        {
676d6b81f9SEvan Bacon          file: '@expo/webpack-config/package.json',
686d6b81f9SEvan Bacon          pkg: '@expo/webpack-config',
696d6b81f9SEvan Bacon          dev: true,
706d6b81f9SEvan Bacon        }
716d6b81f9SEvan Bacon      );
726d6b81f9SEvan Bacon    }
736d6b81f9SEvan Bacon
748d307f52SEvan Bacon    try {
758d307f52SEvan Bacon      return await ensureDependenciesAsync(this.projectRoot, {
767b57a441SEvan Bacon        // This never seems to work when prompting, installing, and running -- instead just inform the user to run the install command and try again.
777b57a441SEvan Bacon        skipPrompt: true,
78*33643b60SEvan Bacon        isProjectMutable: false,
798d307f52SEvan Bacon        exp,
808d307f52SEvan Bacon        installMessage: `It looks like you're trying to use web support but don't have the required dependencies installed.`,
817b57a441SEvan Bacon        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.`,
826d6b81f9SEvan Bacon        requiredPackages,
838d307f52SEvan Bacon      });
848d307f52SEvan Bacon    } catch (error) {
858d307f52SEvan Bacon      // 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.
868d307f52SEvan Bacon      this.resetAssertion();
878d307f52SEvan Bacon      throw error;
888d307f52SEvan Bacon    }
898d307f52SEvan Bacon  }
908d307f52SEvan Bacon}
918d307f52SEvan Bacon
928d307f52SEvan Bacon/** Return `true` if the `web` platform is purposefully excluded from the project Expo config. */
938d307f52SEvan Baconexport function isWebPlatformExcluded(rootConfig: AppJSONConfig): boolean {
948d307f52SEvan Bacon  // Detect if the 'web' string is purposefully missing from the platforms array.
958d307f52SEvan Bacon  const isWebExcluded =
968d307f52SEvan Bacon    Array.isArray(rootConfig.expo?.platforms) &&
978d307f52SEvan Bacon    !!rootConfig.expo?.platforms.length &&
988d307f52SEvan Bacon    !rootConfig.expo?.platforms.includes('web');
998d307f52SEvan Bacon  return isWebExcluded;
1008d307f52SEvan Bacon}
101