vizon-countdown-website/webpack.config.babel.js

387 lines
11 KiB
JavaScript

import {
DefinePlugin,
NamedModulesPlugin,
HashedModuleIdsPlugin,
LoaderOptionsPlugin,
ContextReplacementPlugin,
HotModuleReplacementPlugin,
NoEmitOnErrorsPlugin,
optimize,
} from 'webpack';
import path from 'path';
import { isatty } from 'tty';
import chalk from 'chalk';
import slash from 'slash';
import autoprefixer from 'autoprefixer';
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import HtmlPlugin from 'html-webpack-plugin';
import ProgressBarPlugin from 'progress-bar-webpack-plugin';
const {
CommonsChunkPlugin,
UglifyJsPlugin,
ModuleConcatenationPlugin,
} = optimize;
const locales = ['en'];
/**
* Plugin for HtmlPlugin which inlines content for an extracted Webpack manifest
* into the HTML in a <script> tag before other emitted asssets are injected by
* HtmlPlugin itself.
*/
function injectManifestPlugin() {
this.plugin('compilation', (compilation) => {
compilation.plugin('html-webpack-plugin-before-html-processing', (data, cb) => {
Object.keys(compilation.assets).forEach((key) => {
if (!key.startsWith('manifest.')) { return; }
const { children } = compilation.assets[key];
if (children && children[0]) {
// eslint-disable-next-line no-param-reassign
data.html = data.html.replace(/^(\s*)<\/body>/m, `$1<script>${children[0]._value}</script>\n$1</body>`);
// Remove the manifest from HtmlPlugin's assets to prevent a <script>
// tag being created for it.
const manifestIndex = data.assets.js.indexOf(data.assets.publicPath + key);
data.assets.js.splice(manifestIndex, 1);
// eslint-disable-next-line no-param-reassign
delete data.assets.chunks.manifest;
}
});
cb(null, data);
});
});
}
export default (options) => {
const environment = {
development: true,
production: false,
server: false,
};
function processEnvironmentString(env) {
switch (env.toLowerCase()) {
case 'development':
environment.development = true;
environment.production = false;
break;
case 'production':
environment.development = false;
environment.production = true;
break;
case 'server':
environment.server = true;
break;
default:
console.warn('Unknown environment:', env);
break;
}
}
if (options) {
switch (true) {
case typeof (options) === 'string': // string
processEnvironmentString(options);
break;
case Array.isArray(options): // array
options.forEach(processEnvironmentString);
break;
default: // object
Object.keys(options).forEach((k) => {
environment[k] = options[k] || environment[k];
});
break;
}
} else if (process.env.NODE_ENV) {
processEnvironmentString(process.env.NODE_ENV);
}
const {
development,
production,
server,
} = environment;
const baseOutputFilename = development
? 'assets/dev/[name].dev.[ext]'
// Always use a hash (in production) to prevent files with the same name causing issues
: 'assets/prod/[chunkhash:2]/[name].[chunkhash:8].[ext]';
const webpackChunkFilename = baseOutputFilename
.replace(/\[ext(.*?)\]/g, 'js');
const webpackOutputFilename = webpackChunkFilename;
const assetOutputFilename = baseOutputFilename
.replace(/\[chunkhash(.*?)\]/g, '[hash$1]');
const cssOutputFileName = baseOutputFilename
.replace(/\[ext(.*?)\]/g, 'css')
.replace(/\[chunkhash(.*?)\]/g, '[contenthash$1]');
const cssOutputRebasePath = `${slash(path.relative(path.dirname(cssOutputFileName), ''))}/`;
// Default options for url-loader
const urlLoaderOptions = {
limit: 1, // Don't inline anything (but empty files) by default
name: assetOutputFilename,
publicPath: cssOutputRebasePath,
};
function styleLoaders(...preprocessingLoaders) {
let cssLoaders = [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
sourceMap: true,
modules: true,
localIdentName: production
? '[name]__[local]--[hash:base64:5]'
: '[name]__[local]--[hash:base64:5]',
},
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [
autoprefixer({
browsers: '> 1%, last 4 versions, Firefox ESR, ios >= 8',
grid: false,
}),
],
sourceMap: true,
},
},
].filter(loader => loader !== false);
if (preprocessingLoaders && preprocessingLoaders.length > 0) {
cssLoaders.push(
{
loader: 'resolve-url-loader',
options: {
fail: true,
silent: false,
},
},
...preprocessingLoaders.map(loader => Object.assign({}, loader, {
options: Object.assign({}, loader.options || {}, {
sourceMap: true,
}),
})),
);
}
if (!server) {
const fallback = cssLoaders.shift();
cssLoaders = ExtractTextPlugin.extract({
fallback,
use: cssLoaders,
});
}
return cssLoaders;
}
return {
devServer: {
// inline: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
historyApiFallback: true,
hot: true,
noInfo: true,
overlay: true,
publicPath: '',
quiet: false,
watchOptions: {
ignored: /node_modules/,
},
},
module: {
rules: [
{
test: /\.jsx?/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
// Look for babel configuration in project directory
babelrc: true,
// Cache transformations to the filesystem (in default temp dir)
cacheDirectory: true,
// Skip module transpiling as Webpack 2+ support it ootb
forceEnv: 'browser',
},
},
...[
/\.(gif|png|webp)$/i, // graphics
/\.svg$/i, // svg
/\.jpe?g$/i, // jpeg
/\.(mp4|ogg|webm)$/i, // video
/\.(eot|otf|ttf|woff|woff2)$/i, // fonts
/\.(wav|mp3|m4a|aac|oga)$/i, // audio
].map(test => ({
test,
loader: 'url-loader',
options: urlLoaderOptions,
})),
{
test: /\.css$/,
use: styleLoaders(),
},
{
test: /\.s[ac]ss$/,
use: styleLoaders(
{
loader: 'sass-loader',
},
),
},
],
strictExportPresence: true,
},
output: {
filename: webpackOutputFilename,
chunkFilename: webpackChunkFilename,
path: path.join(__dirname, 'dist'),
publicPath: '',
},
plugins: [
// Show progress as a bar during build
isatty(1) && new ProgressBarPlugin({
complete: chalk.white('\u2588'),
incomplete: chalk.grey('\u2591'),
format: `:bar ${chalk.cyan.bold(':percent')} Webpack build: ${chalk.grey(':msg')}`,
}),
// Enforce case-sensitive import paths
new CaseSensitivePathsPlugin(),
// Replace specified expressions with values
new DefinePlugin({
'process.env.__DEV__': JSON.stringify(development),
'process.env.__PROD__': JSON.stringify(production),
'process.env.__SERVER__': JSON.stringify(server),
'process.env.NODE_ENV': JSON.stringify(production ? 'production' : 'development'),
}),
// Dev server build
...[
// Hot module reloading
new HotModuleReplacementPlugin(),
new NoEmitOnErrorsPlugin(),
// Use paths as names when serving
new NamedModulesPlugin(),
].filter(() => server),
// If we're not serving, we're creating a static build
...[// Extract imported stylesheets out into .css files
new ExtractTextPlugin({
allChunks: true,
filename: cssOutputFileName,
}),
].filter(() => !server),
// Move modules imported from node_modules/ into a vendor chunk when enabled
new CommonsChunkPlugin({
name: 'vendor',
minChunks(module) {
return (module.resource && module.resource.includes('node_modules'));
},
}),
// If we're generating an HTML file, we must be building a web app, so
// configure deterministic hashing for long-term caching.
// Generate stable module ids instead of having Webpack assign integers.
// NamedModulesPlugin allows for easier debugging and
// HashedModuleIdsPlugin does this without adding too much to bundle
// size.
development
? new NamedModulesPlugin()
: new HashedModuleIdsPlugin(),
// The Webpack manifest is normally folded into the last chunk, changing
// its hash - prevent this by extracting the manifest into its own
// chunk - also essential for deterministic hashing.
new CommonsChunkPlugin({ name: 'manifest' }),
// Inject the Webpack manifest into the generated HTML as a <script>
injectManifestPlugin,
// Production builds
...[
// JavaScript minification
new LoaderOptionsPlugin({ debug: false, minimize: true }),
new UglifyJsPlugin({
compress: {
warnings: false,
},
output: {
comments: false,
},
sourceMap: true,
}),
// Hoisting
new ModuleConcatenationPlugin(),
].filter(() => production),
new HtmlPlugin({
template: 'src/index.html',
filename: 'index.html',
hash: false,
inject: true,
compile: true,
favicon: false,
minify: false,
cache: true,
showErrors: true,
chunks: 'all',
excludeChunks: [],
title: 'VIzon Countdown',
xhtml: false,
chunksSortMode: 'dependency',
}),
// Only include selected locales for moment.js
new ContextReplacementPlugin(/moment[/\\]locale$/, new RegExp(`^\\.\\/(${locales.join('|')})$`)),
].filter(plugin => plugin !== false),
resolve: {
extensions: [
'.js', '.json', '.jsx',
],
alias: {
// Use preact instead of react
/* react: 'preact-compat\\dist\\preact-compat',
'react-dom': 'preact-compat\\dist\\preact-compat',
'create-react-class': 'preact-compat/lib/create-react-class', */
'moment-timezone': 'moment-timezone/builds/moment-timezone-with-data-2012-2022.js',
},
},
resolveLoader: {
modules: ['node_modules', 'nwb'],
},
devtool: server ? 'cheap-module-source-map' : 'source-map',
entry: {
app: [
...[
'eventsource-polyfill',
'react-hot-loader/patch',
'webpack-hot-middleware/client',
].filter(() => server),
'./src',
],
},
};
};