diff --git a/InitialChunksPlugin.js b/InitialChunksPlugin.js new file mode 100644 index 0000000..4dd62c4 --- /dev/null +++ b/InitialChunksPlugin.js @@ -0,0 +1,125 @@ +/* eslint-disable no-var,strict,vars-on-top */ +'use strict'; +var Promise = require('bluebird'); +var fs = Promise.promisifyAll(require('fs')); +var _ = require('lodash'); +var path = require('path'); + +function InitialChunksPlugin(options) { + // Default options + this.options = _.extend({ + publicPath: './static' + }, options); +} + +InitialChunksPlugin.prototype.apply = function(compiler) { + var self = this; + + compiler.plugin('after-emit', function(compilation, callback) { + var stats = compilation.getStats().toJson(); + var entrypoints = stats.entrypoints.js.assets || []; + + var htmlAsset = stats.assets.find(function(asset) { + return asset.name.match(/[.]html?$/); + }); + + console.log('entrypoints', entrypoints); + + if (!htmlAsset) { + return callback(); + } + + var htmlFile = htmlAsset.name; + var filename = path.resolve(compilation.compiler.context, self.options.publicPath, htmlFile); + + if (self.isHotUpdateCompilation(entrypoints)) { + return callback(); + } + + var tags = self.createTags(entrypoints); + + // If the template and the assets did not change we don't have to emit the html + var assetJson = JSON.stringify(entrypoints); + if (self.options.cache && assetJson === self.assetJson) { + return callback(); + } + + self.assetJson = assetJson; + + return Promise.props({ + size: fs.statAsync(filename), + source: fs.readFileAsync(filename, 'utf-8') + }) + .catch(function() { + return Promise.reject(new Error('InitialChunksPlugin: could not load file ' + filename)); + }) + .then(function(results) { + return fs.writeFileAsync(filename, self.injectAssetsIntoHtml(results.source, tags)); + }). + finally(function() { + callback(); + }); + }); +}; + + +InitialChunksPlugin.prototype.isHotUpdateCompilation = function(assets) { + return assets.length && assets.every(function(name) { + return /\.hot-update\.js$/.test(name); + }); +}; + +/** + * Turn a tag definition into a html string + */ +InitialChunksPlugin.prototype.createTags = function(assets) { + var tags = this.generateAssetTags(assets); + return tags.map(function(tagDefinition) { + var attributes = Object.keys(tagDefinition.attributes || {}).map(function(attributeName) { + return attributeName + '="' + tagDefinition.attributes[attributeName] + '"'; + }); + return '<' + [tagDefinition.tagName].concat(attributes).join(' ') + (tagDefinition.selfClosingTag ? '/' : '') + '>' + + (tagDefinition.innerHTML || '') + + (tagDefinition.closeTag ? '' : ''); + }); +}; + +/** + * Injects the assets into the given html string + */ +InitialChunksPlugin.prototype.generateAssetTags = function(assets) { + // Turn script files into script tags + return assets.map(function(scriptPath) { + return { + tagName: 'script', + closeTag: true, + attributes: { + type: 'text/javascript', + src: scriptPath + } + }; + }); +}; + +/** + * Injects the assets into the given html string + */ +InitialChunksPlugin.prototype.injectAssetsIntoHtml = function(html, assetTags) { + var bodyRegExp = /(<\/body>)/i; + + if (assetTags.length) { + if (bodyRegExp.test(html)) { + // Append assets to body element + html = html.replace(bodyRegExp, function(match) { + return assetTags + match; + }); + } else { + // Append scripts to the end of the file if no element exists: + html += assetTags; + } + } + + return html; +}; + +module.exports = InitialChunksPlugin; diff --git a/client/index.html b/client/index.html index 13f5b8a..8ef08d7 100644 --- a/client/index.html +++ b/client/index.html @@ -4,11 +4,8 @@ React Router + Webpack 2 + Dynamic Chunk Navigation -
- - diff --git a/client/index.js b/client/index.js index 2b5b250..c28aabe 100644 --- a/client/index.js +++ b/client/index.js @@ -5,6 +5,9 @@ import rootRoute from 'pages/routes'; import 'index.html'; import 'general.scss'; +console.log('STARTING'); +window.foo = '123'; + render( , document.getElementById('root') diff --git a/package.json b/package.json index 8816d81..618a110 100644 --- a/package.json +++ b/package.json @@ -12,25 +12,25 @@ }, "license": "MIT", "devDependencies": { - "babel-core": "^6.16.0", - "babel-loader": "^6.2.7", - "babel-plugin-transform-runtime": "^6.15.0", - "babel-preset-es2015": "^6.18.0", - "babel-preset-react": "^6.15.0", + "babel-core": "6.16.0", + "babel-loader": "6.2.7", + "babel-plugin-transform-runtime": "6.15.0", + "babel-preset-es2015": "6.18.0", + "babel-preset-react": "6.22.0", "css-loader": "0.14.5", - "extract-text-webpack-plugin": "^2.0.0-beta.4", - "file-loader": "^0.8.5", - "node-sass": "^3.12.2", - "sass-loader": "^4", - "style-loader": "^0.13.1", - "webpack": "^2.1.0-beta.4", - "webpack-dev-server": "^2.1.0-beta.10" + "extract-text-webpack-plugin": "2.0.0-beta.5", + "file-loader": "0.8.5", + "html-webpack-plugin": "2.26.0", + "node-sass": "3.12.2", + "sass-loader": "3.2.3", + "style-loader": "0.13.1", + "webpack": "2.2.0", + "webpack-dev-server": "2.2.0" }, "dependencies": { - "babel-polyfill": "^6.16.0", - "babel-runtime": "^6.16.0", - "react": "^15.3.0", - "react-dom": "^15.3.0", - "react-router": "^2.8.0" + "babel-runtime": "6.22.0", + "react": "15.4.2", + "react-dom": "15.4.2", + "react-router": "2.8.0" } } diff --git a/webpack.config.js b/webpack.config.js index 2d97dc7..4119ef6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,6 @@ const webpack = require('webpack'); const path = require('path'); -const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const InitialChunksPlugin = require('./InitialChunksPlugin'); const nodeEnv = process.env.NODE_ENV || 'development'; const isProd = nodeEnv === 'production'; @@ -8,17 +8,21 @@ const isProd = nodeEnv === 'production'; const sourcePath = path.join(__dirname, './client'); const staticsPath = path.join(__dirname, './static'); -const extractCSS = new ExtractTextPlugin('style.css'); - const plugins = [ - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor', - minChunks: Infinity, - filename: 'vendor.bundle.js' - }), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify(nodeEnv) } }), + new webpack.LoaderOptionsPlugin({ + options: { + context: sourcePath, + sassLoader: { + outputStyle: 'expanded', + includePaths: [sourcePath], + }, + }, + }), + new webpack.optimize.AggressiveSplittingPlugin(), + new InitialChunksPlugin({ publicPath: staticsPath, }), ]; if (isProd) { @@ -43,46 +47,41 @@ if (isProd) { output: { comments: false }, - }), - extractCSS + }) ); } module.exports = { devtool: isProd ? 'source-map' : 'eval', context: sourcePath, + entry: { js: [ 'index', 'pages/Home' ], - vendor: [ - 'react', - 'react-dom' - ] }, + output: { path: staticsPath, - filename: 'bundle.js', + filename: '[id]-[chunkhash].js', publicPath: '/', }, + module: { rules: [ { test: /\.html$/, - use: 'file-loader', - query: { - name: '[name].[ext]' - } + use: { + loader: 'file-loader', + query: { + name: '[name].[ext]' + }, + }, }, { test: /\.scss$/, - use: isProd ? - extractCSS.extract({ - fallbackLoader: 'style-loader', - loader: ['css-loader', 'sass-loader'], - }) : - ['style-loader', 'css-loader', 'sass-loader'] + use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.(js|jsx)$/, @@ -102,18 +101,36 @@ module.exports = { } ], }, + resolve: { - extensions: ['.js', '.jsx'], + extensions: ['.js', '.jsx', '.scss'], modules: [ sourcePath, 'node_modules' ] }, + plugins: plugins, + recordsOutputPath: path.join(__dirname, 'chunk.records.json'), + + stats: { + assets: true, + children: false, + hash: false, + modules: false, + publicPath: false, + timings: true, + version: false, + warnings: true, + colors: { + yellow: '\u001b[33m', + green: '\u001b[32m' + } + }, + devServer: { contentBase: './client', historyApiFallback: true, - inject: true, port: 3000, compress: isProd, stats: { colors: true },