Skip to content

Commit a9b036a

Browse files
committed
feat($dualImport): the 2.0 requires babel-plugin-dual-import to fetch js + css (faster builds!)
1 parent f73b148 commit a9b036a

File tree

4 files changed

+86
-132
lines changed

4 files changed

+86
-132
lines changed

README.md

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,41 +17,45 @@
1717
</p>
1818

1919
# extract-css-chunks-webpack-plugin
20-
> TIP: remove `style-loader` from your dependencies. It's included in this package and must resolve to the correct *latest* version (June 2017).
20+
> **UPDATE (July 7th):** [babel-plugin-dual-import](https://github.com/faceyspacey/babel-plugin-dual-import) is now required to asynchronously import both css + js. *Much Faster Builds!*
2121
2222
Like `extract-text-webpack-plugin`, but creates multiple css files (one per chunk). Then, as part of server side rendering, you can deliver just the css chunks needed by the current request. The result is the most minimal CSS initially served compared to emerging JS-in-CSS solutions.
2323

2424
*Note: this is a companion package to:*
2525
- [webpack-flush-chunks](https://github.com/faceyspacey/webpack-flush-chunks)
2626
- [react-universal-component](https://github.com/faceyspacey/react-universal-component)
27+
- [babel-plugin-dual-import](https://github.com/faceyspacey/babel-plugin-dual-import)
2728

2829
For a complete usage example, see the [Flush Chunks Boilerplates](https://github.com/faceyspacey/webpack-flush-chunks#boilerplates).
2930

3031
Here's the sort of CSS you can expect to serve:
3132

3233
```
3334
<head>
34-
<link rel='stylesheet' href='/static/0.css' />
3535
<link rel='stylesheet' href='/static/main.css' />
36+
<link rel='stylesheet' href='/static/0.css' />
37+
<link rel='stylesheet' href='/static/7.css' />
3638
</head>
3739
3840
<body>
3941
<div id="react-root"></div>
4042
4143
<script type='text/javascript' src='/static/vendor.js'></script>
42-
<script type='text/javascript' src='/static/0.no_css.js'></script>
43-
<script type='text/javascript' src='/static/main.no_css.js'></script>
44+
<script type='text/javascript' src='/static/0.js'></script>
45+
<script type='text/javascript' src='/static/7.js'></script>
46+
<script type='text/javascript' src='/static/main.js'></script>
4447
</body>
4548
```
4649

4750
If you use [webpack-flush-chunks](https://github.com/faceyspacey/webpack-flush-chunks), it will scoop up the exact stylesheets to embed in your response string for you.
4851

52+
[babel-plugin-dual-import](https://github.com/faceyspacey/babel-plugin-dual-import) is required for ascynchronous requests via `import()`. It requests both your js + your css. *Very Nice!* Read *Sokra's* (author of webpack) article on how [on how this is the future of CSS for webpack](https://medium.com/webpack/the-new-css-workflow-step-1-79583bd107d7).
4953

5054
## Perks:
5155
- **HMR:** It also has first-class support for **Hot Module Replacement** across ALL those css files/chunks!!!
52-
53-
- **2 VERSIONS OF YOUR JS CHUNKS:** In addition to generating CSS chunks, this plugin in fact creates 2 javascript chunks instead of 1. It leaves untouched your typical chunk that injects styles via `style-loader` and creates another chunk named `name.no_css.js`, which has all CSS removed, thereby greatly reducing its file size. This allows for future asynchronously-loaded bundles (which will be loaded *without* a css file) to also have their corresponding styles, ***while reducing the size of your initially served javascript chunks as much as possible*** 🤓
54-
56+
- cacheable stylesheets
57+
- smallest total bytes sent compared to "render-path" css-in-js solutions that include your CSS definitions in JS
58+
- Faster than V1!
5559

5660

5761
## Installation
@@ -101,36 +105,6 @@ Keep in mind we've added sensible defaults, specifically: `[name].css` is used w
101105
The 2 exceptions are: `allChunks` will no longer do anything, and `fallback` will no longer do anything when passed to to `extract`. Basically just worry about passing your `css-loader` string and `localIdentName` 🤓
102106

103107

104-
105-
## How It Works
106-
107-
Just like your JS, it moves all the the required CSS into corresponding css chunk files. So entry chunks might be named: `main.12345.css` and dynamic split chunks would be named: `0.123456.css`, `1.123456.css`, etc. You will however now have 2 files for each javascript chunk: `0.no_css.js` and `0.js`. The former is what you should serve in the initial request (and what [webpack-flush-chunks](https://github.com/faceyspacey/webpack-flush-chunks) in conjunction with [react-universal-component](https://github.com/faceyspacey/react-universal-component) will automatically serve). The latter, *which DOES contain css injection via style-loader*, is what will asyncronously be loaded in future async requests. This solves the fact that they otherwise would be missing CSS, since the webpack async loading mechanism isn't built to serve both a JS and CSS file. In total, 3 files are created for each named entry chunk and 3 files for each dynamically split chunk, e.g:
108-
109-
**entry chunk:**
110-
- `main.js`
111-
- `main.no.js`
112-
- `main.css`
113-
114-
**dynamic chunks**:
115-
116-
*chunk 0:*
117-
- `0.js`
118-
- `0.no_css.js`
119-
- `0.css`
120-
121-
*chunk 1:*
122-
- `1.js`
123-
- `1.no_css.js`
124-
- `1.css`
125-
126-
*chunk 2:*
127-
- `2.js`
128-
- `2.no_css.js`
129-
- `2.css`
130-
131-
As part of server-side rendering, you obviously should embed within the page the js files with the `no_css.js` extension. The `.js` files will be loaded by default when Webpack asyncronously requests them, *and you won't have to worry about embedding them in any response strings.*
132-
133-
134108
## What about Aphrodite, Glamor, StyleTron, Styled-Components, Styled-Jsx, etc?
135109

136110
If you effectively use code-splitting, **Exract Css Chunks** can be a far better option than using emerging solutions like *StyleTron*, *StyledComponents*, and slightly older tools like *Aphrodite*, *Glamor*, etc. We aren't fans of either rounds of tools because of several issues, but particularly because they all have a runtime overhead. Every time your React component is rendered with those, CSS is generated and updated within the DOM. On the server, you're going to also see unnecessary cycles for flushing the CSS along the critical render path. *Next.js's* `styled-jsx`, by the way, doesn't even work on the server--*not so good when it comes to flash of unstyled content (FOUC).*
@@ -174,11 +148,6 @@ As an aside, so many apps share code between web and React Native--so the answer
174148
**Long live the dream of Code Splitting Everywhere!**
175149

176150

177-
## Notes on extract-text-webpack-plugin
178-
179-
Most the code comes from the original Extract Text Webpack Plugin--the goal is to merge this functionality back into that package at some point, though that process is not looking good. So that might be a while. Until then I'd feel totally comfortable just using this package. Though it took a while to make (and figure out how the original package worked), very little code has changed, and it won't be hard to keep in sync with upstream changes.
180-
181-
182151
## Contributing
183152
We use [commitizen](https://github.com/commitizen/cz-cli), so run `npm run cm` to make commits. A command-line form will appear, requiring you answer a few questions to automatically produce a nicely formatted commit. Releases, semantic version numbers, tags and changelogs will automatically be generated based on these commits thanks to [semantic-release](https://github.com/semantic-release/semantic-release). Be good.
184153

hotModuleReplacement.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,31 @@ module.exports = function(publicPath, outputFilename) {
1111
var newChunk = newHref.split('.')[0];
1212

1313
if (oldChunk === newChunk) {
14+
var oldSheet = styleSheets[i]
15+
var url = newHref + '?' + (+new Date)
16+
var head = document.getElementsByTagName('head')[0]
17+
var link = document.createElement('link')
18+
1419
// date insures sheets update when [contenthash] is not used in file names
15-
var url = newHref + '?' + (+new Date);
16-
styleSheets[i].href = url;
17-
console.log('[HMR]', 'Reload css: ', url);
20+
link.href = url
21+
link.charset = 'utf-8'
22+
link.type = 'text/css'
23+
link.rel = 'stylesheet'
24+
25+
head.insertBefore(link, oldSheet.nextSibling)
26+
27+
// remove the old sheet only after the old one loads so it's seamless
28+
// we gotta do it this way since link.onload basically doesn't work
29+
var img = document.createElement('img')
30+
img.onerror = function() {
31+
oldSheet.remove()
32+
console.log('[HMR]', 'Reload css: ', url);
33+
}
34+
img.src = url
1835
break;
1936
}
2037
}
2138
}
2239
}
2340
}
41+

index.js

Lines changed: 43 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -22,32 +22,6 @@ function ExtractTextPluginCompilation() {
2222
this.modulesByIdentifier = {};
2323
}
2424

25-
// ExtractTextPlugin.prototype.mergeNonInitialChunks = function(chunk, intoChunk, checkedChunks) {
26-
// if (chunk.chunks) {
27-
// // Fix error when hot module replacement used with CommonsChunkPlugin
28-
// chunk.chunks = chunk.chunks.filter(function(c) {
29-
// return typeof c !== 'undefined';
30-
// })
31-
// }
32-
33-
// if(!intoChunk) {
34-
// checkedChunks = [];
35-
// chunk.chunks.forEach(function(c) {
36-
// if(c.isInitial()) return;
37-
// this.mergeNonInitialChunks(c, chunk, checkedChunks);
38-
// }, this);
39-
// } else if(checkedChunks.indexOf(chunk) < 0) {
40-
// checkedChunks.push(chunk);
41-
// chunk.modules.slice().forEach(function(module) {
42-
// intoChunk.addModule(module);
43-
// module.addChunk(intoChunk);
44-
// });
45-
// chunk.chunks.forEach(function(c) {
46-
// if(c.isInitial()) return;
47-
// this.mergeNonInitialChunks(c, intoChunk, checkedChunks);
48-
// }, this);
49-
// }
50-
// };
5125

5226
ExtractTextPluginCompilation.prototype.addModule = function(identifier, originalModule, source, additionalInformation, sourceMap, prevModules) {
5327
var m;
@@ -328,8 +302,28 @@ ExtractTextPlugin.prototype.apply = function(compiler) {
328302
}.bind(this));
329303
}.bind(this));
330304

305+
// HMR: inject file name into corresponding javascript modules in order to trigger
306+
// appropriate hot module reloading of CSS
307+
if (DEV) {
308+
compilation.plugin("optimize-module-ids", function(modules){
309+
extractedChunks.forEach(function(extractedChunk) {
310+
extractedChunk.modules.forEach(function (module) {
311+
if(module.__fileInjected) {
312+
return;
313+
}
314+
module.__fileInjected = true;
315+
316+
extractedChunk.modules.forEach(function(module){
317+
var originalModule = module.getOriginalModule();
318+
var file = getFile(compilation, filename, module, extractedChunk);
319+
originalModule._source._value = originalModule._source._value.replace('%%extracted-file%%', file);
320+
});
321+
});
322+
});
323+
});
324+
}
331325
compilation.plugin("additional-assets", function(callback) {
332-
extractedChunks.forEach(function(extractedChunk) {
326+
extractedChunks.forEach(function(extractedChunk) {
333327
if(extractedChunk.modules.length) {
334328
extractedChunk.modules.sort(function(a, b) {
335329
if(!options.ignoreOrder && isInvalidOrder(a, b)) {
@@ -338,57 +332,35 @@ ExtractTextPlugin.prototype.apply = function(compiler) {
338332
}
339333
return getOrder(a, b);
340334
});
341-
var chunk = extractedChunk.originalChunk;
342-
var source = this.renderExtractedChunk(extractedChunk);
343-
344-
var getPath = (format) => compilation.getPath(format, {
345-
chunk: chunk
346-
}).replace(/\[(?:(\w+):)?contenthash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, function() {
347-
return loaderUtils.getHashDigest(source.source(), arguments[1], arguments[2], parseInt(arguments[3], 10));
348-
});
349335

350-
var file = (isFunction(filename)) ? filename(getPath) : getPath(filename);
336+
var chunk = extractedChunk.originalChunk;
337+
var module = this.renderExtractedChunk(extractedChunk);
338+
var file = getFile(compilation, filename, module, extractedChunk)
351339

352340
// add the css files to assets and the files array corresponding to its chunk
353-
compilation.assets[file] = source;
341+
compilation.assets[file] = module;
354342
chunk.files.push(file);
355-
356-
// HMR: inject file name into corresponding javascript modules in order to trigger
357-
// appropriate hot module reloading of CSS
358-
extractedChunk.modules.forEach(function(module){
359-
var originalModule = module.getOriginalModule();
360-
originalModule._source._value = originalModule._source._value.replace('%%extracted-file%%', file);
361-
});
362343
}
363344
}, this);
345+
callback();
346+
}.bind(this));
364347

365-
// duplicate js chunks into secondary files that don't have css injection,
366-
// giving the additional js files the extension: `.no_css.js`
367-
Object.keys(compilation.assets).forEach(function(name) {
368-
var asset = compilation.assets[name];
369-
370-
if (/\.js$/.test(name) && asset._source) {
371-
var newName = name.replace(/\.js/, '.no_css.js');
372-
var newAsset = new CachedSource(asset._source);
373-
var regex = /\/\*__START_CSS__\*\/[\s\S]*?\/\*__END_CSS__\*\//g
348+
}.bind(this));
349+
};
374350

375-
// remove js that adds css to DOM via style-loader, so that React Loadable
376-
// can serve smaller files (without css) in initial request.
377-
newAsset._cachedSource = asset.source().replace(regex, '');
378351

379-
compilation.assets[newName] = newAsset;
352+
function getFile(compilation, filename, module, chunk) {
353+
return typeof filename === 'function'
354+
? filename(getPath(compilation, module.source(), chunk))
355+
: getPath(compilation, module.source(), chunk)(filename)
356+
}
380357

381-
// add no_css file to files associated with chunk so that they are minified,
382-
// and receive source maps, and can be found by React Loadable
383-
extractedChunks.forEach(function(extractedChunk) {
384-
var chunk = extractedChunk.originalChunk;
385-
if (chunk.files.indexOf(name) > -1) {
386-
chunk.files.push(newName);
387-
}
388-
})
389-
}
390-
})
391-
callback()
392-
}.bind(this));
393-
}.bind(this));
394-
};
358+
function getPath(compilation, source, chunk) {
359+
return function(format) {
360+
return compilation.getPath(format, {
361+
chunk: chunk
362+
}).replace(/\[(?:(\w+):)?contenthash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, function() {
363+
return loaderUtils.getHashDigest(source, arguments[1], arguments[2], parseInt(arguments[3], 10));
364+
});
365+
}
366+
}

loader.js

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,10 @@ var SingleEntryPlugin = require("webpack/lib/SingleEntryPlugin");
1313
var LimitChunkCountPlugin = require("webpack/lib/optimize/LimitChunkCountPlugin");
1414

1515
var NS = fs.realpathSync(__dirname);
16+
var DEV = process.env.NODE_ENV === 'development'
1617

1718
module.exports = function(source) {
18-
if(this.cacheable) this.cacheable();
19-
// Even though this gets overwritten if extract+remove are true, without it, the runtime doesn't get added to the chunk
20-
return `require("style-loader/lib/addStyles.js");
21-
if (module.hot) { require('${require.resolve("./hotModuleReplacement.js")}'); }
22-
${source}`;
19+
return source;
2320
};
2421

2522
module.exports.pitch = function(request) {
@@ -132,22 +129,20 @@ module.exports.pitch = function(request) {
132129
resultSource += "\nmodule.exports = " + JSON.stringify(text.locals) + ";";
133130
}
134131

135-
// module.hot.data is undefined on initial load, and an object in hot updates
136-
var jsescOpts = { wrap: true, quotes: "double" };
137-
resultSource += `
138-
/*__START_CSS__*/
139-
var moduleId = ${jsesc(text[0][0], jsescOpts)};
140-
var css = ${jsesc(text[0][1], jsescOpts)};
141-
var addStyles = require("style-loader/lib/addStyles.js");
142-
addStyles([[moduleId, css]], "");
143-
/*__END_CSS__*/
144-
132+
// module.hot.data is undefined on initial load, and an object in hot updates.
133+
//
134+
// All we need is a date that changes during dev, to trigger a reload since
135+
// hashes generated based on the file contents are what trigger HMR.
136+
if (DEV) {
137+
resultSource += `
145138
if (module.hot) {
146139
module.hot.accept();
147140
if (module.hot.data) {
148-
require("${require.resolve('./hotModuleReplacement.js')}")("${publicPath}", "%%extracted-file%%");
141+
var neverUsed = ${+new Date()}
142+
require(${loaderUtils.stringifyRequest(this, path.join(__dirname, "hotModuleReplacement.js"))})("${publicPath}", "%%extracted-file%%");
149143
}
150144
}`;
145+
}
151146
}
152147
} catch(e) {
153148
return callback(e);

0 commit comments

Comments
 (0)