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