import type Log from '@expo/bunyan' ; import { attachInspectorProxy , createDevServerMiddleware , LogReporter , MessageSocket , } from '@expo/dev-server' ; import { createSymbolicateMiddleware } from '@expo/dev-server/build/webpack/symbolicateMiddleware' ; import * as devcert from '@expo/devcert' ; import openBrowserAsync from 'better-opn' ; import chalk from 'chalk' ; import fs from 'fs-extra' ; import getenv from 'getenv' ; import http from 'http' ; import * as path from 'path' ; import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages' ; import webpack from 'webpack' ; import WebpackDevServer from 'webpack-dev-server' ; import { choosePortAsync , ExpoUpdatesManifestHandler , ip , Logger , ManifestHandler , ProjectSettings , ProjectUtils , UrlUtils , WebpackEnvironment , XDLError , } from './internal' ; const WEBPACK_LOG_TAG = 'expo' ; type DevServer = WebpackDevServer | http . Server ; let webpackDevServerInstance = null ; let webpackServerPort = null ; interface WebpackSettings { url : string ; server : DevServer ; port : number ; protocol : 'http' | 'https' ; host ? : string ; } type CLIWebOptions = { dev ? : boolean ; clear ? : boolean ; pwa ? : boolean ; nonInteractive ? : boolean ; port ? : number ; onWebpackFinished ? : ( error ? : Error ) => void ; forceManifestType ? : 'classic' | 'expo-updates' ; } ; type BundlingOptions = { dev ? : boolean ; clear ? : boolean ; port ? : number ; pwa ? : boolean ; isImageEditingEnabled ? : boolean ; webpackEnv ? : object ; mode ? : 'development' | 'production' | 'test' | 'none' ; https ? : boolean ; nonInteractive ? : boolean ; onWebpackFinished ? : ( error ? : Error ) => void ; } ; type WebpackConfiguration = webpack . Configuration ; export type WebEnvironment = { projectRoot : string ; isImageEditingEnabled : boolean ; pwa : boolean ; mode : 'development' | 'production' | 'test' | 'none' ; https : boolean ; logger : Log ; } ; let customMessageSocketBroadcaster ; async function clearWebCacheAsync ( projectRoot , mode ) { const cacheFolder = path . join ( projectRoot , '.expo' , 'web' , 'cache' , mode ) ; ProjectUtils . logInfo ( projectRoot , WEBPACK_LOG_TAG , chalk . dim ( `Clearing ${ mode } cache directory...` ) ) ; try { await fs . remove ( cacheFolder ) ; } catch { } } const _isTargetingNative = [ 'ios' , 'android' ] . includes ( process . env . EXPO_WEBPACK_PLATFORM || '' ) ; export function isTargetingNative ( ) { return _isTargetingNative ; } export type WebpackDevServerResults = { server : DevServer ; location : Omit < WebpackSettings , 'server' > ; messageSocket : MessageSocket ; } ; export async function broadcastMessage ( message , data ? ) { if ( ! webpackDevServerInstance || ! ( webpackDevServerInstance instanceof WebpackDevServer ) ) { return ; } if ( customMessageSocketBroadcaster ) { customMessageSocketBroadcaster ( message , data ) ; return ; } if ( message !== 'reload' ) { return ; } const hackyConvertedMessage = message === 'reload' ? 'content-changed' : message ; webpackDevServerInstance . sockWrite ( webpackDevServerInstance . sockets , hackyConvertedMessage , data ) ; } function createNativeDevServerMiddleware ( projectRoot , { port , compiler , forceManifestType , } ) { if ( ! isTargetingNative ( ) ) { return null ; } const nativeMiddleware = createDevServerMiddleware ( { logger : ProjectUtils . getLogger ( projectRoot ) , port , watchFolders : [ projectRoot ] } ) ; const useExpoUpdatesManifest = forceManifestType === 'expo-updates' ; const middleware = useExpoUpdatesManifest ? ExpoUpdatesManifestHandler . getManifestHandler ( projectRoot ) : ManifestHandler . getManifestHandler ( projectRoot ) ; nativeMiddleware . middleware . use ( middleware ) . use ( '/symbolicate' , createSymbolicateMiddleware ( { projectRoot , compiler , logger : nativeMiddleware . logger } ) ) ; return nativeMiddleware ; } function attachNativeDevServerMiddlewareToDevServer ( projectRoot , { server , middleware , attachToServer , logger , } ) { const { messageSocket , debuggerProxy , eventsSocket } = attachToServer ( server ) ; customMessageSocketBroadcaster = messageSocket . broadcast ; const logReporter = new LogReporter ( logger ) ; logReporter . reportEvent = eventsSocket . reportEvent ; const { inspectorProxy } = attachInspectorProxy ( projectRoot , { middleware , server } ) ; return { messageSocket , eventsSocket , debuggerProxy , logReporter , inspectorProxy } ; } export async function startAsync ( projectRoot , options = { } ) { await stopAsync ( projectRoot ) ; if ( webpackDevServerInstance ) { ProjectUtils . logError ( projectRoot , WEBPACK_LOG_TAG , chalk . red ( `Webpack is already running.` ) ) ; return null ; } const fullOptions = transformCLIOptions ( options ) ; const env = await getWebpackConfigEnvFromBundlingOptionsAsync ( projectRoot , fullOptions ) ; if ( fullOptions . clear ) { await clearWebCacheAsync ( projectRoot , env . mode ) ; } if ( env . https ) { if ( ! process . env . SSL_CRT_FILE || ! process . env . SSL_KEY_FILE ) { const ssl = await getSSLCertAsync ( { name : 'localhost' , directory : projectRoot } ) ; if ( ssl ) { process . env . SSL_CRT_FILE = ssl . certPath ; process . env . SSL_KEY_FILE = ssl . keyPath ; } } } const config = await loadConfigAsync ( env ) ; const port = await getAvailablePortAsync ( { projectRoot , defaultPort : options . port } ) ; webpackServerPort = port ; ProjectUtils . logInfo ( projectRoot , WEBPACK_LOG_TAG , `Starting Webpack on port ${ webpackServerPort } in ${ chalk . underline ( env . mode ) } mode.` ) ; const protocol = env . https ? 'https' : 'http' ; if ( isTargetingNative ( ) ) { await ProjectSettings . setPackagerInfoAsync ( projectRoot , { expoServerPort : webpackServerPort , packagerPort : webpackServerPort } ) ; } const compiler = webpack ( config ) ; const nativeMiddleware = createNativeDevServerMiddleware ( projectRoot , { port , compiler , forceManifestType : options . forceManifestType } ) ; const originalBefore = config . devServer ! . before ! . bind ( config . devServer ! . before ) ; config . devServer ! . before = ( app , server , compiler ) => { originalBefore ( app , server , compiler ) ; if ( nativeMiddleware ?. middleware ) { app . use ( nativeMiddleware . middleware ) ; } } ; const server = new WebpackDevServer ( compiler , config . devServer ) ; server . listen ( port , WebpackEnvironment . HOST , function ( this , error ) { if ( nativeMiddleware ) { attachNativeDevServerMiddlewareToDevServer ( projectRoot , { server : this , ... nativeMiddleware } ) ; } if ( error ) { ProjectUtils . logError ( projectRoot , WEBPACK_LOG_TAG , error . message ) ; } if ( typeof options . onWebpackFinished === 'function' ) { options . onWebpackFinished ( error ) ; } } ) ; webpackDevServerInstance = server ; await ProjectSettings . setPackagerInfoAsync ( projectRoot , { webpackServerPort } ) ; const host = ip . address ( ) ; const url = `${ protocol }://${ host }:${ port }` ; const originalClose = server . close . bind ( server ) ; server . close = ( callback ? ) => { return originalClose ( ( err ? ) => { ProjectSettings . setPackagerInfoAsync ( projectRoot , { webpackServerPort : null } ) . finally ( ( ) => { callback ?. ( err ) ; webpackDevServerInstance = null ; webpackServerPort = null ; } ) ; } ) ; } ; return { server , location : { url , port , protocol , host } , messageSocket : { broadcast : broadcastMessage } } ; } export async function stopAsync ( projectRoot ) { if ( webpackDevServerInstance ) { await new Promise ( res => { if ( webpackDevServerInstance ) { ProjectUtils . logInfo ( projectRoot , WEBPACK_LOG_TAG , '\u203A Stopping Webpack server' ) ; webpackDevServerInstance . close ( res ) ; } } ) ; } } export async function openAsync ( projectRoot , options ? ) { if ( ! webpackDevServerInstance ) { await startAsync ( projectRoot , options ) ; } await openProjectAsync ( projectRoot ) ; } async function compileWebAppAsync ( projectRoot , compiler ) { const { warnings } = await new Promise ( ( resolve , reject ) => compiler . run ( ( error , stats ) => { let messages ; if ( error ) { if ( ! error . message ) { return reject ( error ) ; } messages = formatWebpackMessages ( { errors : [ error . message ] , warnings : [ ] , _showErrors : true , _showWarnings : true } ) ; } else { messages = formatWebpackMessages ( stats . toJson ( { all : false , warnings : true , errors : true } ) ) ; } if ( messages . errors . length ) { if ( messages . errors . length > 1 ) { messages . errors . length = 1 ; } return reject ( new XDLError ( 'WEBPACK_BUNDLE' , messages . errors . join ( '\n\n' ) ) ) ; } if ( getenv . boolish ( 'EXPO_WEB_BUILD_STRICT' , false ) && getenv . boolish ( 'CI' , false ) && messages . warnings . length ) { ProjectUtils . logWarning ( projectRoot , WEBPACK_LOG_TAG , chalk . yellow ( '\nTreating warnings as errors because `process.env.CI = true` and `process.env.EXPO_WEB_BUILD_STRICT = true`. \n' + 'Most CI servers set it automatically.\n' ) ) ; return reject ( new XDLError ( 'WEBPACK_BUNDLE' , messages . warnings . join ( '\n\n' ) ) ) ; } resolve ( { warnings : messages . warnings } ) ; } ) ) ; return { warnings } ; } async function bundleWebAppAsync ( projectRoot , config ) { const compiler = webpack ( config ) ; try { const { warnings } = await compileWebAppAsync ( projectRoot , compiler ) ; if ( warnings . length ) { ProjectUtils . logWarning ( projectRoot , WEBPACK_LOG_TAG , chalk . yellow ( 'Compiled with warnings.\n' ) ) ; ProjectUtils . logWarning ( projectRoot , WEBPACK_LOG_TAG , warnings . join ( '\n\n' ) ) ; } else { ProjectUtils . logInfo ( projectRoot , WEBPACK_LOG_TAG , chalk . green ( 'Compiled successfully.\n' ) ) ; } } catch ( error ) { ProjectUtils . logError ( projectRoot , WEBPACK_LOG_TAG , chalk . red ( 'Failed to compile.\n' ) ) ; throw error ; } } export async function bundleAsync ( projectRoot , options ? ) { const fullOptions = transformCLIOptions ( { ... options } ) ; const env = await getWebpackConfigEnvFromBundlingOptionsAsync ( projectRoot , { ... fullOptions , mode : 'production' } ) ; if ( typeof env . offline !== 'undefined' ) { throw new Error ( 'offline support must be added manually: https://expo.fyi/enabling-web-service-workers' ) ; } if ( fullOptions . clear ) { await clearWebCacheAsync ( projectRoot , env . mode ) ; } const config = await loadConfigAsync ( env ) ; await bundleWebAppAsync ( projectRoot , config ) ; } param projectRoot export async function getUrlAsync ( projectRoot ) { if ( ! webpackDevServerInstance ) { return null ; } const host = ip . address ( ) ; const protocol = await getProtocolAsync ( projectRoot ) ; return `${ protocol }://${ host }:${ webpackServerPort }` ; } async function getProtocolAsync ( projectRoot ) { const { https } = await ProjectSettings . readAsync ( projectRoot ) ; return https === true ? 'https' : 'http' ; } async function getAvailablePortAsync ( options ) { try { const defaultPort = 'defaultPort' in options && options . defaultPort ? options . defaultPort : WebpackEnvironment . DEFAULT_PORT ; const port = await choosePortAsync ( options . projectRoot , { defaultPort , host : 'host' in options && options . host ? options . host : WebpackEnvironment . HOST } ) ; if ( ! port ) { throw new Error ( `Port ${ defaultPort } not available.` ) ; } return port ; } catch ( error ) { throw new XDLError ( 'NO_PORT_FOUND' , error . message ) ; } } function setMode ( mode ) { process . env . BABEL_ENV = mode ; process . env . NODE_ENV = mode ; } function validateBoolOption ( name , value , defaultValue ) { if ( typeof value === 'undefined' ) { value = defaultValue ; } if ( typeof value !== 'boolean' ) { throw new XDLError ( 'WEBPACK_INVALID_OPTION' , `'${ name }' option must be a boolean.` ) ; } return value ; } function transformCLIOptions ( options ) { return { ... options , isImageEditingEnabled : options . pwa } ; } async function applyOptionsToProjectSettingsAsync ( projectRoot , options ) { const newSettings = { } ; if ( typeof options . https === 'boolean' ) { newSettings . https = options . https ; } if ( Object . keys ( newSettings ) . length ) { await ProjectSettings . setAsync ( projectRoot , newSettings ) ; } return await ProjectSettings . readAsync ( projectRoot ) ; } async function getWebpackConfigEnvFromBundlingOptionsAsync ( projectRoot , options ) { const { dev , https } = await applyOptionsToProjectSettingsAsync ( projectRoot , options ) ; const mode = typeof options . mode === 'string' ? options . mode : dev ? 'development' : 'production' ; const isImageEditingEnabled = validateBoolOption ( 'isImageEditingEnabled' , options . isImageEditingEnabled , true ) ; return { projectRoot , pwa : isImageEditingEnabled , logger : ProjectUtils . getLogger ( projectRoot ) , isImageEditingEnabled , mode , https , ... ( options . webpackEnv || { } ) } ; } async function getSSLCertAsync ( { name , directory , } ) { console . log ( chalk . magenta `Ensuring auto SSL certificate is created (you might need to re-run with sudo)` ) ; try { const result = await devcert . certificateFor ( name ) ; if ( result ) { const { key , cert } = result ; const folder = path . join ( directory , '.expo' , 'web' , 'development' , 'ssl' ) ; await fs . ensureDir ( folder ) ; const keyPath = path . join ( folder , `key-${ name }.pem` ) ; await fs . writeFile ( keyPath , key ) ; const certPath = path . join ( folder , `cert-${ name }.pem` ) ; await fs . writeFile ( certPath , cert ) ; return { keyPath , certPath } ; } return result ; } catch ( error ) { console . log ( `Error creating SSL certificates: ${ error }` ) ; } return false ; } function applyEnvironmentVariables ( config ) { if ( WebpackEnvironment . isDebugModeEnabled ( ) && config . mode === 'production' ) { console . log ( chalk . bgYellow . black ( 'Bundling the project in debug mode.' ) ) ; const output = config . output || { } ; const optimization = config . optimization || { } ; output . devtoolLineToLine = true ; output . pathinfo = true ; optimization . namedModules = true ; optimization . namedChunks = true ; optimization . moduleIds = 'named' ; optimization . chunkIds = 'named' ; if ( optimization . splitChunks ) { optimization . splitChunks . name = true ; } Object . assign ( config , { output , optimization } ) ; } return config ; } async function loadConfigAsync ( env , argv ? ) { setMode ( env . mode ) ; const projectWebpackConfig = path . resolve ( env . projectRoot , 'webpack.config.js' ) ; let config ; if ( fs . existsSync ( projectWebpackConfig ) ) { const webpackConfig = require ( projectWebpackConfig ) ; if ( typeof webpackConfig === 'function' ) { config = await webpackConfig ( env , argv ) ; } else { config = webpackConfig ; } } else { const loadDefaultConfigAsync = require ( '@expo/webpack-config' ) ; config = await loadDefaultConfigAsync ( env , argv ) ; } return applyEnvironmentVariables ( config ) ; } async function openProjectAsync ( projectRoot ) { try { const url = await UrlUtils . constructWebAppUrlAsync ( projectRoot , { hostType : 'localhost' } ) ; if ( ! url ) { throw new Error ( 'Webpack Dev Server is not running' ) ; } openBrowserAsync ( url ) ; return { success : true , url } ; } catch ( e ) { Logger . global . error ( `Couldn't start project on web: ${ e . message }` ) ; return { success : false , error : e } ; } }