hot-import
Version:
Hot Module Replacement (HMR) for Node.js
282 lines • 11.4 kB
JavaScript
;
/* eslint no-whitespace-before-property: off */
/* eslint import/export: off */
/* eslint no-useless-return: off */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const path = require("path");
const brolog_1 = require("brolog");
exports.log = brolog_1.log;
const callsites_1 = require("callsites");
var version_1 = require("./version");
exports.VERSION = version_1.VERSION;
exports.moduleStore = {};
exports.proxyStore = {};
exports.watcherStore = {};
function refreshImport(absFilePath) {
return __awaiter(this, void 0, void 0, function* () {
brolog_1.log.silly('HotImport', 'refreshImport(%s)', absFilePath);
const oldCache = purgeRequireCache(absFilePath);
try {
const refreshedModule = yield importFile(absFilePath);
exports.moduleStore[absFilePath] = refreshedModule;
cloneProperties(exports.proxyStore[absFilePath], exports.moduleStore[absFilePath]);
brolog_1.log.verbose('HotImport', 'refreshImport(%s) Hot Module Replacement (HMR): Re-Imported', absFilePath);
}
catch (e) {
brolog_1.log.error('HotImport', 'refreshImport(%s) exception: %s', absFilePath, e);
restoreRequireCache(absFilePath, oldCache);
brolog_1.log.error('HotImport', 'refreshImport(%s) keep using the latest usable version', absFilePath);
}
});
}
exports.refreshImport = refreshImport;
function hotImport(modulePathRelativeToCaller, watch = true) {
return __awaiter(this, void 0, void 0, function* () {
brolog_1.log.verbose('HotImport', 'hotImport(%s, %s)', modulePathRelativeToCaller, watch);
if (!modulePathRelativeToCaller) {
if (watch) {
makeHotAll();
}
else {
makeColdAll();
}
return;
}
// convert './module' to '${cwd}/module.js'
const absFilePath = require.resolve(callerResolve(modulePathRelativeToCaller));
if (!watch) {
makeCold(absFilePath);
return;
}
if (absFilePath in exports.moduleStore) {
brolog_1.log.silly('HotImport', 'hotImport() moduleStore[%s] already exist.', absFilePath);
return exports.proxyStore[absFilePath];
}
const realModule = yield importFile(absFilePath);
exports.moduleStore[absFilePath] = realModule;
exports.proxyStore[absFilePath] = initProxyModule(absFilePath);
cloneProperties(exports.proxyStore[absFilePath], exports.moduleStore[absFilePath]);
makeHot(absFilePath);
return exports.proxyStore[absFilePath];
});
}
exports.hotImport = hotImport;
function makeHot(absFilePath) {
brolog_1.log.silly('HotImport', 'makeHot(%s)', absFilePath);
if (exports.watcherStore[absFilePath]) {
brolog_1.log.silly('HotImport', `makeHot(${absFilePath}) it's already hot, skip it`);
return;
}
const watcher = fs.watch(absFilePath);
watcher.on('change', onChange);
exports.watcherStore[absFilePath] = watcher;
function onChange(eventType, filename) {
brolog_1.log.silly('HotImport', 'makeHot(%s) onChange(%s, %s)', absFilePath, eventType, filename);
if (eventType !== 'change') {
return;
}
let size = 0;
try {
size = fs.statSync(absFilePath).size;
}
catch (e) {
brolog_1.log.silly('HotImport', 'makeHot() onChange() fs.statSync(%s) exception: %s', absFilePath, e);
}
// change event might fire multiple times, one for create(with zero size), others for write.
if (size === 0) {
brolog_1.log.silly('HotImport', 'makeHot() onChange() fs.statSync(%s) size:0', absFilePath);
return;
}
refreshImport(absFilePath).catch(e => brolog_1.log.error('HotImport', 'makeHot() refreshImport rejection: %s', e && e.message));
}
}
exports.makeHot = makeHot;
function makeCold(absFilePathOrMod) {
brolog_1.log.silly('HotImport', 'makeCold(%s)', absFilePathOrMod);
let absFilePath;
if (typeof absFilePathOrMod === 'string') { // filePath
absFilePath = absFilePathOrMod;
}
else { // module
absFilePath = mod2file(absFilePathOrMod);
}
const watcher = exports.watcherStore[absFilePath];
if (watcher) {
watcher.close();
delete exports.watcherStore[absFilePath];
}
else {
brolog_1.log.silly('HotImport', 'makeCold(%s) already cold.', absFilePath);
}
return;
function mod2file(mod) {
for (const file in exports.proxyStore) {
if (exports.proxyStore[file] === mod) {
return file;
}
}
throw new Error('makeClold() mod2file() can not found module in proxyStore');
}
}
exports.makeCold = makeCold;
function makeColdAll() {
brolog_1.log.verbose('HotImport', 'makeColdAll()');
for (const file in exports.watcherStore) {
makeCold(file);
}
}
exports.makeColdAll = makeColdAll;
function makeHotAll() {
brolog_1.log.verbose('HotImport', 'makeHotAll()');
for (const file in exports.watcherStore) {
makeHot(file);
}
}
exports.makeHotAll = makeHotAll;
function cloneProperties(dst, src) {
brolog_1.log.silly('HotImport', 'cloneProperties()');
for (const prop in dst) {
brolog_1.log.silly('HotImport', 'cloneProperties() cleaning dst.%s', prop);
delete dst[prop];
}
for (const prop in src) {
brolog_1.log.silly('HotImport', 'cloneProperties() cloning %s', prop);
dst[prop] = src[prop];
}
if (src.prototype) {
brolog_1.log.silly('HotImport', 'cloneProperties() cloning prototype');
dst.prototype = src.prototype;
}
}
exports.cloneProperties = cloneProperties;
/**
* Resolve filename based on caller's __dirname
*/
function callerResolve(filePath, callerFileExcept) {
brolog_1.log.verbose('HotImport', 'callerResolve(%s, %s)', filePath, callerFileExcept);
if (path.isAbsolute(filePath)) {
return filePath;
}
const fileSkipList = [__filename];
if (callerFileExcept) {
fileSkipList.push(callerFileExcept);
}
let callerFile;
for (const callsite of callsites_1.default()) {
const file = callsite.getFileName();
const type = callsite.getTypeName();
brolog_1.log.silly('HotImport', 'callerResolve() callsites() file=%s, type=%s', file, type);
/**
* type sometimes is null?
* callsites() file=/home/zixia/chatie/wechaty/node_modules/hot-import/dist/src/hot-import.js, type=Object
* callsites() file=/home/zixia/chatie/wechaty/src/puppet/puppet.ts, type=PuppetPuppeteer
* callsites() file=/home/zixia/chatie/wechaty/src/puppet-puppeteer/puppet-puppeteer.ts, type=null
* callsites() file=/home/zixia/chatie/wechaty/src/wechaty.ts, type=Wechaty
* callerFile=/home/zixia/chatie/wechaty/src/wechaty.ts
*/
if (file /* && type */) {
let skip = false;
fileSkipList.some(skipFile => !!(skip = (skipFile === file)));
if (skip) {
continue;
}
callerFile = file;
break;
}
}
if (!callerFile) {
throw new Error('not found caller file?');
}
brolog_1.log.silly('HotImport', 'callerResolve() callerFile=%s', callerFile);
const callerDir = path.dirname(callerFile);
const absFilePath = path.resolve(callerDir, filePath);
return absFilePath;
}
exports.callerResolve = callerResolve;
/**
* create an object instance (via the new operator),
* but pass an arbitrary number of arguments to the constructor.
* https://stackoverflow.com/a/8843181/1123955
*/
// eslint-disable-next-line
function newCall(cls, ..._) {
const instance = new (Function.prototype.bind.apply(cls, arguments))();
return instance;
}
exports.newCall = newCall;
function initProxyModule(absFilePath) {
brolog_1.log.silly('HotImport', 'initProxyModule(%s)', absFilePath);
if (!(absFilePath in exports.moduleStore)) {
throw new Error(`moduleStore has no ${absFilePath}!`);
}
const proxyModule = function (...args) {
brolog_1.log.silly('HotImport', 'initProxyModule() proxyModule()');
let realModule = exports.moduleStore[absFilePath];
if (!realModule) {
brolog_1.log.error('HotImport', 'initProxyModule() proxyModule() moduleStore[%s] empty!', absFilePath);
throw new Error('Cannot get realModule from moduleStore for ' + absFilePath);
}
// use default by default.
// `hotMod = hotImport(...) v.s. import hotMod from '...'
if (typeof realModule.default === 'function') {
brolog_1.log.silly('HotImport', 'initProxyModule() proxyModule() using default export');
realModule = realModule.default;
}
if (typeof realModule !== 'function') {
throw new TypeError('is not a function');
}
// https://stackoverflow.com/a/31060154/1123955
if (new.target) { // called with constructor: `new HotMod()`
return newCall(realModule, ...args);
}
// called without constructor: `hotMethod()`
return realModule.apply(this, args);
};
return proxyModule;
}
exports.initProxyModule = initProxyModule;
function importFile(absFilePath) {
return __awaiter(this, void 0, void 0, function* () {
brolog_1.log.silly('HotImport', 'importFile(%s)', absFilePath);
if (!path.isAbsolute(absFilePath)) {
throw new Error('must be absolute path!');
}
try {
const realModule = yield Promise.resolve().then(() => require(absFilePath));
return realModule;
}
catch (e) {
brolog_1.log.error('HotImport', 'importFile(%s) rejected: %s', absFilePath, e);
throw e;
}
});
}
exports.importFile = importFile;
function purgeRequireCache(absFilePath) {
brolog_1.log.silly('HotImport', 'purgeRequireCache(%s)', absFilePath);
const mod = require.resolve(absFilePath);
const oldCache = require.cache[mod];
if (!oldCache) {
throw new Error(`oldCache not found for mod:${mod}`);
}
delete require.cache[mod];
return oldCache;
}
exports.purgeRequireCache = purgeRequireCache;
function restoreRequireCache(absFilePath, cache) {
brolog_1.log.silly('HotImport', 'restoreRequireCache(%s, cache)', absFilePath);
const mod = require.resolve(absFilePath);
require.cache[mod] = cache;
}
exports.restoreRequireCache = restoreRequireCache;
exports.default = hotImport;
//# sourceMappingURL=hot-import.js.map