387 lines
11 KiB
JavaScript
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: true,
|
|
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: 'Webpack App',
|
|
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',
|
|
],
|
|
},
|
|
};
|
|
};
|