diff --git a/.npmignore b/.npmignore index 5143799..94133af 100644 --- a/.npmignore +++ b/.npmignore @@ -13,4 +13,5 @@ __tests__ coverage src *.png +*.jpg .idea diff --git a/README.md b/README.md index 0581783..7620a5b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# Webpack Flush Chunks [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square)](https://gitter.im/faceyspacey/Lobby) +

Version @@ -32,17 +34,17 @@

-# Webpack Flush Chunks +![webpack-flush-chunks](https://raw.githubusercontent.com/faceyspacey/redux-first-router/master/docs/poo.jpg) -> flush webpack chunks for SSR from [React Loadable](https://github.com/thejameskyle/react-loadable), [React Universal Component](https://github.com/faceyspacey/react-universal-component) or similar packages +Use this package server-side to flush webpack chunks from *[React Universal Component](https://github.com/faceyspacey/react-universal-component)* or any package that flushes an array of rendered `moduleIds` or `chunkNames`. ```js -import { flushModuleIds } from 'react-universal-component/server' +import { flushChunkNames } from 'react-universal-component/server' import flushChunks from 'webpack-flush-chunks' const app = ReactDOMServer.renderToString() -const { js, styles } = flushChunks(webpackStats, { - moduleIds: flushModuleIds() +const { js, styles, cssHash } = flushChunks(webpackStats, { + chunkNames: flushChunkNames() }) res.send(` @@ -54,6 +56,7 @@ res.send(`
${app}
${js} + ${cssHash} `) @@ -61,7 +64,7 @@ res.send(` The code has been cracked for while now for Server Side Rendering and Code-Splitting *individually*. Accomplishing both *simultaneously* has been an impossibility without jumping through major hoops or using a *framework*, specifically Next.js. -*Webpack Flush Chunks* is essentially the backend to universal rendering components like [React Loadable](https://github.com/thejameskyle/react-loadable) and [React Universal Component](https://github.com/faceyspacey/react-universal-component). It works with both or any "universal" component/module that buffers a list of `moduleIds` or `chunkNames` evaluated. +*Webpack Flush Chunks* is essentially the backend to universal rendering components like [React Universal Component](https://github.com/faceyspacey/react-universal-component). It works with any "universal" component/module that buffers a list of `moduleIds` or `chunkNames` evaluated. Via a simple API it gives you the chunks (javascript, stylesheets, etc) corresponding to the modules that were ***synchronously*** rendered on the server, which otherwise are *asynchronously* rendered on the client. In doing so, it also allows your first client-side render on page-load to render those otherwise async components ***synchronously***! @@ -80,7 +83,7 @@ https://medium.com/@faceyspacey/code-cracked-for-code-splitting-ssr-in-reactland ## Installation ``` -yarn add react-loadable webpack-flush-chunks +yarn add react-universal-component webpack-flush-chunks ``` Optionally to generate multiple CSS files for each chunk (with HMR!) install: @@ -88,28 +91,10 @@ Optionally to generate multiple CSS files for each chunk (with HMR!) install: yarn add --dev extract-css-chunks-webpack-plugin ``` -***Extract CSS Chunk*** is another companion package made to complete the CSS side of the code-splitting dream. To learn more visit: [faceyspacey/extract-css-chunks-webpack-plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin) +***Extract Css Chunks Webpack Plugin*** is another companion package made to complete the CSS side of the code-splitting dream. It uses the `cssHash` string to asynchronously request CSS assets as part of a "dual import" when calling `import()`. To learn more visit: [extract-css-chunks-webpack-plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin) and [babel-plugin-dual-import](https://github.com/faceyspacey/babel-plugin-dual-import). *...if you like to move fast, visit the [boilerplates section](#boilerplates).* - -## Motivation - -Webpack long ago introduced the capability of code-splitting. However, it's been somewhat of an enigma for many. Firstly, just grokking the original `require.ensure` API and how to create your Webpack configuration to support it wasn't a natural thing for many. More importantly, if you were just to read how developers were using it, you'd think it was a done deal. But anyone who's tried to take this feature full circle and incorporate server-side rendering were left scratching their head *(it's surprising how little-talked-about this is)*. What's the point of code-splitting when on your initial page-load--if code from additional chunks was evaluated server-side--it required an additional request to get them? Sure, as your users navigate your single-page app it came in handy, but what about SEO? What about not showing loading spinners after your initial page loaded? If you're like me, you ended up only using code-splitting for a few areas of your app where SEO wasn't important--often lesser used portions of your app. There certainly hasn't been any off-the-shelf solutions to handle this, bar Next.js. And even as far as Next.js goes, I've been surprised at how long it took for something at all to be able to tackle this feature. - -So, React can syncronously render itself in one go on the server. However, to do so on the client requires all the chunks used to perform that render, -which obviously is different for each unique URL, authenticated user, etc. While additional asynchcronous requests triggered as the user -navigates your app is what code-splitting is all about, it's sub-optimal to have to load additional chunks in the initial render. Similarly, you -don't want to just send all the chunks down to the client for that initial request, as that defeats the purpose of *code-splitting.* In additition, -if your strategy is the former, *checksums* won't match and an additional unnecessary render will happen on the client. - -As a result, the goal becomes to get to the client precisely those chunks used in the first render, no more, no less. `flushChunks` does exactly -this, providing strings containing those scripts and stylesheets you can embed in your response. `flushFiles` is a lower level API that leaves out -non-dynamic chunks, such as the chunks containing the files `main.js`, `vendor.js`, etc. - -If you can provide these chunks to the client, *React Universal Component* (or comparable) will perform the first render synchcronously just like the server. - - ## How It Works *React Universal Component*, when used on the server, skips the *loading* phase and syncronously renders your contained component, while recording the ID of @@ -157,7 +142,7 @@ and takeaway anyone who's pursued this route comes upon.* In conjunction with your Webpack configuration (which we'll specify [below](#webpack-configuration)), *Webpack Flush Chunks* solves these problems for you by consuming your Webpack compilation `stats` and generating strings and components you can embed in the final output rendered on the server. -## Usage +## Usage (if not using Webpack's "magic comments" for chunk names) Call `ReactUniversalComponent.flushModuleIds` immediately after `ReactDOMServer.renderToString`, and then pass the returned `moduleIds` plus your Webpack client bundle's compilation stats to `flushChunks`. The return object of `flushChunks` will provide several options you can embed in your response string. The easiest is the `js` and `styles` strings: @@ -169,7 +154,7 @@ import flushChunks from 'webpack-flush-chunks' const app = ReactDOMServer.renderToString() const moduleIds = flushModuleIds() -const { js, styles } = flushChunks(stats, { moduleIds }) +const { js, styles, cssHash } = flushChunks(stats, { moduleIds }) res.send(` @@ -180,30 +165,14 @@ res.send(`
${app}
${js} + ${cssHash} `) ``` -If using *React Loadable*, `moduleIds` can be one of the following: - -```js -import * ReactLodable from 'react-loadable' -const moduleIds = ReactLodable.flushServerSideRequirePaths() -``` - -*or:* - -```js -import * ReactLodable from 'react-loadable' -const moduleIds = ReactLodable.flushwebpackRequireWeakIds() -``` - -If you're using something else--which we welcome and expect to be the natural progression here--look for whatever method the given package provides that returns *module paths when using Babel on the server* **or** *Webpack module IDs when using Webpack to transpile your server*. **OR** *webpack chunk names*, which the next section describes: - - -**As of Webpack 2.4.1 (released spring 2017) you can also name chunks created by `import()`. [React Universal Component](https://github.com/faceyspacey/react-universal-component) supports this pattern as well:** +**As of Webpack 2.4.1 (released spring 2017) you can the new "magic comments" feature to name chunks created by `import()`:** *src/components/App.js:* ```js @@ -229,7 +198,7 @@ import flushChunks from 'webpack-flush-chunks' const app = ReactDOMServer.renderToString() const chunkNames = flushChunkNames() -const { js, styles } = flushChunks(stats, { chunkNames }) +const { js, styles, cssHash } = flushChunks(stats, { chunkNames }) res.send(` @@ -240,6 +209,7 @@ res.send(`
${app}
${js} + ${cssHash} `) @@ -247,11 +217,6 @@ res.send(` *et voila!* -**Summary:** By code-splitting extensively in combination with a simple server-side flushing API you can give yourself -deep control of the amount of bytes you send in your initial request while taking into account server-side rendering. -Until now, the best you could do is split your app into chunks, but then additional requests on the client were needed -to get those chunks; and then on top of that the promise of server-side rendering was lost. - > Note: if you require a less automated approach where you're given just the stylesheets and scripts corresponding to dynamic chunks (e.g. not `main.js`), see `flushFiles` in the [the low-level API section](#low-level-api-flushfiles). ## Options API: @@ -266,13 +231,13 @@ flushChunks(stats, { // optional: before: ['bootstrap', 'vendor'], // default after: ['main'], // default - rootDir: path.resolve(__dirname, '..'), // required only for Babel + rootDir: path.resolve(__dirname, '..'), // required only for a Babel-compiled server not using chunkNames outputPath: path.resolve(__dirname, '../dist'), // required only if you want to serve raw CSS }) ``` If you are rendering *both your client and server with webpack* and using the *default -names* for entry chunks, **only `moduleIds` or `chunkNames` are required**. If you're rendering the server with Babel, `rootDir` is also required. Here is a description of all possible options: +names* for entry chunks, **only `moduleIds` or `chunkNames` are required**. If you're rendering the server with Babel and not using `chunkNames`, `rootDir` is also required. Here is a description of all possible options: - **before** - ***array of named entries that come BEFORE your dynamic chunks:*** A typical pattern is to create a `vendor` chunk. A better strategy is to create a `vendor` and a `bootstrap` chunk. The "bootstrap" @@ -304,8 +269,6 @@ express/koa/hapi/etc code via Babel and then by requiring your Webpack server bu See [one of our boilerplates](#boilerplates) for an example. -> **UPDATE:** as of Webpack 2.4.1, if using Babel, `rootDir` isn't required anymore when using Webpack's *"magic comment"* feature. Simple tag your universal components with a `chunkName` and pass `chunkNames` as shown above in the [usage section above](#usage). - ## Return API: The return of `flushChunks` provides many options to render server side requests, giving you maximum flexibility: @@ -326,6 +289,11 @@ const { scripts, stylesheets, + // cssHash for use with babel-plugin-dual-import + cssHashRaw, // hash object of chunk names to css file paths + cssHash, // string: + CssHash, // react component of above + // important paths: publicPath, outputPath @@ -335,139 +303,13 @@ const { Let's take a look at some examples: -## 1) Generated \ + \ components: -```js -import React from 'react' -import ReactDOM from 'react-dom/server' -import { flushModuleIds } from 'react-universal-component/server' -import flushChunks from 'webpack-flush-chunks' -import App from '../src/components/App' - -export default function render(stats) { - return (req, res, next) => { - const app = ReactDOM.renderToString() - const moduleIds = flushModuleIds() - - const { Js, Styles } = flushChunks(stats, { - moduleIds, - before: ['bootstrap', 'vendor'], - after: ['main'], - }) - - const html = ReactDOM.renderToStaticMarkup( - - - - - -
- - - - ) - - res.send(`${html}`) - } -} -``` - -> Here the React Components `` and `` are returned from `flushChunks` for use in composing the final component tree passed to `renderToStaticMarkup`. - -This is just one option though. There are several other things **returned** from `flushChunks`, which fulfill most other common needs: - - -## 2) Strings instead of React Components: -```js -const app = ReactDOM.renderToString() -const moduleIds = flushModuleIds() -const { js, styles } = flushChunks(stats, { moduleIds }); - -res.send(` - - - - ${styles} - - -
${app}
- ${js} - - -`) -``` -> **note:** notice how no options map was passed to `flushChunks`. That's because the named entry chunks, `bootstrap`, `vendor` and `main`, are looked for by default. - - -## 3) CSS instead of Stylesheets: -```js -const app = ReactDOM.renderToString() -const moduleIds = flushModuleIds() -const { js, css } = flushChunks(stats, { - moduleIds, - outputPath: '/Users/jamesgillmore/App/dist' // required! -}); - -res.send(` - - - - ${css} - - -
${app}
- ${js} - - -`) -``` -> **note:** `` is available as well if taking the route of composing another React component tree. - - -Here the raw css will be inserted into the page, rather than links to external stylesheets. -To accomplish this, you must provide as an option the `outputPath` of your client webpack bundle. - - -Also note: during development stylesheets are still used in order to enable HMR. Build your app -with `process.env.NODE_ENV === 'production'` and you will see the raw CSS embeded in your responses. - - -## 4) Plain Array of Scripts and Stylesheets: -```js -const app = ReactDOM.renderToString() -const chunkNames = flushChunkNames() // dont forget you can do this too -const { scripts, stylesheets, publicPath } = flushChunks(stats, { chunkNames }); - -const html = ReactDOM.renderToStaticMarkup( - - - {stylesheets.map(file => ( - - ))} - - -
- - {scripts.map(file => ( - ` + `` } } @@ -127,17 +130,17 @@ export default ( /** HELPERS */ -const getJsFileRegex = (files: Array): RegExp => { +export const getJsFileRegex = (files: Array): RegExp => { const isUsingExtractCssChunk = !!files.find(file => file.includes('no_css')) return isUsingExtractCssChunk ? /\.no_css\.js$/ : /\.js$/ } -const isJs = (regex: RegExp, file: string): boolean => +export const isJs = (regex: RegExp, file: string): boolean => regex.test(file) && !/\.hot-update\.js$/.test(file) -const isCss = (file: string): boolean => /\.css$/.test(file) +export const isCss = (file: string): boolean => /\.css$/.test(file) -const stylesAsString = ( +export const stylesAsString = ( stylesheets: Array, outputPath: ?string ): string => { @@ -160,6 +163,16 @@ const stylesAsString = ( .replace(/\/\*# sourceMappingURL=.+\*\//g, '') // hide prod sourcemap err } -/** EXPORTS FOR TESTING */ - -export { getJsFileRegex, isJs, isCss, stylesAsString } +export const createCssHash = ({ + assetsByChunkName, + publicPath +}: { + assetsByChunkName: FilesMap, + publicPath: string +}): CssChunksHash => + Object.keys(assetsByChunkName).reduce((hash, name) => { + if (!assetsByChunkName[name] || !assetsByChunkName[name].find) return hash + const file = assetsByChunkName[name].find(file => file.endsWith('.css')) + if (file) hash[name] = `${publicPath}${file}` + return hash + }, {}) diff --git a/src/flushChunks.js b/src/flushChunks.js index e32576d..9c43f03 100644 --- a/src/flushChunks.js +++ b/src/flushChunks.js @@ -6,7 +6,7 @@ declare function __webpack_require__(id: string): any type Files = Array -type FilesMap = { +export type FilesMap = { [key: string]: Array } @@ -21,7 +21,7 @@ type Module = { chunks: Array } -type Stats = { +export type Stats = { assetsByChunkName: FilesMap, chunks: Array, modules: Array, @@ -77,9 +77,8 @@ const flushChunks = (stats: Stats, isWebpack: boolean, opts: Options = {}) => { ...jsAfter.reverse(), // main.css, someElseYouPutBeforeMain.css, etc ...files // correct incrementing order already ], - stats.publicPath, - opts.outputPath, - createCssHash(stats.assetsByChunkName, stats.publicPath) + stats, + opts.outputPath ) } @@ -200,17 +199,6 @@ const filesFromChunks = ( return [].concat(...chunkNames.filter(hasChunk).map(entryToFiles)) } -const createCssHash = ( - assetsByChunkName: FilesMap, - publicPath: string -): CssChunksHash => - Object.keys(assetsByChunkName).reduce((hash, name) => { - if (!assetsByChunkName[name] || !assetsByChunkName[name].find) return hash - const file = assetsByChunkName[name].find(file => file.endsWith('.css')) - if (file) hash[name] = `${publicPath}${file}` - return hash - }, {}) - /** EXPORTS FOR TESTS */ export {