Skip to content

Instantly share code, notes, and snippets.

@davidhellmann
Created January 17, 2020 08:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidhellmann/6d807ad2d5c17b0f22de1a5dae540150 to your computer and use it in GitHub Desktop.
Save davidhellmann/6d807ad2d5c17b0f22de1a5dae540150 to your computer and use it in GitHub Desktop.
// webpack.prod.js - production builds
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';
// node modules
const git = require('git-rev-sync');
const glob = require('glob-all');
const merge = require('webpack-merge');
const moment = require('moment');
const path = require('path');
const webpack = require('webpack');
// webpack plugins
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const CreateSymlinkPlugin = require('create-symlink-webpack-plugin');
const CriticalCssPlugin = require('critical-css-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ImageminWebpWebpackPlugin = require('imagemin-webp-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const SaveRemoteFilePlugin = require('save-remote-file-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const WebappWebpackPlugin = require('webapp-webpack-plugin');
const WhitelisterPlugin = require('purgecss-whitelister');
const WorkboxPlugin = require('workbox-webpack-plugin');
const zopfli = require('@gfx/zopfli');
// config files
const common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
// Custom PurgeCSS extractor for Tailwind that allows special characters in
// class names.
//
// https://github.com/FullHuman/purgecss#extractor
class SpecialCharsExtractor {
static extract(content) {
return content.match(/[A-Za-z0-9-_:\/]+/g) || [];
}
}
// Configure file banner
const configureBanner = () => {
let commitHash = 'n/a';
let branch = 'n/a';
try {
let commitHash = git.long();
let branch = git.branch();
} catch (error) {
console.log('No git repository is associated with this project');
}
return {
banner: [
'/*!',
' * @project ' + settings.name,
' * @name ' + '[filebase]',
' * @author ' + pkg.author.name,
' * @build ' + moment().format('llll') + ' ET',
' * @release ' + commitHash + ' [' + branch + ']',
' * @copyright Copyright (c) ' +
moment().format('YYYY') +
' ' +
settings.copyright,
' *',
' */',
'',
].join('\n'),
raw: true,
};
};
// Configure Bundle Analyzer
const configureBundleAnalyzer = buildType => {
if (buildType === LEGACY_CONFIG) {
return {
analyzerMode: 'static',
reportFilename: 'report-legacy.html',
};
}
if (buildType === MODERN_CONFIG) {
return {
analyzerMode: 'static',
reportFilename: 'report-modern.html',
};
}
};
// Configure Compression webpack plugin
const configureCompression = () => {
return {
filename: '[path].gz[query]',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false,
compressionOptions: {
numiterations: 15,
level: 9,
},
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
},
};
};
// Configure Critical CSS
const configureCriticalCss = () => {
return settings.criticalCssConfig.pages.map(row => {
const criticalSrc = settings.urls.critical + row.url;
const criticalDest =
settings.criticalCssConfig.base +
row.template +
settings.criticalCssConfig.suffix;
let criticalWidth = settings.criticalCssConfig.criticalWidth;
let criticalHeight = settings.criticalCssConfig.criticalHeight;
// Handle Google AMP templates
if (row.template.indexOf(settings.criticalCssConfig.ampPrefix) !== -1) {
criticalWidth = settings.criticalCssConfig.ampCriticalWidth;
criticalHeight = settings.criticalCssConfig.ampCriticalHeight;
}
console.log('source: ' + criticalSrc + ' dest: ' + criticalDest);
return new CriticalCssPlugin({
base: './',
src: criticalSrc,
dest: criticalDest,
extract: false,
inline: false,
minify: true,
width: criticalWidth,
height: criticalHeight,
});
});
};
// Configure Clean webpack
const configureCleanWebpack = () => {
return {
cleanOnceBeforeBuildPatterns: settings.paths.dist.clean,
verbose: true,
dry: false,
};
};
// Configure Html webpack
const configureHtml = () => {
return {
templateContent: '',
filename: 'webapp.html',
inject: false,
};
};
// Configure Image loader
const configureImageLoader = buildType => {
if (buildType === LEGACY_CONFIG) {
return {
test: /\.(png|jpe?g|gif|svg|webp)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'img/[name].[hash].[ext]',
},
},
],
};
}
if (buildType === MODERN_CONFIG) {
return {
test: /\.(png|jpe?g|gif|svg|webp)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'img/[name].[hash].[ext]',
},
},
{
loader: 'img-loader',
options: {
plugins: [
require('imagemin-gifsicle')({
interlaced: true,
}),
require('imagemin-mozjpeg')({
progressive: true,
arithmetic: false,
}),
require('imagemin-optipng')({
optimizationLevel: 5,
}),
require('imagemin-svgo')({
plugins: [{ convertPathData: false }],
}),
],
},
},
],
};
}
};
// Configure optimization
const configureOptimization = buildType => {
if (buildType === LEGACY_CONFIG) {
return {
splitChunks: {
cacheGroups: {
default: false,
common: false,
styles: {
name: settings.vars.cssName,
test: /\.(pcss|scss|css|vue)$/,
chunks: 'all',
enforce: true,
},
},
},
minimizer: [
new TerserPlugin(configureTerser()),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
map: {
inline: false,
annotation: true,
},
safe: true,
discardComments: true,
},
}),
],
};
}
if (buildType === MODERN_CONFIG) {
return {
minimizer: [new TerserPlugin(configureTerser())],
};
}
};
// Configure Postcss loader
const configurePostcssLoader = buildType => {
if (buildType === LEGACY_CONFIG) {
return {
test: /\.(pcss|css|scss)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 3,
sourceMap: true,
},
},
{
loader: 'resolve-url-loader',
},
{
loader: 'postcss-loader',
options: {
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
// Prefer `dart-sass`
implementation: require('sass'),
sassOptions: {
includePaths: ['./node_modules'],
},
sourceMap: true,
},
},
],
};
}
// Don't generate CSS for the modern config in production
if (buildType === MODERN_CONFIG) {
return {
test: /\.(pcss|css|scss)$/,
loader: 'ignore-loader',
};
}
};
// Configure PurgeCSS
const configurePurgeCss = () => {
let paths = [];
// Configure whitelist paths
for (const [key, value] of Object.entries(settings.purgeCssConfig.paths)) {
paths.push(path.join(__dirname, value));
}
return {
paths: glob.sync(paths),
whitelist: WhitelisterPlugin(settings.purgeCssConfig.whitelist),
whitelistPatterns: settings.purgeCssConfig.whitelistPatterns,
extractors: [
{
extractor: SpecialCharsExtractor,
extensions: settings.purgeCssConfig.extensions,
},
],
};
};
// Configure terser
const configureTerser = () => {
return {
cache: true,
parallel: true,
sourceMap: true,
};
};
// Configure Webapp webpack
const configureWebapp = () => {
return {
logo: settings.webappConfig.logo,
prefix: settings.webappConfig.prefix,
cache: false,
inject: 'force',
favicons: {
appName: pkg.name,
appDescription: pkg.description,
developerName: pkg.author.name,
developerURL: pkg.author.url,
path: settings.paths.dist.base,
},
};
};
// Configure Workbox service worker
const configureWorkbox = () => {
let config = settings.workboxConfig;
return config;
};
// Production module exports
module.exports = [
merge(common.legacyConfig, {
output: {
filename: path.join('./js', '[name]-legacy.[chunkhash].js'),
},
mode: 'production',
devtool: 'source-map',
optimization: configureOptimization(LEGACY_CONFIG),
module: {
rules: [
configurePostcssLoader(LEGACY_CONFIG),
configureImageLoader(LEGACY_CONFIG),
],
},
plugins: [
new MiniCssExtractPlugin({
path: path.resolve(__dirname, settings.paths.dist.base),
filename: path.join('./css', '[name].[chunkhash].css'),
}),
new PurgecssPlugin(configurePurgeCss()),
new webpack.BannerPlugin(configureBanner()),
new HtmlWebpackPlugin(configureHtml()),
new WebappWebpackPlugin(configureWebapp()),
new CreateSymlinkPlugin(settings.createSymlinkConfig, true),
new SaveRemoteFilePlugin(settings.saveRemoteFileConfig),
new CompressionPlugin(configureCompression()),
// new BundleAnalyzerPlugin(
// configureBundleAnalyzer(LEGACY_CONFIG),
// ),
].concat(configureCriticalCss()),
}),
merge(common.modernConfig, {
output: {
filename: path.join('./js', '[name].[chunkhash].js'),
},
mode: 'production',
devtool: 'source-map',
optimization: configureOptimization(MODERN_CONFIG),
module: {
rules: [
configurePostcssLoader(MODERN_CONFIG),
configureImageLoader(MODERN_CONFIG),
],
},
plugins: [
new CleanWebpackPlugin(configureCleanWebpack()),
new webpack.BannerPlugin(configureBanner()),
new ImageminWebpWebpackPlugin(),
new WorkboxPlugin.GenerateSW(configureWorkbox()),
new CompressionPlugin(configureCompression()),
// new BundleAnalyzerPlugin(
// configureBundleAnalyzer(MODERN_CONFIG),
// ),
],
}),
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment