browserify-hmr
Version:
Hot Module Replacement plugin for Browserify
663 lines (625 loc) • 23 kB
JavaScript
;
var has = require('../lib/has');
var StrSet = require('../lib/str-set');
var forEach = require('lodash/forEach');
var some = require('lodash/some');
var map = require('lodash/map');
var filter = require('lodash/filter');
var zipObject = require('lodash/zipObject');
var forOwn = require('lodash/forOwn');
var mapValues = require('lodash/mapValues');
var assign = require('lodash/assign');
function emitError(err) {
setTimeout(function() {
throw err;
}, 0);
}
function makeModuleIndexesToNames(moduleMeta) {
var moduleIndexesToNames = {};
forOwn(moduleMeta, function(value, name) {
moduleIndexesToNames[value.index] = name;
});
return moduleIndexesToNames;
}
var console = global.console ? global.console : {
error: function(){}, log: function() {}
};
function main(
moduleDefs, cachedModules, moduleMeta, updateUrl,
updateMode, supportModes, ignoreUnaccepted, updateCacheBust, bundleKey,
socketio,
bundle__filename, bundle__dirname
) {
var moduleIndexesToNames = makeModuleIndexesToNames(moduleMeta);
var socket;
var name, i, len;
if (!global._hmr[bundleKey].setStatus) {
var runtimeModuleInfo = {};
var createInfoEntry = function(name) {
runtimeModuleInfo[name] = {
index: moduleMeta[name].index,
hash: moduleMeta[name].hash,
parents: new StrSet(moduleMeta[name].parents),
module: null,
disposeData: null,
accepters: new StrSet(),
accepting: new StrSet(),
decliners: new StrSet(),
declining: new StrSet(),
selfAcceptCbs: [], // may contain null. nonzero length means module is self-accepting
disposeHandlers: []
};
};
for (name in moduleMeta) {
if (has(moduleMeta, name)) {
createInfoEntry(name);
}
}
// loaders take a callback(err, data). They may give null for data if they
// know there hasn't been an update.
var fileReloaders = {
fs: function(cb) {
var fs;
try {
fs = require('f'+'s');
} catch(e) {
cb(e);
return;
}
fs.readFile(localHmr.updateUrl || bundle__filename, 'utf8', cb);
},
ajax: function(cb) {
var xhr;
try {
xhr = new XMLHttpRequest();
} catch(e) {
cb(e);
return;
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
cb(null, xhr.responseText);
} else {
cb(new Error("Request had response "+xhr.status));
}
}
};
var url = localHmr.updateUrl + (updateCacheBust?'?_v='+(+new Date()):'');
xhr.open('GET', url, true);
xhr.send();
}
};
var lastScriptData = null;
// cb(err, expectUpdate)
var reloadAndRunScript = function(cb) {
if (!has(fileReloaders, localHmr.updateMode)) {
cb(new Error("updateMode "+localHmr.updateMode+" not implemented"));
return;
}
var reloader = fileReloaders[localHmr.updateMode];
reloader(function(err, data) {
if (err || !data || lastScriptData === data) {
cb(err, false);
return;
}
lastScriptData = data;
localHmr.newLoad = null;
try {
if (bundle__filename || bundle__dirname) {
new Function('require', '__filename', '__dirname', data)(require, bundle__filename, bundle__dirname);
} else {
new Function('require', data)(require);
}
// running the file sets _hmr.newLoad
} catch (err2) {
localHmr.newLoad = null;
cb(err2);
return;
}
if (!localHmr.newLoad) {
cb(new Error("Reloaded script did not set hot module reload data"));
return;
}
cb(null, true);
});
};
var getOutdatedModules = function() {
var outdated = [];
var name;
// add changed and deleted modules
for (name in runtimeModuleInfo) {
if (has(runtimeModuleInfo, name)) {
if (
!has(localHmr.newLoad.moduleMeta, name) ||
runtimeModuleInfo[name].hash !== localHmr.newLoad.moduleMeta[name].hash
) {
outdated.push(name);
}
}
}
// add brand new modules
for (name in localHmr.newLoad.moduleMeta) {
if (has(localHmr.newLoad.moduleMeta, name)) {
if (!has(runtimeModuleInfo, name)) {
outdated.push(name);
}
}
}
// add modules that are non-accepting/declining parents of outdated modules.
// important: if outdated has new elements added during the loop,
// then we iterate over them too.
for (var i=0; i<outdated.length; i++) {
name = outdated[i];
if (has(runtimeModuleInfo, name)) {
runtimeModuleInfo[name].parents.forEach(function(parentName) {
if (
runtimeModuleInfo[name].selfAcceptCbs.length === 0 &&
!runtimeModuleInfo[name].accepters.has(parentName) &&
!runtimeModuleInfo[name].decliners.has(parentName) &&
outdated.indexOf(parentName) === -1
) {
outdated.push(parentName);
}
});
}
}
return outdated;
};
var moduleHotCheck = function(autoApply, cb) {
if (typeof autoApply === 'function') {
cb = autoApply;
autoApply = false;
}
if (!cb) {
throw new Error("module.hot.check callback parameter required");
}
if (localHmr.status !== 'idle') {
cb(new Error("module.hot.check can only be called while status is idle"));
return;
}
if (updateMode === 'websocket') {
cb(new Error("module.hot.check can't be used when update mode is websocket"));
return;
}
localHmr.setStatus('check');
reloadAndRunScript(function(err, expectUpdate) {
if (err || !expectUpdate) {
localHmr.setStatus('idle');
cb(err, null);
return;
}
var outdatedModules = getOutdatedModules();
if (outdatedModules.length === 0) {
localHmr.setStatus('idle');
cb(null, null);
} else {
localHmr.setStatus('ready');
if (autoApply) {
moduleHotApply(autoApply, cb);
} else {
cb(null, outdatedModules);
}
}
});
};
var moduleHotApply = function(options, cb) {
if (typeof options === 'function') {
cb = options;
options = null;
}
if (!cb) {
throw new Error("module.hot.apply callback parameter required");
}
var ignoreUnaccepted = !!(options && options.ignoreUnaccepted);
if (localHmr.status !== 'ready') {
cb(new Error("module.hot.apply can only be called while status is ready"));
return;
}
var outdatedModules = getOutdatedModules();
var isValueNotInOutdatedModules = function(value) {
return outdatedModules.indexOf(value) === -1;
};
var i, len;
var acceptedUpdates = filter(outdatedModules, function(name) {
if (has(runtimeModuleInfo, name)) {
if (
runtimeModuleInfo[name].decliners.some(isValueNotInOutdatedModules) ||
(
runtimeModuleInfo[name].accepters.size() === 0 &&
runtimeModuleInfo[name].selfAcceptCbs.length === 0 &&
runtimeModuleInfo[name].parents.some(isValueNotInOutdatedModules)
)
) {
return false;
}
}
return true;
});
if (!ignoreUnaccepted && outdatedModules.length !== acceptedUpdates.length) {
localHmr.setStatus('idle');
cb(new Error("Some updates were declined"));
return;
}
var an;
for (i=0, len=acceptedUpdates.length; i<len; i++) {
an = acceptedUpdates[i];
if (has(runtimeModuleInfo, an)) {
runtimeModuleInfo[an].disposeData = {};
for (var j=0; j<runtimeModuleInfo[an].disposeHandlers.length; j++) {
try {
runtimeModuleInfo[an].disposeHandlers[j].call(null, runtimeModuleInfo[an].disposeData);
} catch(e) {
localHmr.setStatus('idle');
cb(e || new Error("Unknown dispose callback error"));
return;
}
}
}
}
var selfAccepters = [];
for (i=0, len=acceptedUpdates.length; i<len; i++) {
an = acceptedUpdates[i];
if (!has(runtimeModuleInfo, an)) {
// new modules
runtimeModuleInfo[an] = {
index: an,
hash: localHmr.newLoad.moduleMeta[an].hash,
parents: new StrSet(localHmr.newLoad.moduleMeta[an].parents),
module: null,
disposeData: null,
accepters: new StrSet(),
accepting: new StrSet(),
decliners: new StrSet(),
declining: new StrSet(),
selfAcceptCbs: [],
disposeHandlers: []
};
} else if (!has(localHmr.newLoad.moduleMeta, an)) {
// removed modules
delete cachedModules[runtimeModuleInfo[an].index];
delete runtimeModuleInfo[an];
continue;
} else {
// updated modules
runtimeModuleInfo[an].hash = localHmr.newLoad.moduleMeta[an].hash;
runtimeModuleInfo[an].parents = new StrSet(localHmr.newLoad.moduleMeta[an].parents);
runtimeModuleInfo[an].module = null;
runtimeModuleInfo[an].accepting.forEach(function(accepted) {
runtimeModuleInfo[accepted].accepters.del(an);
});
runtimeModuleInfo[an].accepting = new StrSet();
runtimeModuleInfo[an].declining.forEach(function(accepted) {
runtimeModuleInfo[accepted].decliners.del(an);
});
runtimeModuleInfo[an].declining = new StrSet();
forEach(runtimeModuleInfo[an].selfAcceptCbs, function(cb) {
selfAccepters.push({name: an, cb: cb});
});
runtimeModuleInfo[an].selfAcceptCbs = [];
runtimeModuleInfo[an].disposeHandlers = [];
}
moduleDefs[runtimeModuleInfo[an].index] = [
// module function
localHmr.newLoad.moduleDefs[localHmr.newLoad.moduleMeta[an].index][0],
// module deps
mapValues(localHmr.newLoad.moduleDefs[localHmr.newLoad.moduleMeta[an].index][1], function(depIndex, depRef) {
var depName = localHmr.newLoad.moduleIndexesToNames[depIndex];
if (has(localHmr.runtimeModuleInfo, depName)) {
return localHmr.runtimeModuleInfo[depName].index;
} else {
return depName;
}
})
];
cachedModules[runtimeModuleInfo[an].index] = null;
}
// Update the accept handlers list and call the right ones
var errCanWait = null;
var updatedNames = new StrSet(acceptedUpdates);
var oldUpdateHandlers = localHmr.updateHandlers;
var relevantUpdateHandlers = [];
var newUpdateHandlers = [];
for (i=0, len=oldUpdateHandlers.length; i<len; i++) {
if (!updatedNames.has(oldUpdateHandlers[i].accepter)) {
newUpdateHandlers.push(oldUpdateHandlers[i]);
}
if (updatedNames.hasIntersection(oldUpdateHandlers[i].deps)) {
relevantUpdateHandlers.push(oldUpdateHandlers[i]);
}
}
localHmr.updateHandlers = newUpdateHandlers;
for (i=0, len=relevantUpdateHandlers.length; i<len; i++) {
try {
relevantUpdateHandlers[i].cb.call(null, acceptedUpdates);
} catch(e) {
if (errCanWait) emitError(errCanWait);
errCanWait = e;
}
}
// Call the self-accepting modules
forEach(selfAccepters, function(obj) {
try {
require(runtimeModuleInfo[obj.name].index);
} catch(e) {
if (obj.cb) {
obj.cb.call(null, e);
} else {
if (errCanWait) emitError(errCanWait);
errCanWait = e;
}
}
});
localHmr.setStatus('apply');
localHmr.setStatus('idle');
cb(errCanWait, acceptedUpdates);
};
var moduleHotSetUpdateMode = function(mode, options) {
options = options || {};
if (supportModes.indexOf(mode) === -1) {
throw new Error("Mode "+mode+" not in supportModes. Please check the Browserify-HMR plugin options.");
}
if (mode === 'ajax' && !options.url) {
throw new Error("url required for ajax update mode");
}
if (localHmr.status !== 'idle') {
throw new Error("module.hot.setUpdateMode can only be called while status is idle");
}
localHmr.newLoad = null;
localHmr.updateMode = updateMode = mode;
localHmr.updateUrl = updateUrl = options.url;
updateCacheBust = options.cacheBust;
ignoreUnaccepted = has(options, 'ignoreUnaccepted') ? options.ignoreUnaccepted : true;
if (socket) {
socket.disconnect();
socket = null;
}
if (mode === 'websocket') {
socket = setupSocket();
}
};
var setupSocket = function() {
var url = updateUrl || 'http://localhost:3123';
var socket = socketio(url, {'force new connection': true});
console.log('[HMR] Attempting websocket connection to', url);
var isAcceptingMessages = false;
socket.on('connect', function() {
isAcceptingMessages = false;
var syncMsg = mapValues(runtimeModuleInfo, function(value, name) {
return {
hash: value.hash
};
});
socket.emit('sync', syncMsg);
});
var isUpdating = false;
var queuedUpdateMessages = [];
socket.on('sync confirm', function() {
console.log('[HMR] Websocket connection successful.');
isAcceptingMessages = true;
queuedUpdateMessages = [];
});
socket.on('error', function(err) {
console.error('[HMR] Websocket error: ' + err);
});
socket.on('disconnect', function() {
console.log('[HMR] Websocket connection lost.');
});
var acceptNewModules = function(msg) {
// Make sure we don't accept new modules before we've synced ourselves.
if (!isAcceptingMessages) return;
if (isUpdating) {
queuedUpdateMessages.push(msg);
return;
}
// Take the message and create a localHmr.newLoad value as if the
// bundle had been re-executed, then call moduleHotApply.
isUpdating = true;
// random id so we can make the normally unnamed args have random names
var rid = String(Math.random()).replace(/[^0-9]/g, '');
var newModuleDefs = localHmr.newLoad ? localHmr.newLoad.moduleDefs : assign({}, moduleDefs);
var newModuleMeta = localHmr.newLoad ?
localHmr.newLoad.moduleMeta : mapValues(runtimeModuleInfo, function(value, key) {
return {
index: value.index,
hash: value.hash,
parents: value.parents.toArray()
};
});
forOwn(msg.newModuleData, function(value, key) {
newModuleMeta[key] = {
index: value.index,
hash: value.hash,
parents: value.parents
};
});
forEach(msg.removedModules, function(removedName) {
delete newModuleDefs[runtimeModuleInfo[removedName].index];
delete newModuleMeta[removedName];
});
var newModuleIndexesToNames = makeModuleIndexesToNames(newModuleMeta);
forOwn(msg.newModuleData, function(value, key) {
// this part needs to run after newModuleMeta and
// newModuleIndexesToNames are populated.
var newModuleFunction = (function() {
var fn;
if (bundle__filename || bundle__dirname) {
fn = new Function('require', 'module', 'exports', '_u1'+rid, '_u2'+rid, '__u3'+rid, '__u4'+rid, '__filename', '__dirname', value.source);
return function(require, module, exports, _u1, _u2, _u3, _u4) {
global._hmr[bundleKey].initModule(key, module);
fn.call(this, require, module, exports, _u1, _u2, _u3, _u4, bundle__filename, bundle__dirname);
};
} else {
fn = new Function('require', 'module', 'exports', '_u1'+rid, '_u2'+rid, '__u3'+rid, '__u4'+rid, value.source);
return function(require, module, exports, _u1, _u2, _u3, _u4) {
global._hmr[bundleKey].initModule(key, module);
fn.call(this, require, module, exports, _u1, _u2, _u3, _u4);
};
}
})();
newModuleDefs[newModuleMeta[key].index] = [
// module function
newModuleFunction,
// module deps
mapValues(value.deps, function(depIndex, depRef) {
var depName = newModuleIndexesToNames[depIndex];
if (has(newModuleMeta, depName)) {
return newModuleMeta[depName].index;
} else {
return depName;
}
})
];
});
localHmr.newLoad = {
moduleDefs: newModuleDefs,
moduleMeta: newModuleMeta,
moduleIndexesToNames: newModuleIndexesToNames
};
localHmr.setStatus('ready');
var outdatedModules = getOutdatedModules();
moduleHotApply({ignoreUnaccepted: ignoreUnaccepted}, function(err, updatedNames) {
if (err) {
console.error('[HMR] Error applying update', err);
}
if (updatedNames) {
console.log('[HMR] Updated modules', updatedNames);
if (outdatedModules.length !== updatedNames.length) {
var notUpdatedNames = filter(outdatedModules, function(name) {
return updatedNames.indexOf(name) === -1;
});
console.log('[HMR] Some modules were not updated', notUpdatedNames);
}
}
isUpdating = false;
var queuedMsg;
while ((queuedMsg = queuedUpdateMessages.shift())) {
acceptNewModules(queuedMsg);
}
});
};
socket.on('new modules', acceptNewModules);
return socket;
};
var localHmr = {
updateUrl: updateUrl,
updateMode: updateMode,
runtimeModuleInfo: runtimeModuleInfo,
status: "idle",
setStatus: function(status) {
this.status = status;
var statusHandlers = this.statusHandlers.slice();
for (var i=0, len=statusHandlers.length; i<len; i++) {
statusHandlers[i].call(null, status);
}
},
statusHandlers: [],
updateHandlers: [],
// during a reload this is set to an object with moduleDefs,
// moduleMeta, and moduleIndexesToNames properties
newLoad: null,
initModule: function(name, module) {
// Set module.id for react-hot-loader support.
module.id = moduleMeta[name] ? moduleMeta[name].index : name;
runtimeModuleInfo[name].module = module;
module.hot = {
accept: function(deps, cb) {
if (!cb && (!deps || typeof deps === 'function')) { // self
cb = deps;
deps = null;
runtimeModuleInfo[name].selfAcceptCbs.push(cb);
} else {
if (typeof deps === 'string') {
deps = [deps];
}
var depNames = new StrSet();
for (var i=0, depsLen=deps.length; i<depsLen; i++) {
var depIndex = moduleDefs[runtimeModuleInfo[name].index][1][deps[i]];
if (depIndex === undefined || !has(moduleIndexesToNames, depIndex)) {
throw new Error("File does not use dependency: "+deps[i]);
}
depNames.add(moduleIndexesToNames[depIndex]);
}
deps = null;
depNames.forEach(function(depName) {
runtimeModuleInfo[depName].accepters.add(name);
runtimeModuleInfo[name].accepting.add(depName);
});
if (cb) {
localHmr.updateHandlers.push({
accepter: name,
deps: depNames,
cb: cb
});
}
}
},
decline: function(deps) {
if (!deps) { // self
runtimeModuleInfo[name].decliners.add(name);
runtimeModuleInfo[name].declining.add(name);
} else {
if (typeof deps === 'string') {
deps = [deps];
}
for (var i=0, depsLen=deps.length; i<depsLen; i++) {
var depIndex = moduleDefs[runtimeModuleInfo[name].index][1][deps[i]];
if (depIndex === undefined || !has(moduleIndexesToNames, depIndex)) {
throw new Error("File does not use dependency: "+deps[i]);
}
var depName = moduleIndexesToNames[depIndex];
runtimeModuleInfo[depName].decliners.add(name);
runtimeModuleInfo[name].declining.add(depName);
}
}
},
data: runtimeModuleInfo[name].disposeData,
dispose: function(cb) {
return this.addDisposeHandler(cb);
},
addDisposeHandler: function(cb) {
runtimeModuleInfo[name].disposeHandlers.push(cb);
},
removeDisposeHandler: function(cb) {
var ix = runtimeModuleInfo[name].disposeHandlers.indexOf(cb);
if (ix !== -1) {
runtimeModuleInfo[name].disposeHandlers.splice(ix, 1);
}
},
// Management
check: moduleHotCheck,
apply: moduleHotApply,
status: function(cb) {
if (cb) {
return this.addStatusHandler(cb);
}
return localHmr.status;
},
addStatusHandler: function(cb) {
localHmr.statusHandlers.push(cb);
},
removeStatusHandler: function(cb) {
var ix = localHmr.statusHandlers.indexOf(cb);
if (ix !== -1) {
localHmr.statusHandlers.splice(ix, 1);
}
},
setUpdateMode: moduleHotSetUpdateMode
};
}
};
global._hmr[bundleKey] = localHmr;
if (updateMode === 'websocket') {
socket = setupSocket();
}
return true;
} else { // We're in a reload!
global._hmr[bundleKey].newLoad = {
moduleDefs: moduleDefs,
moduleMeta: moduleMeta,
moduleIndexesToNames: moduleIndexesToNames
};
return false;
}
}
module.exports = main;