UNPKG

node-web-mvc

Version:
273 lines (272 loc) 10.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /** * @module HotReload */ const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const HotModule_1 = __importDefault(require("./HotModule")); const crypto_1 = __importDefault(require("crypto")); const ListReplacement_1 = __importDefault(require("./updater/ListReplacement")); const index_1 = __importDefault(require("./updater/index")); const HotUpdaterReleaseManager_1 = __importDefault(require("./HotUpdaterReleaseManager")); class HotReload { constructor() { this.ListReplacement = ListReplacement_1.default; this.isHotUpdating = false; this.hotModules = new Map(); this.allHash = {}; } /** * 创建一个数据热更新器 */ createHotUpdater(data, now, old) { return (0, index_1.default)(data, now, old); } /** * 创建指定id的热更新模块,如果模块已存在,则直接返回 * @param {Module} mod 模块对象 */ create(mod) { const id = mod.filename || mod.id; if (!this.hotModules.get(id)) { mod.hot = new HotModule_1.default(id); this.hotModules.set(id, mod.hot); } return this.hotModules.get(id); } /** * 监听文件改动 */ watch(cwd) { if (!cwd) { return; } const runtime = {}; return fs_1.default.watch(cwd, { recursive: true }, (type, filename) => { const isNodeModules = this.options.includeNodeModules !== true && /node_module/.test(filename); if (!isNodeModules && /\.(ts|js)$/.test(filename)) { const filePath = fs_1.default.lstatSync(cwd).isDirectory() ? path_1.default.join(cwd, filename) : cwd; const id = filePath.replace(/^[A-Z]:/, (a) => a.toUpperCase()); clearTimeout(runtime[id]); runtime[id] = setTimeout(() => { delete runtime[id]; this.hotWatch(type, id); }, this.reloadTimeout); } }); } hotWatch(type, filename) { this.isHotUpdating = true; const mode = type === 'rename' ? fs_1.default.existsSync(filename) ? 'created' : 'remove' : type; switch (mode) { case 'created': if (!require.cache[filename]) { console.log('Hot created:', filename); require(filename); const m = require.cache[filename]; // 从子依赖中删除掉刚刚引入的模块,防止出现错误的依赖关系 const index = module.children.indexOf(m); index > -1 ? module.children.splice(index, 1) : undefined; this.invokeHook('created', m); this.invokeHook('postend', m, m); this.invokeHook('done', m, m); } else { this.handleReload(filename); } break; default: this.handleReload(filename); } this.isHotUpdating = false; } renderId(id) { if (process.platform === 'win32') { return require.cache[id] ? id : id.replace(/^[A-Z]:/, (a) => a.toLowerCase()); } return id; } /** * 文件改动时,处理热更新 * @param id */ handleReload(file) { var _a, _b, _c; const id = this.renderId(file); // 当前逻辑不可删除,用于保证不会把没有引用的文件加载进来 const old = require.cache[id]; const hash = this.allHash[id]; const newHash = this.makeHash(file); if (!old || hash === newHash) { // 如果模块已删除,则直接掠过 return; } if (((_c = (_b = (_a = old.hot) === null || _a === void 0 ? void 0 : _a.hooks) === null || _b === void 0 ? void 0 : _b.accept) === null || _c === void 0 ? void 0 : _c.count) > 0) { // 如果模块自定义了accept 则直接执行accept后返回 old.hot.hooks.accept.invoke(null, old); return; } const start = Date.now(); // 检索当前改动模块的所有依赖(无序的,返回的数组顺序不代表依赖顺序) const reasons = this.findAllReasons(old); // 统一清空缓存,用于解决(dependencies无序状态下也能正常按照依赖加载) const dependencies = reasons.map((item) => { const mod = require.cache[item.id]; // 执行hooks.pre this.invokeHook('pre', mod); // 执行hooks.preend this.invokeHook('preend', mod); delete require.cache[item.id]; return mod; }); // 加载依赖 dependencies.filter(Boolean).forEach((dependency) => this.tryReload(dependency)); const now = require.cache[id]; // 执行done this.invokeHook('done', now, old); const end = new Date(); console.log(`Time: ${end.getTime() - start}ms`); console.log(`Built at: ${end.toLocaleDateString()} ${end.toLocaleTimeString()}`); console.log(`Hot reload successfully`); } /** * 重载模块 */ tryReload(old) { const id = old.id; const parent = old.parent; try { // 将hot对象从旧的模块实例上分离 delete old.hot; if (fs_1.default.existsSync(id)) { console.log(`Hot reload: ${id}`); HotUpdaterReleaseManager_1.default.releaseFor(id); // 重新加载 require(id); } else { console.log(`Hot removed: ${id}`); } // 获取当前更新后的模块实例 const now = (require.cache[id] || { removed: true }); // 从子依赖中删除掉刚刚引入的模块,防止出现错误的依赖关系 const index = module.children.indexOf(now); index > -1 ? module.children.splice(index, 1) : undefined; this.invokeHook('postend', now, old); // 还原父依赖 if (old.parent && now) { now.parent = require.cache[old.parent.id]; } return true; } catch (ex) { // 如果热更新异常,则需要还原被删除的内容 const mod = require.cache[id] = (require.cache[id] || old); mod.parent = parent; // 从子依赖中删除掉刚刚引入的模块,防止出现错误的依赖关系 const finded = module.children.find((m) => m.filename === id); const index = module.children.indexOf(finded); index > -1 ? module.children.splice(index, 1) : undefined; console.error('Hot reload error', ex.stack); return false; } } /** * 广播注册的热更新消息 */ invokeHook(name, ...args) { this.hotModules.forEach((m) => { const hook = m.hooks[name]; if (hook) { return hook.invoke.call(hook, ...args); } }); } /** * 项目启动后,初始化构建热更新模块 */ findAllReasons(old) { const isRemoved = !fs_1.default.existsSync(old.id); const topReasons = [this.create(old)]; if (isRemoved) { return topReasons; } const cache = require.cache; const exclude = this.options.exclude; const forMappings = {}; const allMappings = {}; const tryAcceptKeys = Object.keys(cache).filter((k) => !exclude.test(k)); const findDependencies = (source) => { const reasons = []; if (forMappings[source.id]) return []; forMappings[source.id] = true; tryAcceptKeys.forEach((key) => { var _a, _b; const mod = cache[key]; const isAccepted = ((_b = (_a = mod.hot) === null || _a === void 0 ? void 0 : _a.hooks.accept) === null || _b === void 0 ? void 0 : _b.count) > 0; const isSelf = mod == old; if (allMappings[key] || isSelf || isAccepted) return; if (mod.children.indexOf(source) > -1) { allMappings[key] = true; reasons.push(this.create(mod)); reasons.push(...findDependencies(mod)); } }); return reasons; }; return topReasons.concat(findDependencies(old)); } makeHash(file) { const hash = this.createHash(file); const id = this.renderId(file); this.allHash[id] = hash; return hash; } createHash(file) { const content = fs_1.default.existsSync(file) ? fs_1.default.readFileSync(file, 'utf8') : ''; return crypto_1.default.createHash('md5').update(content).digest('hex'); } /** * 监听改变 */ run(options) { options = options || {}; this.options = options; this.options.exclude = this.options.exclude || /node_modules/i; this.reloadTimeout = options.reloadTimeout || 300; const cwd = options.cwd || path_1.default.resolve(''); const dirs = cwd instanceof Array ? cwd : [cwd]; const watchers = dirs.map((item) => this.watch(item)); HotUpdaterReleaseManager_1.default.install(); const handleException = (e) => { if (this.isHotUpdating) { console.error(e); this.isHotUpdating = false; return; } throw e; }; process.on('uncaughtException', handleException); const updater = { options, dirs, close() { process.off('uncaughtException', handleException); watchers.forEach((watcher) => { var _a; return (_a = watcher === null || watcher === void 0 ? void 0 : watcher.close) === null || _a === void 0 ? void 0 : _a.call(watcher); }); }, }; HotUpdaterReleaseManager_1.default.push(() => { this.hotModules.forEach((hot) => hot.clean()); this.hotModules.clear(); updater.close(); }); return updater; } } exports.default = new HotReload();