import OptimizeCssnanoPlugin from "@intervolga/optimize-cssnano-plugin"; import bunyan from "bunyan"; import CaseSensitivePathsPlugin from "case-sensitive-paths-webpack-plugin"; import CompressionPlugin from "compression-webpack-plugin"; import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import HtmlWebpackPlugin from "html-webpack-plugin"; import { identity } from "lodash"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; import path from "path"; import WatchMissingNodeModulesPlugin from "react-dev-utils/WatchMissingNodeModulesPlugin"; import TerserPlugin from "terser-webpack-plugin"; import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; import webpack, { Configuration, Plugin } from "webpack"; import WebpackAssetsManifest from "webpack-assets-manifest"; import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; // TODO: import form coral-common/version, for some reason this fails currently. // Try again when we have a chance to upgrade typescript. import { version } from "../common/version"; import { Config, createClientEnv } from "./config"; import paths from "./paths"; /** * filterPlugins will filter out null values from the array of plugins, allowing * easy embedded ternaries. * * @param plugins array of plugins and null values */ const filterPlugins = (plugins: Array): Plugin[] => plugins.filter(identity) as Plugin[]; // Create the build logger. const logger = bunyan.createLogger({ name: "coral", level: "debug", }); interface CreateWebpackOptions { appendPlugins?: any[]; watch?: boolean; } const publicPath = "/"; export default function createWebpackConfig( config: Config, { appendPlugins = [], watch = false }: CreateWebpackOptions = {} ): Configuration[] { logger.debug({ config: config.toString() }, "loaded configuration"); const maxCores = config.get("maxCores"); const env = createClientEnv(config); const disableSourcemaps = config.get("disableSourcemaps"); const generateReport = config.get("generateReport"); const isProduction = env.NODE_ENV === "production"; const minimize = isProduction && !config.get("disableMinimize"); const treeShake = config.get("enableTreeShake"); const envStringified = { "process.env": Object.keys(env).reduce>( (result, key) => { result[key] = JSON.stringify((env as any)[key]); return result; }, { TALK_VERSION: JSON.stringify(version), } ), }; /** * ifWatch will only include the nodes if we're in watch mode. */ const ifWatch = watch ? (...nodes: any[]) => nodes : () => []; /** * ifBuild will only include the nodes if we're in build mode. */ const ifBuild = !watch ? (...nodes: any[]) => nodes : () => []; const styleLoader = { loader: require.resolve("style-loader"), }; const localesOptions = { pathToLocales: paths.appLocales, // Default locale if none was specified. defaultLocale: config.get("defaultLocale"), // Fallback locale if a translation was not found. // If not set, will use the text that is already // in the code base. fallbackLocale: config.get("defaultLocale"), // Common fluent files are always included in the locale bundles. commonFiles: ["framework.ftl", "common.ftl", "ui.ftl"], // Locales that come with the main bundle. Others are loaded on demand. bundled: [config.get("defaultLocale")], // All available locales can be loadable on demand. // To restrict available locales set: // availableLocales: [config.get("defaultLocale")], }; const additionalPlugins = [ ...ifBuild( new MiniCssExtractPlugin({ filename: isProduction ? "assets/css/[name].[hash].css" : "assets/css/[name].css", chunkFilename: isProduction ? "assets/css/[id].[hash].css" : "assets/css/[id].css", }), isProduction && new OptimizeCssnanoPlugin({ sourceMap: !disableSourcemaps, cssnanoOptions: { preset: [ "default", { discardComments: { removeAll: true, }, }, ], }, }), // Pre-compress all the assets as they will be served as is. new CompressionPlugin({}) ), ...ifWatch( // Add module names to factory functions so they appear in browser profiler. new webpack.NamedModulesPlugin(), // 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 devtool = disableSourcemaps ? false : 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"; const baseConfig: Configuration = { stats: { // https://github.com/TypeStrong/ts-loader#transpileonly-boolean-defaultfalse // Using transpilation only without typechecks gives warnings when we reexport types. // We can ignore them here. warningsFilter: /export .* was not found in/, }, // Set webpack mode. mode: isProduction ? "production" : "development", optimization: { concatenateModules: isProduction, providedExports: true, usedExports: true, // We can't use side effects because it disturbs css order // https://github.com/webpack/webpack/issues/7094. sideEffects: false, splitChunks: { chunks: config.get("disableChunkSplitting") ? "async" : "all", }, minimize: minimize || treeShake, minimizer: [ // Minify the code. new TerserPlugin({ terserOptions: { compress: minimize ? {} : { defaults: false, dead_code: true, pure_getters: true, side_effects: true, unused: true, }, mangle: minimize && {}, output: { comments: !minimize, // Turned on because emoji and regex is not minified properly using default // https://github.com/facebookincubator/create-react-app/issues/2488 ascii_only: true, }, safari10: true, }, cache: true, parallel: true, sourceMap: !disableSourcemaps, }), ], }, devtool, // 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].js" : "assets/js/[name].js", chunkFilename: isProduction ? "assets/js/[name].[chunkhash].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: { // https://github.com/TypeStrong/ts-loader#transpileonly-boolean-defaultfalse // Using transpilation only without typechecks gives warnings when we reexport types // thus we can't turn on `strictExportPresence` which would turn warnings into errors. strictExportPresence: false, rules: [ // Disable require.ensure as it's not a standard language feature. { parser: { requireEnsure: false } }, { // "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.appAccountLocalesTemplate, 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: "account", }, }, ], }, { 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$/, /\.svg$/], loader: require.resolve("url-loader"), options: { limit: 10000, name: isProduction ? "assets/media/[name].[hash].[ext]" : "assets/media/[name].[ext]", }, }, { test: /\.css\.ts$/, use: [ !watch ? MiniCssExtractPlugin.loader : styleLoader, { loader: require.resolve("css-loader"), options: { modules: { localIdentName: "[name]-[local]-[contenthash]", }, importLoaders: 2, sourceMap: !disableSourcemaps, }, }, { loader: require.resolve("postcss-loader"), options: { config: { path: paths.appPostCssConfig, }, parser: "postcss-js", }, }, { loader: require.resolve("babel-loader"), options: { configFile: false, babelrc: false, presets: [ "@babel/typescript", [ "@babel/env", { targets: { node: "current" }, modules: "commonjs" }, ], ], // 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, }, }, ], }, // Process JS with Babel. { test: /\.(ts|tsx)$/, include: paths.appSrc, use: [ { loader: "thread-loader", options: { // there should be 1 cpu for the fork-ts-checker-webpack-plugin workers: maxCores - 1, poolTimeout: watch ? Infinity : 500, // set this to Infinity in watch mode - see https://github.com/webpack-contrib/thread-loader }, }, { 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: require.resolve("ts-loader"), options: { configFile: paths.appTsconfig, compilerOptions: { target: "es2015", module: "esnext", jsx: "preserve", noEmit: false, sourceMap: !disableSourcemaps, }, transpileOnly: true, // 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, happyPackMode: true, // IMPORTANT! use happyPackMode mode to speed-up compilation and reduce errors reported to webpack }, }, ], }, // Makes sure node_modules are transpiled the way we need them to be. { test: /\.js$/, include: /node_modules\//, exclude: /node_modules\/(@babel|babel|core-js|regenerator-runtime)/, use: [ { loader: require.resolve("babel-loader"), options: { 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