diff --git a/CHANGELOG.md b/CHANGELOG.md index 89cf87ddab..97e4bbae5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 9.0.0-alpha.3: September 11th, 2016 +* Fix webpack HMR ([#1713](https://github.com/roots/sage/issues/1713)) +* Remove minor edits from CHANGELOG.md ([3516629](https://github.com/roots/sage/commit/3516629)) + ### 9.0.0-alpha.2: September 4th, 2016 * Refactor build routine ([#1703](https://github.com/roots/sage/pull/1703)) * Update `_grid.scss` to use `@include make-col-ready()` mixin ([#1706](https://github.com/roots/sage/pull/1706)) @@ -16,7 +20,7 @@ * Refactor functions.php ([eae36be](https://github.com/roots/sage/commit/eae36be)) * Rework template wrapper, bring back template_part() ([#1678](https://github.com/roots/sage/pull/1678)) * Remove unused static variable in Wrapper ([9bfdd5a](https://github.com/roots/sage/commit/9bfdd5a)) -* Remove path.extname() check ([#1673](https://github.com/roots/sage/pull/1673)) +* Remove `path.extname()` check ([#1673](https://github.com/roots/sage/pull/1673)) * Updated to align with the Bootstrap 4 docs ([#1667](https://github.com/roots/sage/pull/1667)) * Add `npm prune` to Travis CI ([#1663](https://github.com/roots/sage/pull/1663)) * Bootstrap NPM ^4.0.0-alpha.2 ([#1650](https://github.com/roots/sage/pull/1650)) diff --git a/README.md b/README.md index c90660552f..2a402648ba 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ You now have all the necessary dependencies to run the build process. ### Build commands +* `npm start` — Compile assets when file changes are made, start BrowserSync session * `npm run build` — Compile and optimize the files in your assets directory -* `npm run start` — Compile assets when file changes are made, start BrowserSync session * `npm run build:production` — Compile assets for production #### Additional commands @@ -98,7 +98,7 @@ You now have all the necessary dependencies to run the build process. ### Using BrowserSync -To use BrowserSync during `npm run start` you need to update `devUrl` at the bottom of `assets/config.json` to reflect your local development hostname. +To use BrowserSync during `npm start` you need to update `devUrl` at the bottom of `assets/config.json` to reflect your local development hostname. If your local development URL is `https://project-name.dev`, update the file to read: ```json @@ -115,6 +115,20 @@ If you are not using [Bedrock](https://roots.io/bedrock/), update `publicPath` t ... ``` +By default, BrowserSync will use webpack's [HMR](https://webpack.github.io/docs/hot-module-replacement.html), which won't trigger a page reload in your browser. + +If you would like to force BrowserSync to reload the page whenever certain file types are edited, then add them to `watch` in `assets/config.json`. + +```json +... + "watch": [ + "assets/scripts/**/*.js", + "templates/**/*.php", + "src/**/*.php" + ], +... +``` + ## Documentation Sage 8 documentation is available at [https://roots.io/sage/docs/](https://roots.io/sage/docs/). diff --git a/assets/build/config.js b/assets/build/config.js index 83da856545..f930643d7f 100644 --- a/assets/build/config.js +++ b/assets/build/config.js @@ -47,4 +47,5 @@ module.exports = mergeWithConcat(config, { .map(file => path.join(config.paths.assets, file)); }, }, + publicPath: `${config.publicPath}/${path.basename(config.paths.dist)}/`, }); diff --git a/assets/build/util/addHotMiddleware.js b/assets/build/util/addHotMiddleware.js new file mode 100644 index 0000000000..8a0d108631 --- /dev/null +++ b/assets/build/util/addHotMiddleware.js @@ -0,0 +1,21 @@ +const qs = require('qs'); + +/** + * Loop through webpack entry + * and add the hot middleware + * @param {Object} entry webpack entry + * @return {Object} entry with hot middleware + */ +module.exports = (entry) => { + const results = {}; + const hotMiddlewareScript = `webpack-hot-middleware/client?${qs.stringify({ + timeout: 20000, + reload: false, + })}`; + + Object.keys(entry).forEach(name => { + results[name] = Array.isArray(entry[name]) ? entry[name].slice(0) : [entry[name]]; + results[name].push(hotMiddlewareScript); + }); + return results; +}; diff --git a/assets/build/util/assetsPluginProcessOutput.js b/assets/build/util/assetsPluginProcessOutput.js new file mode 100644 index 0000000000..352da3baff --- /dev/null +++ b/assets/build/util/assetsPluginProcessOutput.js @@ -0,0 +1,18 @@ +const path = require('path'); + +/** + * Process AssetsPlugin output and format it + * for Sage: {"[name].[ext]":"[name]_[hash].[ext]"} + * @param {Object} assets passed by processOutput + * @return {String} JSON + */ +module.exports = (assets) => { + const results = {}; + Object.keys(assets).forEach(name => { + Object.keys(assets[name]).forEach(ext => { + const filename = `${path.dirname(assets[name][ext])}/${path.basename(`${name}.${ext}`)}`; + results[filename] = assets[name][ext]; + }); + }); + return JSON.stringify(results); +}; diff --git a/assets/build/webpack.config.js b/assets/build/webpack.config.js index e34d5724e0..7073812759 100644 --- a/assets/build/webpack.config.js +++ b/assets/build/webpack.config.js @@ -8,22 +8,28 @@ const ImageminPlugin = require('imagemin-webpack-plugin').default; const imageminMozjpeg = require('imagemin-mozjpeg'); const mergeWithConcat = require('./util/mergeWithConcat'); +const addHotMiddleware = require('./util/addHotMiddleware'); const webpackConfigProduction = require('./webpack.config.production'); const webpackConfigWatch = require('./webpack.config.watch'); const config = require('./config'); -const publicPath = `${config.publicPath}/${path.basename(config.paths.dist)}/`; const assetsFilenames = (config.enabled.cacheBusting) ? config.cacheBusting : '[name]'; const sourceMapQueryStr = (config.enabled.sourceMaps) ? '+sourceMap' : '-sourceMap'; const jsLoader = { test: /\.js$/, exclude: [/(node_modules|bower_components)(?![/|\\](bootstrap|foundation-sites))/], - loaders: [`babel?presets[]=${path.resolve('./node_modules/babel-preset-es2015')}&cacheDirectory`], + loaders: [{ + loader: 'babel', + query: { + presets: [[path.resolve('./node_modules/babel-preset-es2015'), { modules: false }]], + cacheDirectory: true, + }, + }], }; if (config.enabled.watcher) { - jsLoader.loaders.unshift('monkey-hot'); + jsLoader.loaders.unshift('monkey-hot?sourceType=module'); } const webpackConfig = { @@ -32,7 +38,7 @@ const webpackConfig = { devtool: (config.enabled.sourceMaps ? '#source-map' : undefined), output: { path: config.paths.dist, - publicPath, + publicPath: config.publicPath, filename: `scripts/${assetsFilenames}.js`, }, module: { @@ -148,11 +154,6 @@ const webpackConfig = { Tether: 'tether', 'window.Tether': 'tether', }), - new webpack.DefinePlugin({ - WEBPACK_PUBLIC_PATH: (config.enabled.watcher) - ? JSON.stringify(publicPath) - : false, - }), new webpack.LoaderOptionsPlugin({ minimize: config.enabled.minify, debug: config.enabled.watcher, @@ -181,19 +182,15 @@ if (config.env.production) { } if (config.enabled.watcher) { - module.exports = mergeWithConcat(webpackConfig, webpackConfigWatch); + module.exports = mergeWithConcat(webpackConfig, webpackConfigWatch, { + entry: addHotMiddleware(webpackConfig.entry), + }); } if (config.enabled.uglifyJs) { module.exports.plugins.push( new webpack.optimize.UglifyJsPlugin({ - compress: config.enabled.minify ? { - drop_debugger: true, - dead_code: true, - warnings: false, - } : false, sourceMap: config.enabled.sourceMaps, - output: { comments: false }, }) ); } diff --git a/assets/build/webpack.config.production.js b/assets/build/webpack.config.production.js index 4a320d7b5d..ab8de65d13 100644 --- a/assets/build/webpack.config.production.js +++ b/assets/build/webpack.config.production.js @@ -1,34 +1,17 @@ const AssetsPlugin = require('assets-webpack-plugin'); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const cssnano = require('cssnano'); -const path = require('path'); +const processOutput = require('./util/assetsPluginProcessOutput'); const config = require('./config'); -/** - * Process AssetsPlugin output and format it - * for Sage: {"[name].[ext]":"[name]_[hash].[ext]"} - * @param {Object} assets passed by processOutput - * @return {String} JSON - */ -const assetsPluginProcessOutput = (assets) => { - const results = {}; - Object.keys(assets).forEach(name => { - Object.keys(assets[name]).forEach(ext => { - const filename = `${path.dirname(assets[name][ext])}/${path.basename(`${name}.${ext}`)}`; - results[filename] = assets[name][ext]; - }); - }); - return JSON.stringify(results); -}; - module.exports = { plugins: [ new AssetsPlugin({ path: config.paths.dist, filename: 'assets.json', fullPath: false, - processOutput: assetsPluginProcessOutput, + processOutput, }), new OptimizeCssAssetsPlugin({ cssProcessor: cssnano, diff --git a/assets/build/webpack.config.watch.js b/assets/build/webpack.config.watch.js index aa84a9c180..6f68603a90 100644 --- a/assets/build/webpack.config.watch.js +++ b/assets/build/webpack.config.watch.js @@ -1,21 +1,24 @@ -const BrowserSyncPlugin = require('browser-sync-webpack-plugin'); -const url = require('url'); +const webpack = require('webpack'); +const BrowserSyncPlugin = require('./webpack.plugin.browsersync'); const config = require('./config'); module.exports = { output: { pathinfo: true }, debug: true, - devTool: 'cheap-module-source-map', + devtool: '#cheap-module-source-map', plugins: [ + new webpack.optimize.OccurrenceOrderPlugin(), + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin(), + new webpack.DefinePlugin({ + WEBPACK_PUBLIC_PATH: JSON.stringify(config.publicPath), + }), new BrowserSyncPlugin({ - host: url.parse(config.proxyUrl).hostname, - port: url.parse(config.proxyUrl).port, - proxy: config.devUrl, - files: [ - 'templates/**/*.php', - 'src/**/*.php', - ], + target: config.devUrl, + publicPath: config.publicPath, + proxyUrl: config.proxyUrl, + browserSyncOptions: { files: config.watch }, }), ], }; diff --git a/assets/build/webpack.plugin.browsersync.js b/assets/build/webpack.plugin.browsersync.js new file mode 100644 index 0000000000..a55ae7bc5f --- /dev/null +++ b/assets/build/webpack.plugin.browsersync.js @@ -0,0 +1,55 @@ +'use strict'; // eslint-disable-line strict + +const webpackDevMiddleware = require('webpack-dev-middleware'); +const webpackHotMiddleware = require('webpack-hot-middleware'); +const browserSync = require('browser-sync'); +const url = require('url'); + +const mergeWithConcat = require('./util/mergeWithConcat'); + +module.exports = class { + constructor(options) { + this.watcher = null; + this.compiler = null; + this.options = mergeWithConcat({ + proxyUrl: 'https://localhost:3000', + callback() {}, + }, options); + } + apply(compiler) { + if (this.options.disable) { + return; + } + this.compiler = compiler; + compiler.plugin('done', this.doneCompiling); + } + doneCompiling() { + if (!this.watcher) { + this.watcher = browserSync.create(); + this.compiler.plugin('compilation', () => this.watcher.notify('Rebuilding...')); + this.start(); + } + // Optionally add logic for this.watcher.reload() + } + start() { + const watcherConfig = mergeWithConcat({ + host: url.parse(this.options.proxyUrl).hostname, + port: url.parse(this.options.proxyUrl).port, + proxy: { + target: this.options.target, + middleware: this.middleware(), + }, + }, this.options.browserSyncOptions); + this.watcher.init(watcherConfig, this.options.callback.bind(this)); + } + middleware() { + this.webpackDevMiddleware = webpackDevMiddleware(this.compiler, { + publicPath: this.options.publicPath, + stats: { colors: true }, + }); + this.webpackHotMiddleware = webpackHotMiddleware(this.compiler, { + log: this.watcher.notify.bind(this.watcher), + }); + return [this.webpackDevMiddleware, this.webpackHotMiddleware]; + } +}; diff --git a/assets/config.json b/assets/config.json index 2cbec6164d..3405de1e1f 100644 --- a/assets/config.json +++ b/assets/config.json @@ -8,6 +8,10 @@ "./scripts/customizer.js" ] }, + "watch": [ + "templates/**/*.php", + "src/**/*.php" + ], "publicPath": "/app/themes/sage", "devUrl": "https://example.dev", "proxyUrl": "https://localhost:3000", diff --git a/package.json b/package.json index b20d56064c..5166b671d1 100644 --- a/package.json +++ b/package.json @@ -35,16 +35,15 @@ "babel-core": "^6.14.0", "babel-eslint": "^6.1.2", "babel-loader": "^6.2.5", - "babel-preset-es2015-webpack": "^6.4.3", + "babel-preset-es2015": "^6.14.0", "babel-register": "^6.14.0", "babel-runtime": "^6.11.6", "body-parser": "^1.15.2", "browser-sync": "^2.15.0", - "browser-sync-webpack-plugin": "^1.1.2", "clean-webpack-plugin": "^0.1.10", "css-loader": "^0.25.0", "cssnano": "^3.7.4", - "eslint": "^3.4.0", + "eslint": "^3.5.0", "eslint-config-airbnb": "^11.0.0", "eslint-config-airbnb-es5": "^1.0.9", "eslint-loader": "^1.5.0", @@ -59,7 +58,7 @@ "imports-loader": "^0.6.5", "lodash": "^4.15.0", "minimist": "^1.2.0", - "monkey-hot-loader": "0.0.3", + "monkey-hot-loader": "github:rmarscher/monkey-hot-loader#webpack2-import", "node-sass": "^3.9.3", "optimize-css-assets-webpack-plugin": "^1.3.0", "postcss": "^5.2.0", @@ -70,7 +69,9 @@ "sass-loader": "^4.0.2", "style-loader": "^0.13.1", "url-loader": "^0.5.7", - "webpack": "^2.1.0-beta.22" + "webpack": "^2.1.0-beta.22", + "webpack-dev-middleware": "^1.7.0", + "webpack-hot-middleware": "^2.12.2" }, "dependencies": { "bootstrap": "github:twbs/bootstrap#v4-dev",