import CaseSensitivePathsPlugin from "case-sensitive-paths-webpack-plugin"; import CompressionPlugin from "compression-webpack-plugin"; import HtmlWebpackPlugin, { Options } from "html-webpack-plugin"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; import path from "path"; import InterpolateHtmlPlugin from "react-dev-utils/InterpolateHtmlPlugin"; import WatchMissingNodeModulesPlugin from "react-dev-utils/WatchMissingNodeModulesPlugin"; import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; import UglifyJsPlugin from "uglifyjs-webpack-plugin"; import webpack, { Configuration } from "webpack"; import ManifestPlugin from "webpack-manifest-plugin"; import PublicURIWebpackPlugin from "./plugins/PublicURIWebpackPlugin"; import paths from "./paths"; interface CreateWebpackConfig { publicPath?: string; publicURL?: string; env?: Record; disableSourcemaps?: boolean; appendPlugins?: any[]; } export default function createWebpackConfig({ publicPath = "/", publicURL = "", env = process.env as Record, appendPlugins = [], disableSourcemaps, }: CreateWebpackConfig = {}): Configuration[] { const envStringified = { "process.env": Object.keys(env).reduce>( (result, key) => { result[key] = JSON.stringify(env[key]); return result; }, {} ), }; const isProduction = env.NODE_ENV === "production"; /** * ifProduction will only include the nodes if we're in production mode. */ const ifProduction = isProduction ? (...nodes: T[]) => nodes : (...nodes: T[]) => []; const htmlWebpackConfig: Options = { minify: isProduction && { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, }; const styleLoader = { loader: require.resolve("style-loader"), options: { hmr: !isProduction, }, }; const localesOptions = { pathToLocales: paths.appLocales, // Default locale if non could be negotiated. defaultLocale: "en-US", // Fallback locale if a translation was not found. // If not set, will use the text that is already // in the code base. fallbackLocale: "en-US", // Common fluent files are always included in the locale bundles. commonFiles: ["framework.ftl", "common.ftl"], // Locales that come with the main bundle. Others are loaded on demand. bundled: ["en-US"], // All available locales can be loadable on demand. // To restrict available locales set: // availableLocales: ["en-US"], }; const additionalPlugins = isProduction ? [ // Minify the code. new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false, // Disabled because of an issue with Uglify breaking seemingly valid code: // https://github.com/facebookincubator/create-react-app/issues/2376 // Pending further investigation: // https://github.com/mishoo/UglifyJS2/issues/2011 comparisons: false, }, mangle: { safari10: true, }, output: { comments: false, // Turned on because emoji and regex is not minified properly using default // https://github.com/facebookincubator/create-react-app/issues/2488 ascii_only: true, }, }, sourceMap: !disableSourcemaps, }), new MiniCssExtractPlugin({ filename: "assets/css/[name].[hash].css", chunkFilename: "assets/css/[id].[hash].css", }), // Pre-compress all the assets as they will be served as is. new CompressionPlugin({}), ] : [ // Add module names to factory functions so they appear in browser profiler. new webpack.NamedModulesPlugin(), // This is necessary to emit hot updates (currently CSS only): new webpack.HotModuleReplacementPlugin(), // Watcher doesn't work well if you mistype casing in a path so we use // a plugin that prints an error when you attempt to do this. // See https://github.com/facebookincubator/create-react-app/issues/240 new CaseSensitivePathsPlugin(), // If you require a missing module and then `npm install` it, you still have // to restart the development server for Webpack to discover it. This plugin // makes the discovery automatic so you don't have to restart. // See https://github.com/facebookincubator/create-react-app/issues/186 new WatchMissingNodeModulesPlugin(paths.appNodeModules), ]; const baseConfig: Configuration = { // Set webpack mode. mode: isProduction ? "production" : "development", devtool: !disableSourcemaps && isProduction ? // We generate sourcemaps in production. This is slow but gives good results. // You can exclude the *.map files from the build during deployment. "source-map" : // You may want 'eval' instead if you prefer to see the compiled output in DevTools. // See the discussion in https://github.com/facebookincubator/create-react-app/issues/343. "cheap-module-source-map", // These are the "entry points" to our application. // This means they will be the "root" imports that are included in JS bundle. // The first two entry points enable "hot" CSS and auto-refreshes for JS. output: { // Add /* filename */ comments to generated require()s in the output. pathinfo: !isProduction, // The dist folder. path: paths.appDistStatic, // Generated JS file names (with nested folders). // There will be one main bundle, and one file per asynchronous chunk. filename: isProduction ? "assets/js/[name].[chunkhash:8].js" : "assets/js/[name].js", chunkFilename: isProduction ? "assets/js/[name].[chunkhash:8].chunk.js" : "assets/js/[name].chunk.js", // We inferred the "public path" (such as / or /my-project) from homepage. publicPath, // Point sourcemap entries to original disk location (format as URL on Windows) devtoolModuleFilenameTemplate: (info: any) => path .relative(paths.appSrc, info.absoluteResourcePath) .replace(/\\/g, "/"), }, resolve: { extensions: [".js", ".json", ".ts", ".tsx"], plugins: [ // Support `tsconfig.json` `path` setting. new TsconfigPathsPlugin({ configFile: paths.appTsconfig, extensions: [".js", ".ts", ".tsx"], }), ], }, resolveLoader: { // Add path to our own loaders. modules: ["node_modules", paths.appLoaders], }, module: { strictExportPresence: true, rules: [ // Disable require.ensure as it's not a standard language feature. { parser: { requireEnsure: false } }, // First, run the linter. // It's important to do this before Babel processes the JS. { test: /\.(js|ts|tsx)$/, enforce: "pre", use: [ { options: { tsConfigFile: paths.appTsconfig, }, loader: require.resolve("tslint-loader"), }, ], include: paths.appSrc, }, { // "oneOf" will traverse all following loaders until one will // match the requirements. When no loader matches it will fall // back to the "file" loader at the end of the loader list. oneOf: [ { test: paths.appStreamLocalesTemplate, use: [ // This is the locales loader that loads available locales // from a particular target. { loader: "locales-loader", options: { ...localesOptions, // Target specifies the prefix for fluent files to be loaded. // ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales. target: "stream", }, }, ], }, { test: paths.appAuthLocalesTemplate, use: [ // This is the locales loader that loads available locales // from a particular target. { loader: "locales-loader", options: { ...localesOptions, // Target specifies the prefix for fluent files to be loaded. // ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales. target: "auth", }, }, ], }, { test: paths.appAdminLocalesTemplate, use: [ // This is the locales loader that loads available locales // from a particular target. { loader: "locales-loader", options: { ...localesOptions, // Target specifies the prefix for fluent files to be loaded. // ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales. target: "admin", }, }, ], }, { test: paths.appInstallLocalesTemplate, use: [ // This is the locales loader that loads available locales // from a particular target. { loader: "locales-loader", options: { ...localesOptions, // Target specifies the prefix for fluent files to be loaded. // ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales. target: "install", }, }, ], }, // Loader for our fluent files. { test: /\.ftl$/, use: ["raw-loader"], }, // "url" loader works like "file" loader except that it embeds assets // smaller than specified limit in bytes as data URLs to avoid requests. // A missing `test` is equivalent to a match. { test: [/\.gif$/, /\.jpe?g$/, /\.png$/], loader: require.resolve("url-loader"), options: { limit: 10000, name: "assets/media/[name].[hash:8].[ext]", }, }, // Process JS with Babel. { test: /\.(ts|tsx)$/, include: paths.appSrc, use: [ { loader: require.resolve("babel-loader"), options: { // This is a feature of `babel-loader` for webpack (not Babel itself). // It enables caching results in ./node_modules/.cache/babel-loader/ // directory for faster rebuilds. cacheDirectory: true, }, }, { loader: "ts-loader", options: { configFile: paths.appTsconfig, compilerOptions: { target: "es2015", module: "esnext", jsx: "preserve", noEmit: false, }, // Overwrites the behavior of `include` and `exclude` to only // include files that are actually being imported and which // are necessary to compile the bundle. onlyCompileBundledFiles: true, }, }, ], }, // Makes sure node_modules are transpiled the way we need them to be. { test: /\.js$/, include: /node_modules\//, use: [ { loader: require.resolve("babel-loader"), options: { presets: [ [ "@babel/env", { targets: "last 2 versions, ie 11", modules: false }, ], ], cacheDirectory: true, }, }, ], }, // "postcss" loader applies autoprefixer to our CSS. // "css" loader resolves paths in CSS and adds assets as dependencies. // "style" loader turns CSS into JS modules that inject