systemjs-hot-reloader-ex
Version:
SystemJS / JSPM hot reloader with support of CSS, SCSS, SASS, LESS, Stylus, React and JavaScript
561 lines (455 loc) • 16.6 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
require('core-js/shim');
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var SystemHotReloader = function () {
/**
* Constructor.
*
* @param {Object} options
* @prop {Object} loader SystemJS instance, default to SystemJS || System
* @prop {Number} logLevel 0 - none, 1 - error, 2 - info (default), 3 - debug
*/
function SystemHotReloader(options) {
_classCallCheck(this, SystemHotReloader);
var opts = options || {};
this.loader = opts.loader || SystemJS || System;
if (!this.loader) {
throw new Error('Unable to instantiate SystemJS Hot Reloader without SystemJS');
}
if (this.loader.hotReloaderOptions) {
Object.assign(opts, this.loader.hotReloaderOptions);
}
this.logLevel = opts.logLevel === undefined ? 2 : opts.logLevel;
this.enableClearResources = opts.clearResources === undefined ? true : opts.clearResources;
this.logger = this.createLogger('HMR');
if (!this.loader.trace) {
this.logger.error('Set "SystemJS.trace = true" to enable hot reload');
}
if (/^0\.20\./.test(this.loader.version)) {
throw new Error('This version of SystemJS hot reloader is designed for SystemJS v0.20.x');
}
}
/**
* Create logger.
*/
_createClass(SystemHotReloader, [{
key: 'createLogger',
value: function createLogger(prefix) {
var _this = this;
return {
debug: function debug(message) {
if (_this.logLevel >= 3 && console && console.debug) {
console.debug('[' + prefix + '] ' + message);
}
},
info: function info(message) {
if (_this.logLevel >= 2 && console && console.info) {
console.info('[' + prefix + '] ' + message);
}
},
error: function error(message) {
if (_this.logLevel >= 1 && console && console.warn) {
console.warn('[' + prefix + '] ' + message);
}
}
};
}
/**
* Resolve module file path to module name.
*/
}, {
key: 'resolvePath',
value: function resolvePath(path) {
// try obvious resolve filename.ext => filename.ext
var name1 = this.loader.normalizeSync(path);
if (this.loader.get(name1)) {
return name1;
}
// try less obvious resolve filename.ext => filename.ext!
var name2 = this.loader.normalizeSync(path + '!');
if (this.loader.get(name2)) {
return name2;
}
// try to find by filename path in all registered modules, slow :-(
var name3 = Object.keys(this.loader.loads).find(function (name) {
return name.startsWith(name1 + '!');
});
if (name3) {
return name3;
}
return undefined;
}
/**
* Reload module by file path.
*/
}, {
key: 'reloadPath',
value: function reloadPath(path) {
this.logger.debug('Reloading file: ' + path);
var name = this.resolvePath(path);
if (name) {
return this.reloadModule(name);
}
// we did not find module :-(
this.logger.info('Nothing to update');
return Promise.resolve();
}
/**
* Clean full module name from useless base url prefix and loader related suffix.
*/
}, {
key: 'cleanName',
value: function cleanName(name) {
// remove base url prefix
if (name.startsWith(this.loader.baseURL)) {
name = './' + name.substr(this.loader.baseURL.length);
}
// remove loader related garbage
return name.replace(/!.*$/, '');
}
/**
* Reload module by full module name.
*/
}, {
key: 'reloadModule',
value: function reloadModule(moduleName) {
var _this2 = this;
var startTime = window.performance.now();
this.logger.info('Reloading module ' + this.cleanName(moduleName));
if (!this.loader.get(moduleName)) {
this.logger.info('Nothing to update');
return Promise.resolve();
}
var moduleChain = this.getReloadChain([moduleName]);
var moduleBackups = {};
return Promise.resolve().then(function () {
_this2.logger.debug('Reload chain:');
moduleChain.forEach(function (name) {
_this2.logger.debug(' - ' + _this2.cleanName(name));
});
}).then(function () {
_this2.logger.debug('Saving backup');
moduleChain.forEach(function (name) {
moduleBackups[name] = _this2.getModuleBackup(name);
});
}).then(function () {
var promise = Promise.resolve();
moduleChain.forEach(function (name) {
promise = promise.then(function () {
return _this2.reloadModuleInstance(name, moduleChain);
});
});
return promise;
}).then(function () {
if (moduleChain.length) {
_this2.logger.info('Updated modules:');
moduleChain.forEach(function (name) {
var exports = _this2.loader.get(name);
var options = [];
if (exports && exports.__reload) {
options.push('__reload()');
} else if (exports && exports.__unload) {
options.push('__unload()');
}
var suffix = options.length ? '{ ' + options.join(', ') + ' }' : '';
_this2.logger.info(' - ' + _this2.cleanName(name) + ' ' + suffix);
});
} else {
_this2.logger.info('Nothing to update');
}
var time = (window.performance.now() - startTime) / 1000;
var timeSecRound = Math.floor(time * 100) / 100;
_this2.logger.info('Reload took ' + timeSecRound + ' sec');
}).catch(function (error) {
if (error) {
var realError = error.originalErr || error;
_this2.logger.error(realError.stack || realError);
}
_this2.logger.error('An error occured during reloading. Reverting...');
var promise = Promise.resolve();
moduleChain.forEach(function (name) {
promise = promise.then(function () {
return _this2.reloadModuleInstance(name, moduleChain, moduleBackups[name]);
});
});
promise = promise.then(function () {
_this2.logger.info('Application state was restored');
});
return promise;
}).catch(function (error) {
if (error) {
_this2.logger.error(error.stack || error);
}
_this2.logger.error('An unrecoverable error occured during reverting');
});
}
/**
* Reload module instance with option to reload from backup.
*/
}, {
key: 'reloadModuleInstance',
value: function reloadModuleInstance(name, moduleChain, backup) {
var _this3 = this;
var exports = backup ? backup.exports : this.loader.get(name);
var unload = exports ? exports.__unload : undefined;
var reload = exports ? exports.__reload : undefined;
var oldDeps = [];
if (reload) {
return Promise.resolve().then(function () {
return _this3.fixModuleDeps(name);
}).then(function () {
_this3.logger.debug('Calling module ' + _this3.cleanName(name) + ' __reload() hook');
return reload(moduleChain);
});
}
return Promise.resolve().then(function () {
if (!unload) {
return undefined;
}
_this3.logger.debug('Calling module ' + _this3.cleanName(name) + ' unload() hook');
return unload(moduleChain);
}).then(function () {
oldDeps = _this3.getModuleDepNames(name);
}).then(function () {
return backup ? _this3.restoreModuleBackup(backup) : _this3.deleteModule(name);
}).then(function () {
return _this3.importModule(name);
}).then(function () {
var newDeps = _this3.getModuleDepNames(name);
_this3.deleteOldDeps(oldDeps, newDeps);
});
}
/**
* Delete modules from oldDeps if they do not exist in newDeps.
*/
}, {
key: 'deleteOldDeps',
value: function deleteOldDeps(oldDeps, newDeps) {
var _this4 = this;
var depDiff = oldDeps.filter(function (depName) {
return newDeps.indexOf(depName) === -1;
});
depDiff.forEach(function (depName) {
return _this4.deleteModule(depName);
});
}
/**
* Get normalized list of module dependency names based on trace information
* or based on module records.
*/
}, {
key: 'getModuleDepNames',
value: function getModuleDepNames(name) {
var load = this.loader.loads[name];
if (load) {
return load.deps.map(function (address) {
return load.depMap[address];
});
}
// fix me
var moduleRecord = this.loader._loader.moduleRecords[name];
if (moduleRecord) {
return moduleRecord.dependencies.map(function (record) {
return record ? record.name : false;
}).filter(function (depName) {
return !!depName;
});
}
return [];
}
/**
* Fix module dependencies before hooked reload.
*/
}, {
key: 'fixModuleDeps',
value: function fixModuleDeps(name) {
var _this5 = this;
var moduleRecords = this.loader._loader.moduleRecords;
var moduleRecord = moduleRecords[name];
moduleRecord.dependencies.forEach(function (depModuleRecord, index) {
if (!depModuleRecord) {
return;
}
var newDepModuleRecord = moduleRecords[depModuleRecord.name];
if (!newDepModuleRecord) {
return;
}
if (newDepModuleRecord !== depModuleRecord) {
_this5.logger.debug('Fixing dependency ' + _this5.cleanName(depModuleRecord.name) + ' for module ' + _this5.cleanName(moduleRecord.name));
moduleRecord.setters[index](newDepModuleRecord.exports);
if (moduleRecord.dependencies[index] !== newDepModuleRecord.exports) {
moduleRecord.dependencies[index] = newDepModuleRecord;
}
var impRecord = newDepModuleRecord.importers.find(function (record) {
return record && record.name === moduleRecord;
});
if (!impRecord) {
newDepModuleRecord.importers.push(moduleRecord);
}
}
});
}
/**
* Import module.
*/
}, {
key: 'importModule',
value: function importModule(name) {
this.logger.debug('Importing module ' + this.cleanName(name));
return this.loader.import(name);
}
/**
* Delete module and fix importers for dependencies.
*/
}, {
key: 'deleteModule',
value: function deleteModule(name) {
var _this6 = this;
var moduleRecord = this.loader._loader.moduleRecords[name];
if (moduleRecord) {
moduleRecord.dependencies.forEach(function (depModuleRecord) {
if (!depModuleRecord) {
return;
}
depModuleRecord.importers.forEach(function (impModuleRecord, index) {
if (impModuleRecord && moduleRecord.name === impModuleRecord.name) {
_this6.logger.debug('Removing importer ' + _this6.cleanName(impModuleRecord.name) + ' from module ' + _this6.cleanName(depModuleRecord.name));
depModuleRecord.importers.splice(index, 1);
}
});
});
}
this.logger.debug('Removing module ' + this.cleanName(name));
this.loader.delete(name);
if (this.enableClearResources) {
this.clearModuleResources(name);
}
}
/**
* Clear module resources located in DOM.
*
* Usefull for CSS/LESS/SASS/SCSS/Stylus plugins who keep CSS as style or link tags.
*/
}, {
key: 'clearModuleResources',
value: function clearModuleResources(name) {
var address = this.getModuleAddress(name);
var removeNode = function removeNode(node) {
if (window.URL && node.href.startsWith('blob:')) {
URL.revokeObjectURL(node.href);
}
node.remove();
};
// for example, plugin-sass
Array.from(document.querySelectorAll('[data-url="' + address + '"]')).forEach(function (node) {
return removeNode(node);
});
// for example, plugin-css
Array.from(document.querySelectorAll('[data-systemjs-css]')).filter(function (node) {
return node.href === address;
}).forEach(function (node) {
return removeNode(node);
});
}
/**
* Get module address by name.
*
* Try to use trace information if availabe, if not then try to guess it.
* Address guessing should work well for plugins without custom translation hook.
*/
}, {
key: 'getModuleAddress',
value: function getModuleAddress(name) {
var load = this.loader.loads[name];
return load ? load.address : name.replace(/!.*$/, '');
}
/**
* Get module backup which could be used to restore module state.
*/
}, {
key: 'getModuleBackup',
value: function getModuleBackup(name) {
var exports = this.loader.get(name);
var record = this.loader._loader.moduleRecords[name];
return { name: name, record: record, exports: exports };
}
/**
* Restore module from backup.
*/
}, {
key: 'restoreModuleBackup',
value: function restoreModuleBackup(data) {
this.loader.set(data.name, data.exports);
this.loader._loader.moduleRecords[data.name] = data.record;
}
/**
* Get shortest distance to the root module (root modules have no importers).
*/
}, {
key: 'getModuleDistanceToRoot',
value: function getModuleDistanceToRoot(name, record, cache) {
var _this7 = this;
var distance = void 0;
if (cache[name] !== undefined) {
return cache[name];
}
if (!record || !record.importers.length) {
distance = 0;
} else {
distance = record.importers.reduce(function (result, impRecord) {
var impDistance = 1 + _this7.getModuleDistanceToRoot(impRecord.name, impRecord, cache);
return result === null ? impDistance : Math.min(result, impDistance);
}, null);
}
cache[name] = distance;
return distance;
}
/**
* Reduce dependency tree and return modules in the order they should be reloaded.
*/
}, {
key: 'getReloadChain',
value: function getReloadChain(modules, cache) {
var _this8 = this;
if (modules.length === 0) {
return modules;
}
var records = this.loader._loader.moduleRecords;
if (!cache) {
cache = {};
}
var farNode = modules.reduce(function (result, name, index) {
var record = records[name] ? records[name] : undefined;
var distance = _this8.getModuleDistanceToRoot(name, record, cache);
var importers = !record ? [] : record.importers.map(function (item) {
return item.name;
});
var reload = record && record.exports && record.exports.__reload;
var meta = { distance: distance, index: index, name: name, importers: importers, reload: reload };
if (result === undefined) {
return meta;
}
return result.distance >= distance ? result : meta;
}, undefined);
var nextModules = modules.slice(0);
nextModules.splice(farNode.index, 1);
if (!farNode.reload) {
farNode.importers.forEach(function (name) {
if (nextModules.indexOf(name) === -1) {
nextModules.push(name);
}
});
}
var nextResult = this.getReloadChain(nextModules, cache);
var result = [farNode.name].concat(nextResult);
return result;
}
}]);
return SystemHotReloader;
}();
exports.default = SystemHotReloader;
//# sourceMappingURL=SystemHotReloader.js.map