UNPKG

auspice

Version:

Web app for visualizing pathogen evolution

304 lines (285 loc) 10.4 kB
/* eslint no-console: off */ const path = require("path"); const webpack = require("webpack"); const CompressionPlugin = require('compression-webpack-plugin'); const fs = require('fs'); const utils = require('./cli/utils'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); const zlib = require("zlib"); /* Webpack config generator */ const generateConfig = ({extensionPath, devMode=false, customOutputPath, analyzeBundle=false}) => { utils.verbose(`Generating webpack config. Extensions? ${!!extensionPath}. devMode: ${devMode}`); // Pins all react stuff, and uses hot loader's dom (can be used safely in production) // Format is either "libName" or "libName:libPath" const coreDeps = [ "react", "react-hot-loader", "react-dom:@hot-loader/react-dom", "regenerator-runtime", "core-js", "styled-components" ]; // Actively searches for the "good" root starting from auspice dir and going backwards // In 99.9% of practical cases these should all resolve wrt the node_modules in the root project, // but if there are conflict they will preferentially resolve to auspice's node_modules let baseDir = __dirname; let foundNodeModules = false; const resolvedCoreDeps = {}; while (!foundNodeModules) { foundNodeModules = true; for (const coreDep of coreDeps) { const coreDepParts = coreDep.split(":"); if (!resolvedCoreDeps[coreDepParts[0] || coreDep]) { const modulePath = path.join(baseDir, "node_modules", coreDepParts[1] || coreDep); if (fs.existsSync(modulePath)) resolvedCoreDeps[coreDepParts[0] || coreDep] = modulePath; else foundNodeModules = false; } } baseDir = path.resolve(baseDir, ".."); } /* webpack alias' used in code import / require statements */ const aliasesToResolve = { "@extensions": path.join(__dirname, '.null'), /* must provide a default, else it won't compile */ "@auspice": path.join(__dirname, 'src'), ...resolvedCoreDeps }; let extensionData; if (extensionPath) { // console.log("BUILDING WITH EXTENSIONS"); const dir = path.resolve(__dirname, path.dirname(extensionPath)); aliasesToResolve["@extensions"] = dir; extensionData = JSON.parse(fs.readFileSync(extensionPath, {encoding: 'utf8'})); if (extensionData.googleAnalyticsKey) { console.log(`DEPRECATION WARNING: your extensions define a Google Analytics key (${extensionData.googleAnalyticsKey}) but GA will be removed from a future release.`); } // console.log("extensionData", extensionData); } /* plugins */ /* inject strings into the client-accessible process.env */ const pluginProcessEnvData = new webpack.DefinePlugin({ "process.env": { NODE_ENV: devMode ? JSON.stringify("development") : JSON.stringify("production"), SKIP_REDUX_CHECKS: JSON.stringify(process.env.SKIP_REDUX_CHECKS), EXTENSION_DATA: JSON.stringify(extensionData) } }); /* gzip everything - https://github.com/webpack-contrib/compression-webpack-plugin */ const pluginCompressGzip = new CompressionPlugin({ filename: "[path].gz[query]", algorithm: "gzip", test: /\.(js|css|html)$/, threshold: 4096 }); const pluginCompressBrotli = new CompressionPlugin({ filename: "[path].br[query]", algorithm: "brotliCompress", test: /\.(js|css|html)$/, compressionOptions: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11, }, }, threshold: 4096 }); const pluginHtml = new HtmlWebpackPlugin({ filename: 'index.html', template: './src/index.html' }); const cleanWebpackPlugin = new CleanWebpackPlugin({ cleanStaleWebpackAssets: true }); const plugins = devMode ? [ new LodashModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(), pluginProcessEnvData, pluginHtml, cleanWebpackPlugin ] : [ new LodashModuleReplacementPlugin(), pluginProcessEnvData, pluginCompressGzip, pluginCompressBrotli, pluginHtml, cleanWebpackPlugin ]; if (analyzeBundle) { const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; plugins.push(new BundleAnalyzerPlugin()); } const entry = devMode ? ["webpack-hot-middleware/client", "./src/indexAsync"] : ["core-js/es/promise", "./src/indexAsync"]; /* Where do we want the output to be saved? * For development we use the (virtual) "devel" directory * Else we must choose to save it in the CWD or the source */ const outputPath = devMode ? path.resolve(__dirname, "devel") : // development: use the (virtual) "devel" directory customOutputPath ? path.resolve(customOutputPath, "dist") : path.resolve(__dirname, "dist"); utils.verbose(`Webpack writing output to: ${outputPath}`); /** * Here we put the libraries that are unlikely to change for a long time. * Every change or update of any of these libraries will change the hash * of the big vendor bundle, so we must be sure they're stable both internally * and with respect to the implementation. * The hashes of the bundles are hardcoded in bundlesize so it will trigger * a check error if it is inadvertently changed. */ const coreVendors = [ "@babel/runtime", "style-loader", "@hot-loader/react-dom", "react(-(redux|select|helmet|i18next))?", "redux", "i18next", "styled-components", "stylis", "@emotion", "@stdlib", ]; // <= needs some review from somebody with more knowledge of the whole codebase to decide what goes in /** * Here we put the (big) libraries that are more prone to change/update. * For example d3-.* is here even if it's a core library, because if we * include some new d3 feature, the whole bundle will change. * The hashes of the bundles are hardcoded in bundlesize so it will trigger * a check error if it is inadvertently changed. */ const bigVendors = [ "d3-.*", // d3 is imported selectively, new usages may change the bundle "lodash", // lodash is imported selectively using the lodash plugin, new usages may change the bundle "react-transition-group", "react-icons", "react-tooltip", "create-react-class", "mousetrap", "react-input-autosize", "typeface-lato", // "papaparse", <= This is only for the drag-and-drop of files and can be separated "dom-to-image", // marked + dompurify are used for MD display of the footer in (most) datasets "marked", "dompurify", // `yaml-front-matter` only used for narrative parsing, but included here to simplify the import code // and avoid it being bundled with most of the Auspice code. It imports "js-yaml". "yaml-front-matter", "js-yaml" ]; const polyfills = [ "core-js", "regenerator-runtime", "whatwg-fetch", "css.escape", ] const mapComponentLibraries = [ "leaflet", "leaflet-gesture-handling", ] /** * It's better to keep small libraries out of the vendor bundles because * the more libraries we include, the more small changes or updates * of a single small library will cause the hash to change. */ const config = { mode: devMode ? 'development' : 'production', context: __dirname, // Better to get a proper full source map in dev mode, this one is pretty fast on rebuild devtool: !devMode ? undefined : "eval-source-map", entry, output: { path: outputPath, filename: `auspice.[name].bundle${!devMode ? ".[contenthash]" : ""}.js`, chunkFilename: `auspice.chunk.[name].bundle${!devMode ? ".[chunkhash]" : ""}.js`, publicPath: "/dist/" }, resolve: { alias: aliasesToResolve, extensions: ['.ts', '.tsx', '...'], fallback: { buffer: require.resolve("buffer/"), fs: false } }, plugins, optimization: { minimize: !devMode, runtimeChunk: "single", splitChunks: { minChunks: 3, minSize: 8192, cacheGroups: { polyfills: { test: new RegExp("[\\\\/]node_modules[\\\\/](" + polyfills.join("|") + ")[\\\\/]"), name: "polyfills", enforce: true, chunks: "all" }, coreVendors: { test: new RegExp("[\\\\/]node_modules[\\\\/](" + coreVendors.join("|") + ")[\\\\/]"), name: "core-vendors", enforce: true, chunks: "all" }, otherVendors: { test: new RegExp("[\\\\/]node_modules[\\\\/](" + bigVendors.join("|") + ")[\\\\/]"), name: "other-vendors", enforce: true, chunks: "all" }, mapComponent: { test: new RegExp("[\\\\/]node_modules[\\\\/](" + mapComponentLibraries.join("|") + ")[\\\\/]"), name: "mapComponent", /* matches the lazily imported component webpackChunkName (via special in-line comment) */ enforce: true, }, /** * ATM the package size is <15kB and so not worth splitting, * but it can be split further if it becomes huge */ locales: { test: /[\\/]src[\\/]locales[\\/](?!en)/, name: "locales", enforce: true, chunks: "all" }, default: { minChunks: 3, minSize: 8192, reuseExistingChunk: false }, defaultVendors: false } } }, module: { rules: [ { test: /\.(ts|js)x?$/, loader: 'babel-loader', exclude: [ /node_modules\/(core-js|regenerator-runtime)/ ], options: { cwd: path.resolve(__dirname) } }, { test: /\.css$/, use: ["style-loader", "css-loader"] }, { test: /\.(gif|png|jpe?g|svg|woff2?|eot|otf|ttf)$/i, type: "asset/resource" }, { // esprima is a (large) dependency of js-yaml which is unnecessary in a browser // see https://github.com/nodeca/js-yaml/issues/230 test: /node_modules\/esprima/, use: 'null-loader' } ] } }; return config; }; module.exports = {default: generateConfig};