refactor(v2): simplify webpack stuff (#1342)

* refactor(v2): webpack stuff

* nits

* nits test
This commit is contained in:
Endilie Yacop Sucipto 2019-04-08 01:40:38 +07:00 committed by GitHub
parent 2248b4b927
commit 84c3e0a045
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 254 additions and 334 deletions

View file

@ -6,6 +6,7 @@
*/
const webpack = require('webpack');
const merge = require('webpack-merge');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
const path = require('path');
@ -48,16 +49,16 @@ module.exports = async function build(siteDir, cliOptions = {}) {
// Apply user webpack config.
const {outDir, plugins} = props;
const clientConfigObj = createClientConfig(props);
// Remove/clean build folders before building bundles.
clientConfigObj.plugin('clean').use(CleanWebpackPlugin, [{verbose: false}]);
// Visualize size of webpack output files with an interactive zoomable treemap.
if (cliOptions.bundleAnalyzer) {
clientConfigObj.plugin('bundleAnalyzer').use(BundleAnalyzerPlugin);
}
let clientConfig = merge(createClientConfig(props), {
plugins: [
// Remove/clean build folders before building bundles.
new CleanWebpackPlugin({verbose: false}),
// Visualize size of webpack output files with an interactive zoomable treemap.
cliOptions.bundleAnalyzer && new BundleAnalyzerPlugin(),
].filter(Boolean),
});
let clientConfig = clientConfigObj.toConfig();
let serverConfig = createServerConfig(props).toConfig();
let serverConfig = createServerConfig(props);
// Plugin lifecycle - configureWebpack
plugins.forEach(plugin => {

View file

@ -17,6 +17,7 @@ const {prepareUrls} = require('react-dev-utils/WebpackDevServerUtils');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
const WebpackDevServer = require('webpack-dev-server');
const merge = require('webpack-merge');
const {normalizeUrl} = require('@docusaurus/utils');
const load = require('../load');
const loadConfig = require('../load/config');
@ -83,24 +84,24 @@ module.exports = async function start(siteDir, cliOptions = {}) {
const openUrl = normalizeUrl([urls.localUrlForBrowser, baseUrl]);
// Create compiler from generated webpack config.
let config = createClientConfig(props);
const {siteConfig, plugins = []} = props;
config.plugin('html-webpack-plugin').use(HtmlWebpackPlugin, [
{
inject: false,
hash: true,
template: path.resolve(
__dirname,
'../core/templates/index.html.template.ejs',
),
filename: 'index.html',
title: siteConfig.title,
},
]);
// Needed for hot reload.
config.plugin('hmr').use(HotModuleReplacementPlugin);
config = config.toConfig();
let config = merge(createClientConfig(props), {
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin({
inject: false,
hash: true,
template: path.resolve(
__dirname,
'../core/templates/index.html.template.ejs',
),
filename: 'index.html',
title: siteConfig.title,
}),
// This is necessary to emit hot updates for webpack-dev-server
new HotModuleReplacementPlugin(),
],
});
// Plugin lifecycle - configureWebpack
plugins.forEach(plugin => {

View file

@ -5,13 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
const CSSExtractPlugin = require('mini-css-extract-plugin');
const Config = require('webpack-chain');
const cacheLoaderVersion = require('cache-loader/package.json').version;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const path = require('path');
const isWsl = require('is-wsl');
const {applyBabel, applyCacheLoader, applyStyle} = require('./utils');
const {getBabelLoader, getCacheLoader, getStyleLoaders} = require('./utils');
const CSS_REGEX = /\.css$/;
const CSS_MODULE_REGEX = /\.module\.css$/;
@ -26,133 +24,114 @@ module.exports = function createBaseConfig(props, isServer) {
cliOptions: {cacheLoader},
} = props;
const config = new Config();
const isProd = process.env.NODE_ENV === 'production';
config
.mode(isProd ? 'production' : 'development')
.output.path(outDir)
.filename(isProd ? '[name].[chunkhash].js' : '[name].js')
.chunkFilename(isProd ? '[name].[chunkhash].js' : '[name].js')
.publicPath(baseUrl);
if (!isProd) {
config.devtool('cheap-module-eval-source-map');
}
config.resolve
.set('symlinks', true)
.alias.set('@theme', themePath)
.set('@site', siteDir)
.set('@build', outDir)
.set('@generated', generatedFilesDir)
.set('@core', path.resolve(__dirname, '../core'))
.set('@docusaurus', path.resolve(__dirname, '../docusaurus'))
.end()
.modules.add(path.resolve(__dirname, '../../node_modules')) // Prioritize our own node modules.
.add(path.resolve(siteDir, 'node_modules')) // load user node_modules
.add(path.resolve(process.cwd(), 'node_modules'))
.add('node_modules');
const jsRule = config.module
.rule('js')
.test(/\.jsx?$/)
.exclude.add(filepath => {
// Always transpile lib directory
if (filepath.startsWith(path.join(__dirname, '..'))) {
return false;
}
// Don't transpile node_modules
return /node_modules/.test(filepath);
})
.end();
applyCacheLoader(jsRule, {
cacheLoader,
siteDir,
cacheLoaderVersion,
isServer,
});
applyBabel(jsRule, {isServer});
applyStyle(config.module.rule('css'), {
cssOptions: {
importLoaders: 1,
sourceMap: !isProd,
minimize: true,
return {
mode: isProd ? 'production' : 'development',
output: {
path: outDir,
filename: isProd ? '[name].[chunkhash].js' : '[name].js',
chunkFilename: isProd ? '[name].[chunkhash].js' : '[name].js',
publicPath: baseUrl,
},
isProd,
isServer,
})
.test(CSS_REGEX)
.exclude.add(CSS_MODULE_REGEX)
.end();
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
applyStyle(config.module.rule('css-module'), {
cssOptions: {
modules: true,
importLoaders: 1,
localIdentName: `[local]_[hash:base64:8]`,
sourceMap: !isProd,
minimize: true,
},
isProd,
isServer,
}).test(CSS_MODULE_REGEX);
// mini-css-extract plugin
config.plugin('extractCSS').use(CSSExtractPlugin, [
{
filename: isProd ? '[name].[chunkhash].css' : '[name].css',
chunkFilename: isProd ? '[name].[chunkhash].css' : '[name].css',
},
]);
// https://webpack.js.org/plugins/split-chunks-plugin/
config.optimization.splitChunks({
// We set max requests to Infinity because of HTTP/2
maxInitialRequests: Infinity,
maxAsyncRequests: Infinity,
cacheGroups: {
// disable the built-in cacheGroups
default: false,
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 20,
// create chunk regardless of the size of the chunk
enforce: true,
},
common: {
name: 'common',
chunks: 'all',
minChunks: 2,
priority: 10,
reuseExistingChunk: true,
enforce: true,
devtool: !isProd && 'cheap-module-eval-source-map',
resolve: {
symlinks: true,
alias: {
'@theme': themePath,
'@site': siteDir,
'@build': outDir,
'@generated': generatedFilesDir,
'@core': path.resolve(__dirname, '../core'),
'@docusaurus': path.resolve(__dirname, '../docusaurus'),
},
modules: [
'node_modules',
path.resolve(__dirname, '../../node_modules'),
path.resolve(siteDir, 'node_modules'),
path.resolve(process.cwd(), 'node_modules'),
],
},
});
if (isProd) {
config.optimization.minimizer([
new TerserPlugin({
cache: true,
// We can't run in parallel for WSL due to upstream bug
// https://github.com/webpack-contrib/terser-webpack-plugin/issues/21
parallel: !isWsl,
sourceMap: true,
terserOptions: {
ecma: 6,
mangle: true,
output: {
comments: false,
optimization: {
// Only minimize client bundle in production because server bundle is only used for static site generation
minimize: isProd && !isServer,
minimizer: [
new TerserPlugin({
cache: true,
// Disabled on WSL (Windows Subsystem for Linux) due to an issue with Terser
// https://github.com/webpack-contrib/terser-webpack-plugin/issues/21
parallel: !isWsl,
sourceMap: true,
terserOptions: {
ecma: 6,
mangle: true,
output: {
comments: false,
},
},
}),
],
splitChunks: {
maxInitialRequests: Infinity,
maxAsyncRequests: Infinity,
cacheGroups: {
// disable the built-in cacheGroups
default: false,
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 20,
// create chunk regardless of the size of the chunk
enforce: true,
},
common: {
name: 'common',
chunks: 'all',
minChunks: 2,
priority: 10,
reuseExistingChunk: true,
enforce: true,
},
},
},
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
cacheLoader && getCacheLoader(isServer),
getBabelLoader(isServer),
].filter(Boolean),
},
{
test: CSS_REGEX,
exclude: CSS_MODULE_REGEX,
use: getStyleLoaders(isServer, {
importLoaders: 1,
sourceMap: !isProd,
minimize: true,
}),
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: CSS_MODULE_REGEX,
use: getStyleLoaders(isServer, {
modules: true,
importLoaders: 1,
localIdentName: `[local]_[hash:base64:8]`,
sourceMap: !isProd,
minimize: true,
}),
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: isProd ? '[name].[chunkhash].css' : '[name].css',
chunkFilename: isProd ? '[name].[chunkhash].css' : '[name].css',
}),
]);
}
return config;
],
};
};

View file

@ -6,43 +6,44 @@
*/
const path = require('path');
const webpackNiceLog = require('webpack-nicelog');
const WebpackNiceLog = require('webpack-nicelog');
const {StatsWriterPlugin} = require('webpack-stats-plugin');
const {ReactLoadablePlugin} = require('react-loadable/webpack');
const merge = require('webpack-merge');
const createBaseConfig = require('./base');
const {applyChainWebpack} = require('./utils');
module.exports = function createClientConfig(props) {
const isProd = process.env.NODE_ENV === 'production';
const config = createBaseConfig(props);
config.entry('main').add(path.resolve(__dirname, '../core/clientEntry.js'));
// https://github.com/gaearon/react-hot-loader#react--dom
// To enable react-hot-loader in development
if (!isProd) {
config.resolve.alias.set('react-dom', '@hot-loader/react-dom').end();
}
const {generatedFilesDir} = props;
// Write webpack stats object so we can pickup correct client bundle path in server.
config
.plugin('clientStats')
.use(StatsWriterPlugin, [{filename: 'client.stats.json'}]);
config
.plugin('reactLoadableStats')
.use(ReactLoadablePlugin, [
{filename: path.join(generatedFilesDir, 'react-loadable.json')},
]);
// Show compilation progress bar and build time.
config
.plugin('niceLog')
.use(webpackNiceLog, [{name: 'Client', skipBuildTime: isProd}]);
const clientConfig = merge(config, {
entry: {
main: path.resolve(__dirname, '../core/clientEntry.js'),
},
resolve: {
alias: {
// https://github.com/gaearon/react-hot-loader#react--dom
'react-dom': '@hot-loader/react-dom',
},
},
plugins: [
// Write webpack stats object so we can pickup correct client bundle path in server.
new StatsWriterPlugin({
filename: 'client.stats.json',
}),
// React-loadable manifests
new ReactLoadablePlugin({
filename: path.join(generatedFilesDir, 'react-loadable.json'),
}),
// Show compilation progress bar and build time.
new WebpackNiceLog({
name: 'Client',
skipBuildTime: isProd,
}),
],
});
// User-extended webpack-chain config.
applyChainWebpack(props.siteConfig.chainWebpack, config, false);
return config;
return clientConfig;
};

View file

@ -7,47 +7,48 @@
const path = require('path');
const StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin');
const webpackNiceLog = require('webpack-nicelog');
const WebpackNiceLog = require('webpack-nicelog');
const merge = require('webpack-merge');
const createBaseConfig = require('./base');
const {applyChainWebpack} = require('./utils');
module.exports = function createServerConfig(props) {
const {baseUrl, routesPaths} = props;
const config = createBaseConfig(props, true);
config.entry('main').add(path.resolve(__dirname, '../core/serverEntry.js'));
config.target('node');
config.resolve.alias.set('ejs', 'ejs/ejs.min.js').end();
config.output.filename('server.bundle.js').libraryTarget('commonjs2');
// no need to minimize server bundle since we only run server compilation to generate static html files
config.optimization.minimize(false);
// Workaround for Webpack 4 Bug (https://github.com/webpack/webpack/issues/6522)
config.output.globalObject('this');
const {siteConfig, routesPaths} = props;
// Static site generator webpack plugin.
config.plugin('siteGenerator').use(StaticSiteGeneratorPlugin, [
{
entry: 'main',
locals: {
baseUrl: siteConfig.baseUrl,
},
paths: routesPaths,
},
]);
// Show compilation progress bar.
const isProd = process.env.NODE_ENV === 'production';
config
.plugin('niceLog')
.use(webpackNiceLog, [
{name: 'Server', color: 'yellow', skipBuildTime: isProd},
]);
// User-extended webpack-chain config.
applyChainWebpack(props.siteConfig.chainWebpack, config, true);
const serverConfig = merge(config, {
entry: {
main: path.resolve(__dirname, '../core/serverEntry.js'),
},
output: {
filename: 'server.bundle.js',
libraryTarget: 'commonjs2',
// Workaround for Webpack 4 Bug (https://github.com/webpack/webpack/issues/6522)
globalObject: 'this',
},
target: 'node',
resolve: {
alias: {
ejs: 'ejs/ejs.min.js',
},
},
plugins: [
// Static site generator webpack plugin.
new StaticSiteGeneratorPlugin({
entry: 'main',
locals: {
baseUrl,
},
paths: routesPaths,
}),
return config;
// Show compilation progress bar.
new WebpackNiceLog({
name: 'Server',
color: 'yellow',
skipBuildTime: isProd,
}),
],
});
return serverConfig;
};

View file

@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
const CSSExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const cacheLoaderVersion = require('cache-loader/package.json').version;
const merge = require('webpack-merge');
// Modify the generated webpack config with normal webpack config.
@ -23,68 +23,59 @@ function applyConfigureWebpack(userConfig, config, isServer) {
return config;
}
// Modify the generated webpack config with webpack-chain API.
function applyChainWebpack(userChainWebpack, config, isServer) {
if (userChainWebpack) {
userChainWebpack(config, isServer);
}
// Utility method to get style loaders
function getStyleLoaders(isServer, cssOptions) {
const isProd = process.env.NODE_ENV === 'production';
const loaders = [
!isServer &&
isProd && {
loader: MiniCssExtractPlugin.loader,
},
!isServer && !isProd && require.resolve('style-loader'),
{
loader: isServer
? require.resolve('css-loader/locals')
: require.resolve('css-loader'),
options: cssOptions,
},
].filter(Boolean);
return loaders;
}
// Utility method to add styling-related rule to Webpack config.
function applyStyle(styleRule, {cssOptions, isServer, isProd}) {
if (!isServer) {
if (isProd) {
styleRule.use('extract-css-loader').loader(CSSExtractPlugin.loader);
} else {
styleRule.use('style-loader').loader('style-loader');
}
}
styleRule
.use('css-loader')
.loader(isServer ? 'css-loader/locals' : 'css-loader')
.options(cssOptions);
return styleRule;
}
function applyCacheLoader(
rule,
{cacheLoader, siteDir, cacheLoaderVersion, isServer},
) {
if (cacheLoader) {
rule
.use('cache-loader')
.loader('cache-loader')
.options({
cacheDirectory: path.resolve(siteDir, '.cache-loader'),
function getCacheLoader(isServer, cacheOptions) {
return {
loader: require.resolve('cache-loader'),
options: Object.assign(
{
cacheIdentifier: `cache-loader:${cacheLoaderVersion}${isServer}`,
});
}
},
cacheOptions,
),
};
}
function applyBabel(rule, {isServer}) {
rule
.use('babel')
.loader('babel-loader')
.options({
// ignore local project babel config (.babelrc)
babelrc: false,
// ignore local project babel config (babel.config.js)
configFile: false,
presets: ['@babel/env', '@babel/react'],
plugins: [
'react-hot-loader/babel', // To enable react-hot-loader
isServer ? 'dynamic-import-node' : '@babel/syntax-dynamic-import',
'react-loadable/babel',
],
});
function getBabelLoader(isServer, babelOptions) {
return {
loader: require.resolve('babel-loader'),
options: Object.assign(
{
babelrc: false,
configFile: false,
presets: ['@babel/env', '@babel/react'],
plugins: [
'react-hot-loader/babel',
isServer ? 'dynamic-import-node' : '@babel/syntax-dynamic-import',
'react-loadable/babel',
],
},
babelOptions,
),
};
}
module.exports = {
applyBabel,
applyCacheLoader,
getBabelLoader,
getCacheLoader,
getStyleLoaders,
applyConfigureWebpack,
applyChainWebpack,
applyStyle,
};

View file

@ -70,7 +70,6 @@
"terser-webpack-plugin": "^1.2.3",
"webpack": "^4.26.0",
"webpack-bundle-analyzer": "^3.1.0",
"webpack-chain": "^4.9.0",
"webpack-dev-server": "^3.2.1",
"webpack-merge": "^4.1.4",
"webpack-nicelog": "^2.3.1",

View file

@ -13,7 +13,7 @@ describe('webpack base config', () => {
test('simple', async () => {
console.log = jest.fn();
const props = await loadSetup('simple');
const config = createBaseConfig(props).toConfig();
const config = createBaseConfig(props);
const errors = validate(config);
expect(errors.length).toBe(0);
});
@ -21,7 +21,7 @@ describe('webpack base config', () => {
test('custom', async () => {
console.log = jest.fn();
const props = await loadSetup('custom');
const config = createBaseConfig(props).toConfig();
const config = createBaseConfig(props);
const errors = validate(config);
expect(errors.length).toBe(0);
});

View file

@ -13,7 +13,7 @@ describe('webpack dev config', () => {
test('simple', async () => {
console.log = jest.fn();
const props = await loadSetup('simple');
const config = createClientConfig(props).toConfig();
const config = createClientConfig(props);
const errors = validate(config);
expect(errors.length).toBe(0);
});
@ -21,7 +21,7 @@ describe('webpack dev config', () => {
test('custom', async () => {
console.log = jest.fn();
const props = await loadSetup('custom');
const config = createClientConfig(props).toConfig();
const config = createClientConfig(props);
const errors = validate(config);
expect(errors.length).toBe(0);
});

View file

@ -13,7 +13,7 @@ describe('webpack production config', () => {
test('simple', async () => {
console.log = jest.fn();
const props = await loadSetup('simple');
const config = createServerConfig(props).toConfig();
const config = createServerConfig(props);
const errors = validate(config);
expect(errors.length).toBe(0);
});
@ -21,7 +21,7 @@ describe('webpack production config', () => {
test('custom', async () => {
console.log = jest.fn();
const props = await loadSetup('custom');
const config = createServerConfig(props).toConfig();
const config = createServerConfig(props);
const errors = validate(config);
expect(errors.length).toBe(0);
});

View file

@ -8,9 +8,8 @@
import '@babel/polyfill';
import {validate} from 'webpack';
import path from 'path';
import Config from 'webpack-chain';
import {applyConfigureWebpack, applyChainWebpack} from '@lib/webpack/utils';
import {applyConfigureWebpack} from '@lib/webpack/utils';
describe('extending generated webpack config', () => {
test('direct mutation on generated webpack config object', async () => {
@ -76,38 +75,4 @@ describe('extending generated webpack config', () => {
const errors = validate(config);
expect(errors.length).toBe(0);
});
test('use webpack-chain API', async () => {
// fake generated webpack config in webpack-chain format
let config = new Config();
config.output.path(__dirname).filename('bundle.js');
// user chainWebpack
/* eslint-disable */
const chainWebpack = (oldConfig, isServer) => {
if (!isServer) {
oldConfig.entry('main').add('./entry.js');
oldConfig.output
.path(path.join(__dirname, 'dist'))
.filename('new.bundle.js');
}
};
/* eslint-enable */
applyChainWebpack(chainWebpack, config, false);
// transform to webpack configuration object format
config = config.toConfig();
expect(config).toEqual({
output: {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
},
entry: {
main: ['./entry.js'],
},
});
const errors = validate(config);
expect(errors.length).toBe(0);
});
});

View file

@ -4347,11 +4347,6 @@ deepmerge@3.2.0:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.2.0.tgz#58ef463a57c08d376547f8869fdc5bcee957f44e"
integrity sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow==
deepmerge@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==
deepmerge@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
@ -7536,11 +7531,6 @@ isurl@^1.0.0-alpha5:
has-to-string-tag-x "^1.2.0"
is-object "^1.0.1"
javascript-stringify@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3"
integrity sha1-FC0RHzpuPa6PSpr9d9RYVbWpzOM=
jest-changed-files@^24.5.0:
version "24.5.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.5.0.tgz#4075269ee115d87194fd5822e642af22133cf705"
@ -13473,14 +13463,6 @@ webpack-bundle-analyzer@^3.1.0:
opener "^1.5.1"
ws "^6.0.0"
webpack-chain@^4.9.0:
version "4.12.1"
resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-4.12.1.tgz#6c8439bbb2ab550952d60e1ea9319141906c02a6"
integrity sha512-BCfKo2YkDe2ByqkEWe1Rw+zko4LsyS75LVr29C6xIrxAg9JHJ4pl8kaIZ396SUSNp6b4815dRZPSTAS8LlURRQ==
dependencies:
deepmerge "^1.5.2"
javascript-stringify "^1.6.0"
webpack-dev-middleware@^3.5.1:
version "3.6.1"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.1.tgz#91f2531218a633a99189f7de36045a331a4b9cd4"