Unverified Commit 66803416 authored by Andrey Azov's avatar Andrey Azov Committed by GitHub
Browse files

Change the way webpack configs are composed (#211)

Rather than starting with an environment-specific config as an entry point for webpack,
use a generic config as an entry point, and from there add more specific configs
via composition using webpack-merge.
parent e16e4498
Pipeline #49163 passed with stages
in 4 minutes and 33 seconds
......@@ -14,6 +14,7 @@ There are several script commands that have been baked into the NPM configuratio
- `npm run serve:prod` - This runs the built production site locally using `http`.
- `npm run serve:prod:secure` - Runs the build production site locally securely using `https`. You will need to run `certify` before running this, in case you already haven't generated an SSL certificate.
- `npm run build` - Runs the production build. It will initially delete the existing local production build and replace it with the new one.
- `npm run prod:analyse` — Runs production build, and also uses `webpack-bundle-analyzer` to report the size of the bundle.
- `npm run deploy` - Runs `deploy.js` file to deploy the production build into the master machine (`ves-hx2-70`). You will need to pass the full address of the machine name along with your username as an argument.
- `npm run certify` - Runs `setup-ssl.js` to create a local SSL certificate to run the production build on `HTTPS`. There are two files that are created for this: `localhost.crt` and `localhost.key`.
- `npm run lint` - Runs both `lint:scripts` and `lint:style`. There is no need to run any of these lint scripts during development. This is because, each time a change is made to the code, the linters run along with the build.
......
......@@ -22809,6 +22809,15 @@
"uuid": "^3.3.2"
}
},
"webpack-merge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz",
"integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==",
"dev": true,
"requires": {
"lodash": "^4.17.15"
}
},
"webpack-sources": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
......
......@@ -15,10 +15,11 @@
"scripts": {
"copy-dotenv": "test ! -f .env && (cp .env.example .env; echo '.env file created') || true",
"start": "npm install --no-save && npm run serve:dev",
"serve:dev": "npm run copy-dotenv && webpack-dev-server --config ./webpack/webpack.config.dev.js",
"serve:dev": "npm run copy-dotenv && webpack-dev-server --config ./webpack/webpack.config.js --env.mode dev",
"serve:prod": "node ./server.js",
"serve:prod:secure": "node ./server.js -p https",
"build": "rimraf ./dist && webpack --config ./webpack/webpack.config.prod.js",
"build": "rimraf ./dist && NODE_ENV=production webpack --config ./webpack/webpack.config.js --env.mode prod",
"prod:analyse": "npm run build -- --env.presets analyse",
"deploy": "node deploy",
"certify": "node setup-ssl",
"lint": "npm run lint:scripts && npm run lint:styles",
......@@ -159,6 +160,7 @@
"webpack-bundle-analyzer": "3.6.0",
"webpack-cli": "3.3.10",
"webpack-dev-server": "3.9.0",
"webpack-merge": "4.2.2",
"workbox-webpack-plugin": "4.3.1"
},
"browserslist": [
......
module.exports = {
root: true, // do not try to use eslint rules from parent folder
parserOptions: {
ecmaVersion: 2019,
}
};
const isDevelopment = (environment) => ['dev', 'development'].includes(environment);
module.exports = {
isDevelopment
};
const path = require('path');
const postcssPresetEnv = require('postcss-preset-env');
const HtmlPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ForkTsCheckerPlugin = require('fork-ts-checker-webpack-plugin');
const { getPaths } = require('../paths');
const { isDevelopment } = require('./environment-detector');
module.exports = (env) => {
const isDev = isDevelopment(env.mode);
const paths = getPaths();
return {
// the starting point of webpack bundling
entry: {
index: path.join(paths.rootPath, 'src/index.tsx')
},
module: {
rules: [
{
test: /.tsx?$/,
loader: 'babel-loader',
exclude: /node_modules/
},
// the loaders for styling
// a scss file will first be loaded via sass loader and transpiled
// afterwards it will be processed by postcss loader to make the css cross-browser compatible
// add the processed css into the html document during runtime for dev
// and extract the css for prod and minify it as external stylesheets
{
test: /.scss$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: true,
modules: {
localIdentName: '[local]__[name]__[hash:base64:5]'
},
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [postcssPresetEnv()]
}
},
'sass-loader'
]
},
// use file-loader on svg's (to be able to require them as a path to the image),
// but also use @svgr/webpack to be able to require svg's directly as React components
{
test: /\.svg$/,
use: ['@svgr/webpack', 'file-loader'],
}
]
},
// prevent webpack from searching fs (node API) to load the web assembly files
node: {
fs: 'empty'
},
// this is the config to define how the output files needs to be
output: {
// dev will take the default file names as no physical files are emitted
// prod will have emitted files and will include the content hash, which will change every time the contents of the js file changes.
filename: isDev ? undefined : '[name].[contenthash].js',
path: paths.buildStaticPath,
// stop webpack from adding additional comments/info to generated bundles as it is a performance hit (slows down build times)
pathinfo: false,
// prepend the public path as the root path to all the files that are inserted into the index file
publicPath: isDev ? '/' : '/static/'
},
// the plugins that extends the webpack configuration
plugins: [
// checks typescript types
new ForkTsCheckerPlugin(),
// generates the index file using the provided html template
new HtmlPlugin({
// in prod, path for saving static assets is dist/static/, and index.html has to be saved top-level in the dist folder
filename: isDev ? 'index.html' : '../index.html',
template: paths.htmlTemplatePath,
publicPath: '/'
})
],
// configuration that allows us to not to use file extensions and shorten import paths (using aliases)
resolve: {
extensions: ['.tsx', '.ts', '.js', '.scss'],
alias: {
config: path.join(paths.rootPath, 'config.ts'),
src: path.join(paths.rootPath, 'src'),
tests: path.join(paths.rootPath, 'tests'),
static: path.join(paths.rootPath, 'static')
}
}
}
};
const dotenv = require('dotenv').config();
const webpack = require('webpack');
const path = require('path');
const url = require('url');
const StylelintWebpackPlugin = require('stylelint-webpack-plugin');
const { getPaths } = require('../paths');
const paths = getPaths('development');
const devServerConfig = {
before(app) {
// use proper mime-type for wasm files
app.get('*.wasm', function(req, res, next) {
const options = {
root: path.join(paths.nodeModulesPath, 'ensembl-genome-browser'),
dotfiles: 'deny',
headers: {
'Content-Type': 'application/wasm'
}
};
const parsedUrl = url.parse(req.url);
const fileName = path.basename(parsedUrl.pathname);
res.sendFile(fileName, options, function(err) {
if (err) {
next(err);
}
});
});
},
// rules to proxy requests to the backend server in development
proxy: {
'/api': {
target: 'https://staging-2020.ensembl.org',
changeOrigin: true,
secure: false
},
'/browser': {
target: 'https://staging-2020.ensembl.org',
changeOrigin: true,
secure: false
}
},
// fallback for the history API used by the react router when page is reloaded
// this should prevent 404 errors that usually occur in SPA on reloads
historyApiFallback: true,
// make the server accessible from other machines
host: '0.0.0.0',
// enable hot module reloading
hot: true,
// configuration to customise what is displayed in the console by webpack
stats: {
assets: false,
chunks: false,
colors: true,
modules: false
}
};
// concatenate the common config with the dev config
module.exports = () => ({
mode: 'development',
devtool: 'cheap-module-eval-source-map',
module: {
rules: [
// this is the loader that will make webpack load file formats that are not supported by other loaders
{
test: /\.(woff|woff2|eot|ttf|otf|gif|png|jpe?g)$/,
loader: 'file-loader',
options: {
// the file path and name that webpack will use to store these files
name: `[path][name].[ext]`
}
}
]
},
plugins: [
// this plugin is required to enable hot module reloading
// for the webpack dev server
new webpack.HotModuleReplacementPlugin(),
// lint the SASS files within the Ensembl codebase only
new StylelintWebpackPlugin({
context: path.join(paths.rootPath, 'src'),
files: '**/*.scss'
}),
// make environment variables available on the client-side
new webpack.DefinePlugin({
'process.env': JSON.stringify(dotenv.parsed)
})
],
// speed up build times for dev
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false
},
// disable webpack from watching node modules (except for ensembl-genome-browser)
// this would reduce memory consumption and also should improve build times
watchOptions: {
ignored: /node_modules([\\]+|\/)+(?!ensembl-genome-browser)/
},
devServer: devServerConfig
});
const webpack = require('webpack');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const BrotliPlugin = require('brotli-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
const RobotstxtPlugin = require('robotstxt-webpack-plugin');
const { getPaths } = require('../paths');
const paths = getPaths('production');
// copy from the environment the same variables that are declared in .env.example
// NOTE: if no environment variable with corresponding key is present, the value from .env.example will be used
const dotenv = require('dotenv').config({
path: paths.envTemplatePath
});
const getEnvironmentVariables = () => Object.keys(dotenv.parsed).reduce((result, key) => ({
...result,
[`process.env.${key}`]: JSON.stringify(process.env[key])
}), {});
// concatenate the common config with the prod config
module.exports = () => {
return {
mode: 'production',
devtool: 'source-map',
module: {
rules: [
// loader for images
// image loader should compress the images
// then file loader takes over to copy the images into the dist folder
{
test: /.*\.(gif|png|jpe?g)$/i,
use: [
{
loader: 'file-loader',
options: {
emitFile: true,
name: '[name].[hash].[ext]',
outputPath: 'images'
}
},
'image-webpack-loader'
]
},
// loader for fonts that copies the fonts into the dist folder
{
test: /static\/fonts\/.*\.(woff2?|eot|ttf|otf|svg)$/i,
loader: 'file-loader',
options: {
emitFile: true,
name: '[path][name].[hash].[ext]'
}
}
]
},
plugins: [
// make environment variables available on the client-side
new webpack.DefinePlugin({
...getEnvironmentVariables()
}),
// plugin to extract css from the webpack javascript build files
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css'
}),
// copy static assets
new CopyWebpackPlugin([
{
from: path.join(paths.nodeModulesPath, 'ensembl-genome-browser/browser*.wasm'),
to: path.join(paths.buildStaticPath, 'browser'),
flatten: true
},
{
from: path.join(paths.staticPath, 'favicons/*'),
to: path.join(paths.buildStaticPath, 'favicons'),
flatten: true
},
{
from: path.join(paths.staticPath, 'manifest.json'),
to: paths.buildStaticPath,
flatten: true
}
]),
// generate unique hashes for files based on the relative paths
new webpack.HashedModuleIdsPlugin(),
new CompressionPlugin({
test: /.(js|css|html|wasm)$/,
threshold: 5120,
minRatio: 0.7
}),
// brotli compression for static files
// only files above 5kB will be compressed
new BrotliPlugin({
test: /.(js|css|html|wasm)$/,
threshold: 5120, // 5kB
minRatio: 0.7
}),
// adds workbox library (from Google) support to enable service workers
new WorkboxPlugin.GenerateSW({
swDest: '../service-worker.js', // save service worker in the root folder (/dist) instead of /dist/static
clientsClaim: true,
skipWaiting: true,
exclude: [/index.html$/, /\.gz$/, /\.br$/, /\.js\.map$/],
runtimeCaching: [{
urlPattern: ({ event }) => event.request.mode === 'navigate',
handler: 'NetworkFirst'
}]
}),
new RobotstxtPlugin({
filePath: '../robots.txt'
})
],
optimization: {
// use terser plugin instead of uglify js to support minimisation for modern React.js features
// optimize css assets plugin to minimise css as it is not yet supported in webpack by default
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true
}),
new OptimizeCSSAssetsPlugin({})
],
// create a separate webpack runtime chunk that will be used for all bundles
runtimeChunk: true,
// the common chunks configuration
splitChunks: {
cacheGroups: {
// commonly shared code i.e. vendor code to be bundled as vendor.js
commons: {
test: /node_modules/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
};
const path = require('path');
const getPaths = (env) => {
const isDev = ['dev', 'development'].includes(env);
const rootPath = path.resolve(__dirname, '../');
const nodeModulesPath = path.resolve(rootPath, 'node_modules');
const staticPath = path.resolve(rootPath, 'static');
const buildPath = path.resolve(rootPath, 'dist');
return {
rootPath,
nodeModulesPath,
buildPath,
staticPath,
buildStaticPath: path.resolve(buildPath, 'static'),
htmlFileName: isDev ? 'index.html' : '../index.html',
htmlTemplatePath: path.resolve(staticPath, 'html/template.html'),
envTemplatePath: path.resolve(rootPath, '.env.example'),
};
};
module.exports = {
getPaths
};
const webpackMerge = require('webpack-merge');
/* NOTE:
- env may or may not be passed
- env may or may not contain the presets field
- the presets field contains either a string or an array of strings
*/
const loadPresets = (env) => {
let { presets = [] } = env;
if (typeof presets === 'string') {
presets = [presets];
}
return webpackMerge(presets.map(presetName =>
require(`./webpack.${presetName}`)(env)
));
};
module.exports = loadPresets;
const WebpackBundleAnalyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = () => ({
plugins: [
new WebpackBundleAnalyzer()
]
})
const path = require('path');
const postcssPresetEnv = require('postcss-preset-env');
const HtmlPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ForkTsCheckerPlugin = require('fork-ts-checker-webpack-plugin');
// exports an anonymous function
// params are:
// 1. isDev: boolean - is it the development build that needs to be returned
// 2. moduleRules: array - all the module rules specific to the dev/prod build
// 3. plugins: array - all the plugins specific to the dev/prod build
// returns: webpack common configuration for the build
module.exports = (isDev, moduleRules, plugins) => ({
// the source map type
// for dev it will be a line by line source map
// for prod it will be a generic source map that may not be as accurate as the former
devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map',
// the starting point of webpack bundling
entry: {
index: path.join(__dirname, '../src/index.tsx')
},
// the mode is what notifies webpack how the build should be made
mode: isDev ? 'development' : 'production',
// the module loaders for webpack
// these loaders help webpack to identify and process different file formats for bundling
module: {
rules: [
// babel-loader is used for typescript as it has good tree shaking support compared to tsc
{
test: /.tsx?$/,
loader: 'babel-loader',
exclude: /node_modules/
},
// the loaders for styling
// there are two sets of them: for global and component styles
// a scss file will first be loaded via sass loader and transpiled
// afterwards it will be processed by postcss loader to make the css cross-browser compatible
// add the processed css into the html document during runtime for dev
// and extract the css for prod and minify it as external stylesheets
{
test: /.scss$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: true,
modules: {
localIdentName: '[local]__[name]__[hash:base64:5]'
},
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [postcssPresetEnv()]
}
},
'sass-loader'
]
},
// use file-loader on svg's (to be able to require them as a path to the image),
// but also use @svgr/webpack to be able to require svg's directly as React components
{
test: /\.svg$/,
use: ['@svgr/webpack', 'file-loader'],
},
// loaders specific to dev/prod
...moduleRules
]
},
// prevent webpack from searching fs (node API) to load the web assembly files
node: {
fs: 'empty'
},
// this is the config to define how the output files needs to be
output: {
// dev will take the default file names as no physical files are emitted
// prod will have emitted files and will include the content hash, which will change every time the contents of the js file changes.
filename: isDev ? undefined : '[name].[contenthash].js',
path: path.resolve(__dirname, '../dist/static'),