From 5c2efa93a2e9d0cabbbd1b8d0d5aa77a635f2c7d Mon Sep 17 00:00:00 2001 From: Tim Branyen Date: Fri, 2 Jun 2017 16:20:27 -0700 Subject: [PATCH] LiveReload always sends all assets A problem with this plugin is that regardless of what has changed, the same output assets are always emitted. I've taken a stable at filtering down, but to be honest, I made it a total hack job. For some reason the webpack plugin API doesn't not make it easy (even after reading docs) to figure out what files changed and what assets they belong to. That aside, the goal of this PR is to allow CSS to LiveReload without doing a full page refresh. It does work. However, it relies on adjusting the `startTime` (https://github.com/webpack/docs/wiki/how-to-write-a-plugin#monitoring-the-watch-graph) by offseting `process.uptime()` at least for the first call, otherwise the modified timestamps aren't accurate. It also relies on enabling sourceMaps for css-loader to get access to the css asset children. I'd love a review and tips on how to improve this, but for now this should greatly assist with reloading ExtractTextPlugin-based webpack configs, without requiring the webpack-dev-server. --- index.js | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 78b92c0..8ac41c3 100755 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ /* jshint node:true */ +var statSync = require('fs').statSync; var lr = require('tiny-lr'); var servers = {}; @@ -17,6 +18,10 @@ function LiveReloadPlugin(options) { this.protocol = this.options.protocol ? this.options.protocol + ':' : ''; this.hostname = this.options.hostname || '" + location.hostname + "'; this.server = null; + + this.startTime = new Date(); + this.startTime.setSeconds(-process.uptime()); + this.prevTimestamps = {}; } function arraysEqual(a1, a2){ @@ -53,18 +58,52 @@ LiveReloadPlugin.prototype.start = function start(watching, cb) { }; LiveReloadPlugin.prototype.done = function done(stats) { + var timestamps = stats.compilation ? stats.compilation.fileTimestamps : {}; + var assets = stats.compilation.assets; + var include = []; + + // If no timestamp information is available, use all assets. + if (!timestamps) { + include.push.apply(include, Object.keys(assets)); + } + else { + this.changedFiles = Object.keys(timestamps).filter(function(watchfile) { + return this.startTime < Math.ceil(statSync(watchfile).mtime); + }.bind(this)).forEach(function(changedFile) { + Object.keys(assets).forEach(function(assetName) { + const asset = Object.assign({}, assets[assetName]); + const sources = []; + + if (asset.emitted && asset.existsAt.split('.').slice(-1)[0] !== 'css') { + include.push(assetName); + } + + (asset.children || []).forEach(function(child) { + if (child && child._sourceMap && child._sourceMap.sources) { + sources.push.apply(sources, child._sourceMap.sources); + } + }); + + if (sources.includes(changedFile)) { + include.push(assetName); + } + }, this); + }, this); + } + + this.startTime = Date.now(); + var hash = stats.compilation.hash; var childHashes = (stats.compilation.children || []).map(child => child.hash); - var files = Object.keys(stats.compilation.assets); - var include = files.filter(function(file) { + var updated = include.filter(function(file) { return !file.match(this.ignore); }, this); - if (this.isRunning && (hash !== this.lastHash || !arraysEqual(childHashes, this.lastChildHashes)) && include.length > 0) { + if (this.isRunning && (hash !== this.lastHash || !arraysEqual(childHashes, this.lastChildHashes)) && updated.length > 0) { this.lastHash = hash; this.lastChildHashes = childHashes; setTimeout(function onTimeout() { - this.server.notifyClients(include); + this.server.notifyClients(updated); }.bind(this), this.delay); } };