Skip to content

Commit 74dbf78

Browse files
committed
Update of code chunk splitting (AppChunk) code for Webpack@4
- Switch from `extract-css-chunks-webpack-plugin` to `mini-css-extract-plugin`, which comes from Wepback team; - Use Webpack stats to only load those CSS chunks that exist (the problem is not solved at the client-side yet); - Fix of styles hot reloading for Webpack@4 compatibility. Fix for #22
1 parent 5962dfb commit 74dbf78

File tree

7 files changed

+120
-45
lines changed

7 files changed

+120
-45
lines changed

config/webpack/app-base.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77
const _ = require('lodash');
88
const autoprefixer = require('autoprefixer');
9-
const ExtractCssChunks = require('extract-css-chunks-webpack-plugin');
9+
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
1010
const forge = require('node-forge');
1111
const fs = require('fs');
1212
const moment = require('moment');
1313
const path = require('path');
14+
const { StatsWriterPlugin } = require('webpack-stats-plugin');
1415
const webpack = require('webpack');
1516

1617
/**
@@ -104,13 +105,16 @@ module.exports = function configFactory(ops) {
104105
publicPath: `${o.publicPath}/`,
105106
},
106107
plugins: [
107-
new ExtractCssChunks({
108+
new MiniCssExtractPlugin({
108109
chunkFilename: `[name]-${now.valueOf()}.css`,
109110
filename: `[name]-${now.valueOf()}.css`,
110111
}),
111112
new webpack.DefinePlugin({
112113
BUILD_INFO: JSON.stringify(buildInfo),
113114
}),
115+
new StatsWriterPlugin({
116+
filename: '__stats__.json',
117+
}),
114118
],
115119
resolve: {
116120
alias: {
@@ -158,7 +162,7 @@ module.exports = function configFactory(ops) {
158162
/* Loads SCSS stylesheets. */
159163
test: /\.scss/,
160164
use: [
161-
ExtractCssChunks.loader, {
165+
MiniCssExtractPlugin.loader, {
162166
loader: 'css-loader',
163167
options: {
164168
localIdentName: o.cssLocalIdent,
@@ -181,10 +185,16 @@ module.exports = function configFactory(ops) {
181185
* from dependencies, as we use SCSS inside our own code. */
182186
test: /\.css$/,
183187
use: [
184-
ExtractCssChunks.loader,
188+
MiniCssExtractPlugin.loader,
185189
'css-loader',
186190
],
187191
}],
188192
},
193+
optimization: {
194+
/* TODO: Dynamic chunk splitting does not play along with server-side
195+
* rendering of split chunks. Probably there is a way to achieve that,
196+
* but it is not a priority now. */
197+
splitChunks: false,
198+
},
189199
};
190200
};

config/webpack/lib-base.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
const autoprefixer = require('autoprefixer');
8-
const ExtractCssChunks = require('extract-css-chunks-webpack-plugin');
8+
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
99
const path = require('path');
1010
const webpack = require('webpack');
1111

