UNPKG

hot-import

Version:

Hot Module Replacement (HMR) for Node.js

282 lines 11.4 kB
"use strict"; /* 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