1const path = require('path'); 2const webpack = require('webpack'); 3const PnpWebpackPlugin = require('pnp-webpack-plugin'); 4const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 5const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); 6const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 7const HtmlWebpackPlugin = require('html-webpack-plugin'); 8const ManifestPlugin = require('webpack-manifest-plugin'); 9const pckg = require('./package.json'); 10 11const absolutePath = location => path.resolve(__dirname, location); 12 13const locations = { 14 // Shouldn't change 15 root: absolutePath('.'), 16 output: absolutePath('web'), 17 rootHtml: absolutePath('web/index.html'), 18 packageJson: absolutePath('package.json'), 19 appMain: absolutePath(pckg.main), 20 21 // TODO: Bacon: Only use this in expo/apps/ 22 modules: absolutePath('../../node_modules'), 23}; 24 25function getAppManifest() { 26 const nativeAppManifest = require(absolutePath('./app.json')); 27 28 if (nativeAppManifest && nativeAppManifest.expo) { 29 const { expo } = nativeAppManifest; 30 const PWAManifest = require(absolutePath('./web/manifest.json')); 31 const web = PWAManifest || {}; 32 33 return { 34 // facebookScheme 35 // facebookAppId 36 // facebookDisplayName 37 name: expo.name, 38 description: expo.description, 39 slug: expo.slug, 40 sdkVersion: expo.sdkVersion, 41 version: expo.version, 42 githubUrl: expo.githubUrl, 43 orientation: expo.orientation, 44 primaryColor: expo.primaryColor, 45 privacy: expo.privacy, 46 icon: expo.icon, 47 scheme: expo.scheme, 48 notification: expo.notification, 49 splash: expo.splash, 50 androidShowExponentNotificationInShellApp: expo.androidShowExponentNotificationInShellApp, 51 web, 52 }; 53 } 54 return {}; 55} 56const environment = process.env.NODE_ENV || 'development'; 57const __DEV__ = environment !== 'production'; 58 59const REACT_APP = /^REACT_APP_/i; 60 61const publicUrl = ''; 62 63function getClientEnvironment(publicUrl) { 64 let processEnv = Object.keys(process.env) 65 .filter(key => REACT_APP.test(key)) 66 .reduce( 67 (env, key) => { 68 env[key] = JSON.stringify(process.env[key]); 69 return env; 70 }, 71 { 72 // Useful for determining whether we’re running in production mode. 73 // Most importantly, it switches React into the correct mode. 74 NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'), 75 76 // Useful for resolving the correct path to static assets in `public`. 77 // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />. 78 // This should only be used as an escape hatch. Normally you would put 79 // images into the root folder and `import` them in code to get their paths. 80 PUBLIC_URL: JSON.stringify(publicUrl), 81 82 // Surface the manifest for use in expo-constants 83 APP_MANIFEST: JSON.stringify(getAppManifest()), 84 } 85 ); 86 return { 87 'process.env': processEnv, 88 __DEV__, 89 }; 90} 91 92const env = getClientEnvironment(publicUrl); 93 94const includeModule = module => { 95 return path.resolve(locations.modules, module); 96}; 97 98// Only compile files from react-native, and expo libraries. 99const includeModulesThatContainPaths = [ 100 'node_modules/react-native', 101 'node_modules/react-navigation', 102 'node_modules/expo', 103 'node_modules/@react', 104 'node_modules/@expo/', 105 // Special case for this app 106 'apps/test-suite', 107]; 108 109const babelLoaderConfiguration = { 110 test: /\.jsx?$/, 111 include(inputPath) { 112 for (const option of includeModulesThatContainPaths) { 113 if (inputPath.includes(option)) { 114 return inputPath; 115 } 116 } 117 return null; 118 }, 119 use: { 120 loader: 'babel-loader', 121 options: { 122 babelrc: false, 123 }, 124 }, 125}; 126 127// This is needed for webpack to import static images in JavaScript files. 128const imageLoaderConfiguration = { 129 test: /\.(gif|jpe?g|png|svg)$/, 130 use: { 131 loader: 'url-loader', 132 options: { 133 limit: 10000, 134 name: '[name].[ext]', 135 }, 136 }, 137}; 138 139// This is needed for loading css 140const cssLoaderConfiguration = { 141 test: /\.css$/, 142 use: ['style-loader', 'css-loader'], 143}; 144 145const ttfLoaderConfiguration = { 146 test: /\.ttf$/, 147 use: [ 148 { 149 loader: 'file-loader', 150 options: { 151 name: './fonts/[hash].[ext]', 152 }, 153 }, 154 ], 155 include: [ 156 locations.root, 157 includeModule('react-native-vector-icons'), 158 includeModule('@expo/vector-icons'), 159 ], 160}; 161 162const htmlLoaderConfiguration = { 163 test: /\.html$/, 164 use: ['html-loader'], 165 include: [absolutePath('./assets')], 166}; 167 168const mediaLoaderConfiguration = { 169 test: /\.(mov|mp4|mp3|wav|webm|db)$/, 170 use: [ 171 { 172 loader: 'file-loader', 173 options: { 174 name: '[path][name].[ext]', 175 }, 176 }, 177 ], 178}; 179 180// This method intercepts modules being referenced in react-native 181// and redirects them to web friendly versions in expo. 182function getWebModule(initialRoot, moduleName) { 183 return function(res) { 184 if (res.context.includes('node_modules/react-native/')) { 185 res.request = includeModule(initialRoot + moduleName); 186 } 187 }; 188} 189 190function useWebModule(modulePathToHiJack, redirectPath, initialRoot = 'expo/build/web/') { 191 return new webpack.NormalModuleReplacementPlugin( 192 new RegExp(modulePathToHiJack), 193 getWebModule(initialRoot, redirectPath) 194 ); 195} 196 197const publicPath = '/'; 198 199module.exports = { 200 mode: 'development', 201 devtool: 'cheap-module-source-map', 202 203 entry: [require.resolve('react-dev-utils/webpackHotDevClient'), locations.appMain], 204 // configures where the build ends up 205 output: { 206 path: locations.output, 207 pathinfo: true, 208 filename: 'bundle.js', 209 // There are also additional JS chunk files if you use code splitting. 210 chunkFilename: '[name].chunk.js', 211 // This is the URL that app is served from. We use "/" in development. 212 publicPath, 213 // Point sourcemap entries to original disk location (format as URL on Windows) 214 devtoolModuleFilenameTemplate: info => 215 path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'), 216 }, 217 optimization: { 218 runtimeChunk: true, 219 }, 220 devServer: { 221 progress: true, 222 historyApiFallback: true, 223 compress: true, 224 disableHostCheck: true, 225 contentBase: locations.output, 226 inline: true, 227 }, 228 module: { 229 rules: [ 230 { parser: { requireEnsure: false } }, 231 232 htmlLoaderConfiguration, 233 babelLoaderConfiguration, 234 cssLoaderConfiguration, 235 imageLoaderConfiguration, 236 ttfLoaderConfiguration, 237 mediaLoaderConfiguration, 238 ], 239 }, 240 plugins: [ 241 // Generates an `index.html` file with the <script> injected. 242 new HtmlWebpackPlugin({ 243 inject: true, 244 template: locations.rootHtml, 245 filename: locations.rootHtml, 246 title: pckg.name, 247 }), 248 new InterpolateHtmlPlugin(HtmlWebpackPlugin, { 249 PUBLIC_URL: publicUrl, 250 }), 251 252 new webpack.HotModuleReplacementPlugin(), 253 254 new CaseSensitivePathsPlugin(), 255 256 // Moment.js is an extremely popular library that bundles large locale files 257 // by default due to how Webpack interprets its code. This is a practical 258 // solution that requires the user to opt into importing specific locales. 259 // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack 260 // You can remove this if you don't use Moment.js: 261 /* expo-localization uses `moment/timezone` */ 262 new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 263 // Generate a manifest file which contains a mapping of all asset filenames 264 // to their corresponding output file so that tools can pick it up without 265 // having to parse `index.html`. 266 new ManifestPlugin({ 267 fileName: 'asset-manifest.json', 268 publicPath, 269 }), 270 271 new webpack.DefinePlugin(env), 272 273 useWebModule('Platform', 'Utilities/Platform'), 274 useWebModule('Performance/Systrace', 'Performance/Systrace'), 275 useWebModule('HMRLoadingView', 'Utilities/HMRLoadingView'), 276 useWebModule('RCTNetworking', 'Network/RCTNetworking'), 277 ], 278 resolve: { 279 symlinks: false, 280 extensions: ['.web.js', '.js', '.jsx', '.json'], 281 alias: { 282 /* Alias direct react-native imports to react-native-web */ 283 'react-native$': 'react-native-web', 284 /* Add polyfills for modules that react-native-web doesn't support */ 285 'react-native/Libraries/Image/AssetSourceResolver$': 286 'expo/build/web/Image/AssetSourceResolver', 287 'react-native/Libraries/Image/assetPathUtils$': 'expo/build/web/Image/assetPathUtils', 288 'react-native/Libraries/Image/resolveAssetSource$': 'expo/build/web/Image/resolveAssetSource', 289 }, 290 plugins: [ 291 // Adds support for installing with Plug'n'Play, leading to faster installs and adding 292 // guards against forgotten dependencies and such. 293 PnpWebpackPlugin, 294 // Prevents users from importing files from outside of node_modules/. 295 // This often causes confusion because we only process files within the root folder with babel. 296 // To fix this, we prevent you from importing files out of the root folder -- if you'd like to, 297 // please link the files into your node_modules/ and let module-resolution kick in. 298 // Make sure your source files are compiled, as they will not be processed in any way. 299 new ModuleScopePlugin(locations.output, [locations.packageJson]), 300 ], 301 }, 302 resolveLoader: { 303 plugins: [ 304 // Also related to Plug'n'Play, but this time it tells Webpack to load its loaders 305 // from the current package. 306 PnpWebpackPlugin.moduleLoader(module), 307 ], 308 }, 309 // Some libraries import Node modules but don't use them in the browser. 310 // Tell Webpack to provide empty mocks for them so importing them works. 311 node: { 312 dgram: 'empty', 313 fs: 'empty', 314 net: 'empty', 315 tls: 'empty', 316 child_process: 'empty', 317 }, 318 // Turn off performance processing because we utilize 319 // our own hints via the FileSizeReporter 320 performance: false, 321}; 322