@@ -78,7 +78,7 @@ module.exports = function configFactory(ops) {
7878
NODE_ENV: JSON.stringify(ops.babelEnv),
7979
},
8080
}),
81-
new ExtractCssChunks({
81+
new MiniCssExtractPlugin({
8282
filename: 'style.css',
8383
}),
8484
],
@@ -118,7 +118,7 @@ module.exports = function configFactory(ops) {
118118
test: /\.scss/,
119119
exclude: /node_modules/,
120120
use: [
121-
ExtractCssChunks.loader, {
121+
MiniCssExtractPlugin.loader, {
122122
loader: 'css-loader',
123123
options: {
124124
importLoaders: 3,
@@ -144,7 +144,7 @@ module.exports = function configFactory(ops) {
144144
* from dependencies, as we use SCSS inside our own code. */
145145
test: /\.css$/,
146146
use: [
147-
ExtractCssChunks.loader,
147+
MiniCssExtractPlugin.loader,
148148
'css-loader',
149149
],
150150
}],

package-lock.json

Lines changed: 48 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@
6464
"eslint-plugin-jest": "^21.17.0",
6565
"eslint-plugin-jsx-a11y": "^6.0.3",
6666
"eslint-plugin-react": "^7.10.0",
67-
"extract-css-chunks-webpack-plugin": "^3.0.6",
6867
"file-loader": "^1.1.11",
6968
"identity-obj-proxy": "^3.0.0",
7069
"jest": "^23.2.0",
7170
"jsdoc-to-markdown": "^4.0.1",
71+
"mini-css-extract-plugin": "^0.4.1",
7272
"mkpath": "^1.0.0",
7373
"mockdate": "^2.0.2",
7474
"node-sass": "^4.9.0",
@@ -88,7 +88,8 @@
8888
"webpack-cli": "^3.0.8",
8989
"webpack-dev-middleware": "^3.1.3",
9090
"webpack-hot-middleware": "^2.22.2",
91-
"webpack-merge": "^4.1.3"
91+
"webpack-merge": "^4.1.3",
92+
"webpack-stats-plugin": "^0.2.1"
9293
},
9394
"engines": {
9495
"node": "~8.11.2",

src/client/index.jsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,13 @@ export default async function Launch({
5757
() => render(getApplication(), store));
5858

5959
/* HMR of CSS code each time webpack hot middleware updates the code. */
60-
/* eslint-disable no-underscore-dangle */
61-
const hotReporter = window.__webpack_hot_middleware_reporter__;
62-
/* eslint-enable no-underscore-dangle */
63-
const hotSuccess = hotReporter.success;
64-
hotReporter.success = () => {
60+
moduleHot.addStatusHandler((status) => {
61+
if (status !== 'ready') return;
6562
const stamp = shortId();
6663
const links = document.querySelectorAll('link[rel=stylesheet][id="tru-style"]');
6764
for (let i = 0; i < links.length; i += 1) {
6865
links[i].href = `${links[i].href.match(/[^?]*/)}?v=${stamp}`;
6966
}
70-
hotSuccess();
71-
};
67+
});
7268
}
7369
}

src/server/renderer.jsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,31 @@ const sanitizedConfig = _.omit(config, 'SECRET');
2525
* At the moment, that file contains build timestamp and a random 32-bit key,
2626
* suitable for cryptographical use.
2727
* @param {String} context Webpack context path used during the build.
28-
* @return {Promise} Resolves to the build-time information.
28+
* @return {Object} Resolves to the build-time information.
2929
*/
3030
function getBuildInfo(context) {
3131
const url = path.resolve(context, '.build-info');
3232
return JSON.parse(fs.readFileSync(url));
3333
}
3434

35+
/**
36+
* Attempts to read from the disk Webpack stats generated during the build.
37+
* It will not work for development builds, where these stats should be captured
38+
* via compilator callback.
39+
* @param {String} buildDir
40+
* @return {Object} Resolves to the stats, or null, if cannot be read.
41+
*/
42+
function getWebpackStats(buildDir) {
43+
const url = path.resolve(buildDir, '__stats__.json');
44+
let res;
45+
try {
46+
res = JSON.parse(fs.readFileSync(url));
47+
} catch (err) {
48+
res = null;
49+
}
50+
return res;
51+
}
52+
3553
/**
3654
* Prepares a new Cipher for data encryption.
3755
* @param {String} key Encryption key (32-bit random key is expected, see
@@ -65,6 +83,8 @@ export default function factory(webpackConfig, options) {
6583

6684
global.TRU_BUILD_INFO = buildInfo;
6785

86+
const WEBPACK_STATS = getWebpackStats(webpackConfig.output.path);
87+
6888
const ops = _.defaults(_.clone(options), {
6989
beforeRender: () => Promise.resolve({}),
7090
});
@@ -139,12 +159,26 @@ export default function factory(webpackConfig, options) {
139159
/* It is supposed to end with '/' symbol as path separator. */
140160
const { publicPath } = webpackConfig.output;
141161

162+
let assetsByChunkName;
163+
const { webpackStats } = res.locals;
164+
if (webpackStats) {
165+
({ assetsByChunkName } = webpackStats.toJson());
166+
} else ({ assetsByChunkName } = WEBPACK_STATS);
167+
142168
const timestamp = moment(buildInfo.timestamp).valueOf();
143169

144170
if (context.status) res.status(context.status);
145-
const styles = context.chunks.map(chunk => (
146-
`<link data-chunk="${chunk}" id="tru-style" href="${publicPath}${chunk}-${timestamp}.css" rel="stylesheet" />`
147-
)).join('');
171+
const styles = [];
172+
context.chunks.forEach((chunk) => {
173+
let assets = assetsByChunkName[chunk];
174+
if (!_.isArray(assets)) assets = [assets];
175+
assets = assets.filter(asset => asset.endsWith('.css'));
176+
assets.forEach((asset) => {
177+
styles.push((
178+
`<link href="${publicPath}${asset}" id="tru-style" rel="stylesheet" />`
179+
));
180+
});
181+
});
148182

149183
res.send((
150184
`<!DOCTYPE html>
@@ -157,7 +191,7 @@ export default function factory(webpackConfig, options) {
157191
id="tru-style"
158192
rel="stylesheet"
159193
/>
160-
${styles}
194+
${styles.join('')}
161195
<link rel="shortcut icon" href="/favicon.ico" />
162196
<meta charset="utf-8" />
163197
<meta

src/shared/containers/AppChunk/index.jsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default class SplitRoute extends React.Component {
4242
* of loaded stylesheets, which is how our CSS should be written. */
4343
const { chunkName } = this.props;
4444
const link = document.querySelector(`link[data-chunk="${chunkName}"]`);
45-
link.setAttribute('data-chunk-unused', unusedCssStamp += 1);
45+
if (link) link.setAttribute('data-chunk-unused', unusedCssStamp += 1);
4646

4747
/* Reset to the initial state. */
4848
this.setState({ component: null });
@@ -100,14 +100,14 @@ export default class SplitRoute extends React.Component {
100100
/* eslint-disable react/prop-types */
101101
const { splits } = props.staticContext;
102102
/* eslint-enable react/prop-types */
103-
if (splits[`${chunkName}`]) throw new Error('SplitRoute: IDs clash!');
104-
else splits[`${chunkName}`] = html;
103+
if (splits[chunkName]) throw new Error('SplitRoute: IDs clash!');
104+
else splits[chunkName] = html;
105105

106106
/* 3. The stylesheet links are injected via links elements in the
107107
* header of the document, to have a better control over styles
108108
* (re-)loading, independent of ReactJS mechanics of
109109
* the document updates. */
110-
props.staticContext.chunks.push(`${chunkName}`);
110+
props.staticContext.chunks.push(chunkName);
111111

112112
/* 4. We also render the mounted component, or the placeholder,
113113
* into the document, using dangerouslySetInnerHTML to inject
@@ -121,14 +121,14 @@ export default class SplitRoute extends React.Component {
121121
/* eslint-enable react/no-danger */
122122
} else {
123123
/* Client side rendering */
124-
if (window.SPLITS[`${chunkName}`]) {
124+
if (window.SPLITS[chunkName]) {
125125
/* If the page has been pre-rendered at the server-side, we render
126126
* exactly the same until the splitted code is loaded. */
127127
/* eslint-disable react/no-danger */
128128
res = (
129129
<div
130130
dangerouslySetInnerHTML={{
131-
__html: window.SPLITS[`${chunkName}`],
131+
__html: window.SPLITS[chunkName],
132132
}}
133133
/>
134134
);
@@ -138,7 +138,7 @@ export default class SplitRoute extends React.Component {
138138
* because if the vistor navigates around the app and comes back
139139
* to this route, we want to re-render the page from scratch in
140140
* that case (because the state of app has changed). */
141-
delete window.SPLITS[`${chunkName}`];
141+
delete window.SPLITS[chunkName];
142142
} else if (renderPlaceholder) {
143143
/* If the page has not been pre-rendered, the best we can do prior
144144
* the loading of split code, is to render the placeholder, if
@@ -169,7 +169,7 @@ export default class SplitRoute extends React.Component {
169169
link.removeAttribute('data-chunk-unused');
170170
} else {
171171
link = document.createElement('link');
172-
link.setAttribute('data-chunk', `${chunkName}`);
172+
link.setAttribute('data-chunk', chunkName);
173173
link.setAttribute('href', `${PUBLIC_PATH}/${chunkName}-${timestamp}.css`);
174174
link.setAttribute('id', 'tru-style');
175175
link.setAttribute('rel', 'stylesheet');
@@ -229,7 +229,7 @@ export default class SplitRoute extends React.Component {
229229
component: () => (
230230
<div>
231231
<ContentWrapper
232-
chunkName={`${chunkName}`}
232+
chunkName={chunkName}
233233
content={component2}
234234
parent={this}
235235
/>
@@ -249,7 +249,6 @@ export default class SplitRoute extends React.Component {
249249
}
250250

251251
SplitRoute.defaultProps = {
252-
// cacheCss: false,
253252
exact: false,
254253
location: null,
255254
path: null,
@@ -259,7 +258,6 @@ SplitRoute.defaultProps = {
259258
};
260259

261260
SplitRoute.propTypes = {
262-
// cacheCss: PT.bool,
263261
chunkName: PT.string.isRequired,
264262
exact: PT.bool,
265263
location: PT.shape(),

0 commit comments

Comments
 (0)