xref: /expo/apps/test-suite/webpack.config.js (revision ef951f14)
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