UNPKG

browserify-hmr

Version:

Hot Module Replacement plugin for Browserify

663 lines (625 loc) 23 kB
'use strict'; 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;