├── .editorconfig ├── .gitignore ├── .travis.yml ├── License ├── README.md ├── example └── express-hot │ ├── hot-middleware │ ├── a.js │ ├── b.js │ ├── c.js │ ├── child │ │ ├── a.js │ │ └── b.js │ └── index.js │ └── index.js ├── index.js ├── package-lock.json ├── package.json └── test ├── fixture ├── circle │ ├── a.js │ ├── b.js │ └── index.js ├── complex │ ├── a.js │ ├── b.js │ ├── c.js │ └── root.js ├── deep │ ├── a.js │ ├── b.js │ └── index.js └── hot-middleware │ ├── a.js │ ├── b.js │ └── index.js ├── main.test.js └── setup.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | node_modules/ 4 | dist/ 5 | public/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 6 5 | - 7 6 | - 8 7 | - stable 8 | 9 | cache: 10 | bundle: true 11 | directories: 12 | - node_modules 13 | 14 | install: 15 | - npm install 16 | 17 | before_script: 18 | - npm install codecov 19 | script: 20 | - npm test -- --coverage 21 | after_script: 22 | - codecov --token=$CODECOV_TOKEN 23 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 imcuttle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hot-module-require 2 | 3 | 4 | 5 | 6 | 7 | [![NPM version](https://img.shields.io/npm/v/hot-module-require.svg?style=flat-square)](https://www.npmjs.com/package/hot-module-require) 8 | [![NPM Downloads](https://img.shields.io/npm/dm/hot-module-require.svg?style=flat-square&maxAge=43200)](https://www.npmjs.com/package/hot-module-require) 9 | 10 | Detect module's update recursively on nodejs. 11 | 12 | ## Usage 13 | 14 | ```javascript 15 | // module.js 16 | module.exports = require('./foo') + require('./bar') 17 | ``` 18 | 19 | ```javascript 20 | const makeHotRequire = require('hot-module-require') 21 | const hotRequire = makeHotRequire(__dirname) 22 | 23 | let mExports = require('./module') 24 | 25 | hotRequire.accept(['./module'], (oldModule, path) => { 26 | // Do something here 27 | // when './module' module or submodules('./foo', './bar'') be detected changed. 28 | let newExports = require('./module') 29 | }) 30 | 31 | 32 | // Or use it like follows 33 | const hotModuleGetter = hotRequire('./module') 34 | hotModuleGetter() // Returns the already updated `require('./module')`` 35 | 36 | hotModuleGetter.remove() // Calls `remove` for interrupting detect updated 37 | ``` 38 | 39 | ## [Express Example](./example/express-hot/index.js) 40 | 41 | ```bash 42 | npm run example 43 | ``` 44 | 45 | ## API 46 | 47 | 48 | 49 | ### makeHotRequireFunction 50 | 51 | [index.js:52-373](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L52-L373 "Source code on GitHub") 52 | 53 | - **See: More options see [detect-dep](https://github.com/imcuttle/detect-dep)** 54 | 55 | make a hot require instance 56 | 57 | #### Parameters 58 | 59 | - `dirname` (optional, default `''`) 60 | - `presetOpts` {{}} (optional, default `{}`) 61 | - `presetOpts.recursive` {boolean} Analysis file recursively (optional, default `true`) 62 | 63 | Returns **[HotRequire](#hotrequire)** 64 | 65 | ### HotRequire 66 | 67 | [index.js:173-184](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L173-L184 "Source code on GitHub") 68 | 69 | ### resolve 70 | 71 | [index.js:256-256](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L256-L256 "Source code on GitHub") 72 | 73 | Resolve file name 74 | 75 | #### Parameters 76 | 77 | - `name` {string} 78 | 79 | ### watcher 80 | 81 | [index.js:263-263](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L263-L263 "Source code on GitHub") 82 | 83 | - **See: [chokidar](https://npmjs.com/chokidar)** 84 | 85 | file Watcher 86 | 87 | ### emitter 88 | 89 | [index.js:269-269](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L269-L269 "Source code on GitHub") 90 | 91 | The event emitter 92 | 93 | ### dependent 94 | 95 | [index.js:276-276](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L276-L276 "Source code on GitHub") 96 | 97 | The map about dependent relations 98 | 99 | Type: [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) 100 | 101 | ### dependence 102 | 103 | [index.js:283-283](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L283-L283 "Source code on GitHub") 104 | 105 | The map about dependence relations 106 | 107 | Type: [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) 108 | 109 | ### getDependenceTree 110 | 111 | [index.js:293-293](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L293-L293 "Source code on GitHub") 112 | 113 | - **See: ** 114 | 115 | Get dependence tree of which file 116 | 117 | #### Parameters 118 | 119 | - `modulePath` {string} 120 | - `opts` 121 | 122 | Returns **{}** 123 | 124 | ### addDependencies 125 | 126 | [index.js:303-303](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L303-L303 "Source code on GitHub") 127 | 128 | Add Dependencies 129 | 130 | #### Parameters 131 | 132 | - `modulePath` {string} 133 | - `deps` {string\[]} 134 | 135 | ### removeDependencies 136 | 137 | [index.js:311-311](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L311-L311 "Source code on GitHub") 138 | 139 | Remove Dependencies 140 | 141 | #### Parameters 142 | 143 | - `modulePath` {string} 144 | - `deps` {string\[]} 145 | 146 | ### accept 147 | 148 | [index.js:320-331](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L320-L331 "Source code on GitHub") 149 | 150 | Watch file with callback and make dependence(dependent) relations 151 | 152 | #### Parameters 153 | 154 | - `deps` {string\[]} 155 | - `callback` {function} 156 | 157 | ### refuse 158 | 159 | [index.js:339-361](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L339-L361 "Source code on GitHub") 160 | 161 | Watch file with callback and make dependence(dependent) relations 162 | 163 | #### Parameters 164 | 165 | - `deps` {string\[]} 166 | - `callback` {function} 167 | 168 | ### close 169 | 170 | [index.js:368-370](https://github.com/imcuttle/hot-module-require/blob/2e3792c30be25d7ebbf83177f08d3506b4749575/index.js#L368-L370 "Source code on GitHub") 171 | 172 | Close file watcher 173 | 174 | Returns **any** void 175 | 176 | ## Related 177 | 178 | - [detect-dep](https://github.com/imcuttle/detect-dep) - Detect file's dependencies. 179 | -------------------------------------------------------------------------------- /example/express-hot/hot-middleware/a.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file a 3 | * @author imcuttle 4 | * @date 2019/6/24 5 | * 6 | * a 7 | * / | 8 | * b / 9 | * / / 10 | * c - 11 | */ 12 | 13 | 14 | module.exports = [require('./b'), require('./c'), 'in a'] 15 | -------------------------------------------------------------------------------- /example/express-hot/hot-middleware/b.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file a 3 | * @author imcuttle 4 | * @date 2019/6/24 5 | * 6 | */ 7 | module.exports = [require('./c'), 'in b'] 8 | -------------------------------------------------------------------------------- /example/express-hot/hot-middleware/c.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file a 3 | * @author imcuttle 4 | * @date 2019/6/24 5 | * 6 | */ 7 | const child = 'b.js' 8 | module.exports = 'in cxd ' + require('./child/' + child) 9 | -------------------------------------------------------------------------------- /example/express-hot/hot-middleware/child/a.js: -------------------------------------------------------------------------------- 1 | module.exports = 'child a.js ' 2 | -------------------------------------------------------------------------------- /example/express-hot/hot-middleware/child/b.js: -------------------------------------------------------------------------------- 1 | module.exports = 'child b.js' + '' 2 | -------------------------------------------------------------------------------- /example/express-hot/hot-middleware/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file index 3 | * @author imcuttle 4 | * @date 2019/6/24 5 | * 6 | */ 7 | 8 | module.exports = (req, res, next) => { 9 | res.json(require('./a')) 10 | } 11 | -------------------------------------------------------------------------------- /example/express-hot/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file index 3 | * @author imcuttle 4 | * @date 2019/6/24 5 | * 6 | */ 7 | const express = require('express') 8 | const makeHotModule = require('../..') 9 | 10 | const hotRequire = makeHotModule(__dirname) 11 | const getter = hotRequire('./hot-middleware') 12 | 13 | const app = express() 14 | app.all('/', function () { 15 | getter().apply(this, arguments) 16 | }) 17 | 18 | app.listen(9999, () => { 19 | console.log(`Run on http://localhost:9999`) 20 | }) 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file index 3 | * @author imcuttle 4 | * @date 2018/4/4 5 | * @description 6 | */ 7 | const { detectDep, tree: detectDepTree } = require('detect-dep'); 8 | const assert = require('assert'); 9 | const visitTree = require('@moyuyc/visit-tree'); 10 | const nps = require('path'); 11 | const chokidar = require('chokidar'); 12 | const debug = require('debug')('hot-module-require'); 13 | const EventEmitter = require('events'); 14 | 15 | function delay(timeout) { 16 | return new Promise((resolve) => { 17 | setTimeout(resolve, timeout); 18 | }); 19 | } 20 | 21 | function toArray(item) { 22 | return Array.isArray(item) ? item : [item]; 23 | } 24 | 25 | function _moduleKey(resolvedModulePath) { 26 | return `dep: ${resolvedModulePath}`; 27 | } 28 | const BOTH_EVENT_TYPE = '$both'; 29 | 30 | function visitUniqTree(tree, visit) { 31 | const map = new WeakMap(); 32 | visitTree(tree, (node, ctx) => { 33 | // Skip the visited node 34 | if (map.has(node)) { 35 | return ctx.skip(); 36 | } 37 | 38 | visit && visit(node, ctx); 39 | map.set(node, 'visited'); 40 | }); 41 | } 42 | 43 | /** 44 | * make a hot require instance 45 | * @param dirname 46 | * @param presetOpts {{}} 47 | * @param [presetOpts.recursive=true] {boolean} Analysis file recursively 48 | * @see More options see [detect-dep](https://github.com/imcuttle/detect-dep) 49 | * @public 50 | * @return {HotRequire} 51 | */ 52 | function makeHotRequireFunction(dirname = '', presetOpts = {}) { 53 | assert(dirname, 'missing dirname'); 54 | assert(typeof dirname === 'string', 'dirname must be a string'); 55 | presetOpts = Object.assign({ recursive: true }, presetOpts); 56 | 57 | const resolve = presetOpts.resolvePath 58 | ? (modulePath) => { 59 | return presetOpts.resolvePath(modulePath, { dirname, ...presetOpts }); 60 | } 61 | : function (modulePath) { 62 | let resolvedModulePath; 63 | if (modulePath.startsWith('.')) { 64 | resolvedModulePath = require.resolve(nps.resolve(dirname, modulePath)); 65 | } else { 66 | resolvedModulePath = require.resolve(modulePath); 67 | } 68 | return resolvedModulePath; 69 | }; 70 | 71 | function getDependenceTree(modulePath, opts) { 72 | const resolvedOptions = Object.assign({ moduleImport: false }, presetOpts, opts); 73 | let resolvedModulePath = hotRequire.resolve(modulePath); 74 | 75 | if (!resolvedOptions.recursive) { 76 | return { 77 | id: resolvedModulePath, 78 | children: detectDep(resolvedModulePath, resolvedOptions).map((path) => { 79 | return { 80 | id: path, 81 | children: [], 82 | }; 83 | }), 84 | }; 85 | } 86 | 87 | return detectDepTree(resolvedModulePath, resolvedOptions); 88 | } 89 | 90 | function addDependencies(modulePath, deps = []) { 91 | const { dependence, dependent } = hotRequire; 92 | let resolvedModulePath = hotRequire.resolve(modulePath); 93 | function add(map, key, value) { 94 | if (!map.has(key)) { 95 | map.set(key, []); 96 | } 97 | let arr = map.get(key); 98 | if (arr.indexOf(value) < 0) { 99 | arr.push(value); 100 | } 101 | } 102 | 103 | deps.forEach((dep) => { 104 | hotRequire.watcher.add(dep); 105 | add(dependence, resolvedModulePath, dep); 106 | add(dependent, dep, resolvedModulePath); 107 | }); 108 | 109 | hotRequire.watcher.add(resolvedModulePath); 110 | } 111 | 112 | function removeDependencies(modulePath, deps) { 113 | const { dependence, dependent } = hotRequire; 114 | let resolvedModulePath = hotRequire.resolve(modulePath); 115 | if (!deps) { 116 | deps = dependence.get(resolvedModulePath); 117 | } 118 | function remove(map, key, value) { 119 | if (!map.has(key)) { 120 | return; 121 | } 122 | let arr = map.get(key); 123 | let i = arr.indexOf(value); 124 | if (i >= 0) { 125 | arr.splice(i, 1); 126 | } 127 | } 128 | 129 | (deps || []).forEach((dep) => { 130 | remove(dependence, resolvedModulePath, dep); 131 | remove(dependent, dep, resolvedModulePath); 132 | 133 | if (!dependent.get(dep) || !dependent.get(dep).length) { 134 | hotRequire.watcher.unwatch(dep); 135 | } 136 | }); 137 | 138 | if (!dependent.get(resolvedModulePath) || !dependent.get(resolvedModulePath).length) { 139 | hotRequire.watcher.unwatch(resolvedModulePath); 140 | } 141 | } 142 | 143 | function hotRegister(modulePath, opts = {}) { 144 | function innerRegister(modulePath, opts = {}, map = {}) { 145 | opts = Object.assign({}, presetOpts, opts); 146 | let resolvedModulePath = hotRequire.resolve(modulePath); 147 | 148 | if (map.hasOwnProperty(resolvedModulePath)) { 149 | return; 150 | } 151 | 152 | map[resolvedModulePath] = 'visiting'; 153 | if (nps.isAbsolute(resolvedModulePath)) { 154 | let depTree = hotRequire.getDependenceTree(resolvedModulePath, opts); 155 | visitUniqTree(depTree, (node, ctx) => { 156 | hotRequire.removeDependencies(node.id); 157 | const deps = node.children.map((mod) => mod.id); 158 | hotRequire.addDependencies(node.id, deps); 159 | debug('deps %O \nof file: %s', deps, node.id); 160 | }); 161 | } 162 | map[resolvedModulePath] = 'visited'; 163 | } 164 | 165 | return innerRegister(modulePath, opts); 166 | } 167 | 168 | /** 169 | * @name HotRequire 170 | * @public 171 | * @typedef {Function & {remove: Function}} 172 | */ 173 | function hotRequire(modulePath) { 174 | modulePath = hotRequire.resolve(modulePath); 175 | 176 | const listener = (oldModule, path) => {}; 177 | hotRequire.accept([modulePath], listener); 178 | 179 | return Object.assign(() => require(modulePath), { 180 | remove: () => { 181 | return hotRequire.refuse(modulePath, listener); 182 | }, 183 | }); 184 | } 185 | 186 | const watcher = chokidar.watch([], { 187 | persistent: true, 188 | }); 189 | const dependent = new Map(); 190 | const dependence = new Map(); 191 | const emitter = new EventEmitter(); 192 | 193 | function hotUpdate(path, opts) { 194 | function innerHotUpdate(path, opts, map = {}) { 195 | if (map.hasOwnProperty(path)) { 196 | return; 197 | } 198 | 199 | let old = require.cache[path]; 200 | debug('hotUpdate %s \n', path); 201 | delete require.cache[path]; 202 | 203 | // Update dep tree 204 | hotRequire.register(path); 205 | // Trigger event 206 | emitter.emit(_moduleKey(path), old, path); 207 | 208 | // Backward update 209 | const { dependent, dependence } = hotRequire; 210 | let dependents = dependent.get(path); 211 | debug('file %s => dependents: %O.', path, dependents); 212 | // Create a new map, For tracking one direction instead of the global dep graph 213 | map = Object.assign( 214 | { 215 | [path]: 'visiting', 216 | }, 217 | map, 218 | ); 219 | dependents && 220 | dependents.forEach((path) => { 221 | // return p.then(() => ) 222 | innerHotUpdate(path, opts, map); 223 | }); 224 | map[path] = 'visited'; 225 | } 226 | 227 | return innerHotUpdate(path, opts, {}); 228 | 229 | // Remove the dependencies 230 | // let deps = dependence.get(path) 231 | // deps && 232 | // deps.forEach(dep => { 233 | // dependents = dependent.get(dep) 234 | // if (dependents) { 235 | // let i = dependents.indexOf(path) 236 | // if (i >= 0) { 237 | // dependents.splice(i, 1) 238 | // } 239 | // } 240 | // }) 241 | } 242 | 243 | watcher.on('change', (path) => { 244 | debug('watch file %s changed.', path); 245 | debug('dependent: %O', dependent); 246 | debug('dependence: %O', dependence); 247 | hotUpdate(path); 248 | }); 249 | 250 | /** 251 | * Resolve file name 252 | * @memberOf HotRequire 253 | * @public 254 | * @param name {string} 255 | */ 256 | hotRequire.resolve = resolve; 257 | /** 258 | * file Watcher 259 | * @memberOf HotRequire 260 | * @public 261 | * @see [chokidar](https://npmjs.com/chokidar) 262 | */ 263 | hotRequire.watcher = watcher; 264 | /** 265 | * The event emitter 266 | * @memberOf HotRequire 267 | * @public 268 | */ 269 | hotRequire.emitter = emitter; 270 | /** 271 | * The map about dependent relations 272 | * @memberOf HotRequire 273 | * @public 274 | * @type {Map} 275 | */ 276 | hotRequire.dependent = dependent; 277 | /** 278 | * The map about dependence relations 279 | * @memberOf HotRequire 280 | * @public 281 | * @type {Map} 282 | */ 283 | hotRequire.dependence = dependence; 284 | /** 285 | * Get dependence tree of which file 286 | * @memberOf HotRequire 287 | * @public 288 | * @param modulePath {string} 289 | * @param opts 290 | * @see https://github.com/imcuttle/detect-dep#tree 291 | * @return {{}} 292 | */ 293 | hotRequire.getDependenceTree = getDependenceTree; 294 | hotRequire.register = hotRegister; 295 | hotRequire.hotUpdate = hotUpdate; 296 | /** 297 | * Add Dependencies 298 | * @memberOf HotRequire 299 | * @public 300 | * @param modulePath {string} 301 | * @param deps {string[]} 302 | */ 303 | hotRequire.addDependencies = addDependencies; 304 | /** 305 | * Remove Dependencies 306 | * @memberOf HotRequire 307 | * @public 308 | * @param modulePath {string} 309 | * @param deps {string[]} 310 | */ 311 | hotRequire.removeDependencies = removeDependencies; 312 | 313 | /** 314 | * Watch file with callback and make dependence(dependent) relations 315 | * @memberOf HotRequire 316 | * @public 317 | * @param deps {string[]} 318 | * @param callback {function} 319 | */ 320 | hotRequire.accept = function accept(deps, callback) { 321 | if (!deps) { 322 | emitter.addListener(BOTH_EVENT_TYPE, callback); 323 | return; 324 | } 325 | 326 | toArray(deps).forEach((dep) => { 327 | let resolvedModulePath = hotRequire.resolve(dep); 328 | hotRequire.register(dep); 329 | emitter.addListener(_moduleKey(resolvedModulePath), callback); 330 | }); 331 | }; 332 | /** 333 | * Watch file with callback and make dependence(dependent) relations 334 | * @memberOf HotRequire 335 | * @public 336 | * @param deps {string[]} 337 | * @param callback {function} 338 | */ 339 | hotRequire.refuse = function refuse(deps, callback) { 340 | function remove(type, path) { 341 | if (!callback) { 342 | emitter.removeAllListeners(type); 343 | } else { 344 | emitter.removeListener(type, callback); 345 | } 346 | 347 | // Remove dependencies & unwatch 348 | if (path && !emitter.listeners(type).length) { 349 | removeDependencies(path); 350 | } 351 | } 352 | if (!deps) { 353 | remove(BOTH_EVENT_TYPE); 354 | return; 355 | } 356 | 357 | toArray(deps).forEach((dep) => { 358 | let resolvedModulePath = hotRequire.resolve(dep); 359 | remove(_moduleKey(resolvedModulePath), resolvedModulePath); 360 | }); 361 | }; 362 | /** 363 | * Close file watcher 364 | * @memberOf HotRequire 365 | * @return void 366 | * @public 367 | */ 368 | hotRequire.close = function () { 369 | hotRequire.watcher.close(); 370 | }; 371 | 372 | return hotRequire; 373 | } 374 | 375 | module.exports = makeHotRequireFunction; 376 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hot-module-require", 3 | "version": "4.0.3", 4 | "main": "index.js", 5 | "description": "Detect module's update recursively on nodejs.", 6 | "author": "imcuttle", 7 | "scripts": { 8 | "test": "node ./test/setup.js ./test/*.test.js", 9 | "example": "node ./example/express-hot", 10 | "doc": "documentation --github --markdown-toc=false readme index.js -a public -s \"API\" && git add README.md", 11 | "version": "npm run doc" 12 | }, 13 | "keywords": [ 14 | "update", 15 | "hot-reload", 16 | "hot-module-require" 17 | ], 18 | "engines": { 19 | "node": ">=8" 20 | }, 21 | "license": "MIT", 22 | "repository": "imcuttle/hot-module-require", 23 | "devDependencies": { 24 | "@types/jest": "^22.2.3", 25 | "documentation": "^11.0.1", 26 | "express": "^4.17.1", 27 | "jest": "^22.4.4", 28 | "minimist": "^1.2.5" 29 | }, 30 | "dependencies": { 31 | "@moyuyc/visit-tree": "^3.0.0", 32 | "chokidar": "^3.4.2", 33 | "debug": "^4.2.0", 34 | "detect-dep": "^1.1.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/fixture/circle/a.js: -------------------------------------------------------------------------------- 1 | module.exports = ["a-2", require("./b")] -------------------------------------------------------------------------------- /test/fixture/circle/b.js: -------------------------------------------------------------------------------- 1 | module.exports = ["b-2", require("./")] -------------------------------------------------------------------------------- /test/fixture/circle/index.js: -------------------------------------------------------------------------------- 1 | module.exports = ["index-2", require("./a")] -------------------------------------------------------------------------------- /test/fixture/complex/a.js: -------------------------------------------------------------------------------- 1 | module.exports = 1 + require('./c') -------------------------------------------------------------------------------- /test/fixture/complex/b.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 -------------------------------------------------------------------------------- /test/fixture/complex/c.js: -------------------------------------------------------------------------------- 1 | module.exports = 10 -------------------------------------------------------------------------------- /test/fixture/complex/root.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./a') + require('./b') + require('./c') -------------------------------------------------------------------------------- /test/fixture/deep/a.js: -------------------------------------------------------------------------------- 1 | module.exports = require('.') -------------------------------------------------------------------------------- /test/fixture/deep/b.js: -------------------------------------------------------------------------------- 1 | module.exports = 2; -------------------------------------------------------------------------------- /test/fixture/deep/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./a") + require("./b"); -------------------------------------------------------------------------------- /test/fixture/hot-middleware/a.js: -------------------------------------------------------------------------------- 1 | module.exports = 4 -------------------------------------------------------------------------------- /test/fixture/hot-middleware/b.js: -------------------------------------------------------------------------------- 1 | module.exports = 4 -------------------------------------------------------------------------------- /test/fixture/hot-middleware/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (v) => { 2 | return (a = 0) => { 3 | return require('./a') + v + a 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/main.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file main 3 | * @author imcuttle 4 | * @date 2018/4/4 5 | */ 6 | // process.env.DEBUG = 'hot-module-require' 7 | const makeHotRequire = require('../') 8 | const nps = require('path') 9 | const fs = require('fs') 10 | 11 | const _aCode = 'module.exports = 1;' 12 | const _bCode = 'module.exports = 2;' 13 | const _indexCode = 'module.exports = require("./a") + require("./b");' 14 | 15 | function clearRequire() { 16 | for (let key in require.cache) { 17 | delete require.cache[key] 18 | } 19 | } 20 | 21 | function delay(timeout = 2000) { 22 | return new Promise(resolve => { 23 | setTimeout(resolve, timeout) 24 | }) 25 | } 26 | 27 | function deepCaseWrite( 28 | dir, 29 | aCode = _aCode, 30 | bCode = _bCode, 31 | indexCode = _indexCode 32 | ) { 33 | aCode && fs.writeFileSync(nps.join(dir, 'a.js'), aCode) 34 | bCode && fs.writeFileSync(nps.join(dir, 'b.js'), bCode) 35 | indexCode && fs.writeFileSync(nps.join(dir, 'index.js'), indexCode) 36 | } 37 | 38 | let hotRequire 39 | it('should hotRequire injected some properties', function(done) { 40 | hotRequire = makeHotRequire(__dirname) 41 | assert.equal(typeof hotRequire.accept, 'function') 42 | assert.equal(typeof hotRequire.refuse, 'function') 43 | hotRequire.close() 44 | done() 45 | }) 46 | 47 | it('should works in deep dependencies', function(done) { 48 | clearRequire() 49 | hotRequire = makeHotRequire(__dirname) 50 | let base = nps.join(__dirname, './fixture/deep') 51 | deepCaseWrite(base) 52 | assert.equal(require('./fixture/deep/index'), 3) 53 | 54 | let count = 0 55 | hotRequire.accept(nps.join(base, 'index.js'), function(module, path) { 56 | count++ 57 | console.log('count', count) 58 | assert.equal(module.id, path) 59 | assert.equal(require.cache[path], undefined) 60 | assert.equal(require(path), 4) 61 | 62 | console.log('hotRequire.close()') 63 | hotRequire.close() 64 | done() 65 | }) 66 | 67 | delay().then(() => { 68 | deepCaseWrite(base, 'module.exports = 2;', null, null) 69 | }) 70 | // expect(count).toBe(1) 71 | }) 72 | 73 | it('should works in simple dependencies', function(done) { 74 | clearRequire() 75 | hotRequire && hotRequire.close() 76 | hotRequire = makeHotRequire(__dirname) 77 | let base = nps.join(__dirname, './fixture/deep') 78 | deepCaseWrite(base) 79 | 80 | require(nps.join(base, 'index.js')) 81 | 82 | return delay(300).then(() => { 83 | let count = 0 84 | hotRequire.accept([nps.join(base, 'a.js')], function(module, path) { 85 | count++ 86 | try { 87 | assert.equal(count, 1) 88 | assert.equal(module.exports, 1) 89 | assert.equal(module.id, path) 90 | assert.equal(require.cache[path], undefined) 91 | 92 | // await delay(0) 93 | console.log(fs.readFileSync(path).toString()) 94 | assert.equal(require(path), 2) 95 | } catch (e) { 96 | done(e) 97 | } 98 | }) 99 | 100 | hotRequire.accept(nps.join(base, 'index.js'), function(module, path) { 101 | count++ 102 | 103 | try { 104 | assert.equal(count, 2) 105 | assert.equal(module.exports, 3) 106 | assert.equal(require.cache[path], undefined) 107 | 108 | // await delay(0) 109 | assert.equal(require(path), 4) 110 | } catch (e) { 111 | done(e) 112 | } 113 | done() 114 | }) 115 | 116 | delay().then(() => { 117 | deepCaseWrite(base, 'module.exports = 2;', null, null) 118 | }) 119 | }) 120 | 121 | // expect(count).toBe(1) 122 | }) 123 | 124 | it('should works in dynamic dependencies', function() { 125 | clearRequire() 126 | hotRequire && hotRequire.close() 127 | hotRequire = makeHotRequire(__dirname) 128 | let base = nps.join(__dirname, './fixture/deep') 129 | deepCaseWrite(base) 130 | 131 | require(nps.join(base, 'index.js')) 132 | 133 | return delay().then(() => { 134 | let count = 0 135 | hotRequire.accept([nps.join(base, 'index.js')], function(module, path) { 136 | count++ 137 | 138 | console.log(path, hotRequire.dependent) 139 | console.log(hotRequire.dependence) 140 | }) 141 | 142 | return delay(2000) 143 | .then(() => { 144 | deepCaseWrite(base, "module.exports = require('.');", null, null) 145 | return delay(2000) 146 | }) 147 | .then(() => { 148 | assert.equal(count, 1) 149 | deepCaseWrite(base, "module.exports = require('.')", null, null) 150 | return delay().then(() => { 151 | assert.equal(count, 2) 152 | }) 153 | }) 154 | }) 155 | }) 156 | 157 | // root 158 | // / | \ 159 | // A / B 160 | // \ / 161 | // C 162 | it('should complex', function() { 163 | clearRequire() 164 | 165 | let complexPath = nps.join(__dirname, './fixture/complex') 166 | let rootPath = nps.join(complexPath, 'root.js') 167 | let aPath = nps.join(complexPath, 'a.js') 168 | let bPath = nps.join(complexPath, 'b.js') 169 | let cPath = nps.join(complexPath, 'c.js') 170 | 171 | !fs.existsSync(complexPath) && fs.mkdirSync(complexPath) 172 | fs.writeFileSync( 173 | rootPath, 174 | `module.exports = require('./a') + require('./b') + require('./c')` 175 | ) // 9 176 | fs.writeFileSync(aPath, `module.exports = 1 + require('./c')`) // 4 177 | fs.writeFileSync(bPath, `module.exports = 2`) 178 | fs.writeFileSync(cPath, `module.exports = 3`) 179 | 180 | const results = [] 181 | return delay().then(() => { 182 | hotRequire && hotRequire.close() 183 | hotRequire = makeHotRequire(complexPath) 184 | 185 | hotRequire.accept(['./root'], (m, p) => { 186 | console.log(require(p)) 187 | results.push(require(p)) 188 | }) 189 | 190 | fs.writeFileSync(cPath, `module.exports = 2`) 191 | 192 | return delay().then(() => { 193 | fs.writeFileSync(cPath, `module.exports = 10`) 194 | 195 | return delay().then(() => { 196 | // assert.equal(JSON.stringify(results), JSON.stringify([7, 7, 15, 23])) 197 | assert.equal(JSON.stringify(results), JSON.stringify([23, 23])) 198 | }) 199 | }) 200 | }) 201 | }) 202 | 203 | it('should callable hotRequire', function() { 204 | clearRequire() 205 | 206 | let path = nps.join(__dirname, './fixture/hot-middleware') 207 | hotRequire && hotRequire.close() 208 | hotRequire = makeHotRequire(path) 209 | 210 | deepCaseWrite( 211 | path, 212 | 'module.exports = 4', 213 | null, 214 | `module.exports = (v) => { 215 | return (a = 0) => { 216 | return require('./a') + v + a 217 | } 218 | } 219 | ` 220 | ) 221 | 222 | const get = hotRequire('./') 223 | assert.equal(get()(2)(), 6) 224 | 225 | return delay() 226 | .then(() => { 227 | deepCaseWrite(path, 'module.exports = 2', null, null) 228 | }) 229 | .then(() => { 230 | return delay().then(() => { 231 | assert.equal(get()(2)(1), 5) 232 | }) 233 | }) 234 | .then(() => { 235 | return delay(2000).then(() => { 236 | // Remove listener 237 | get.remove() 238 | deepCaseWrite(path, 'module.exports = 3', null, null) 239 | }) 240 | }) 241 | .then(() => { 242 | return delay(2000).then(() => { 243 | assert.equal(get()(2)(1), 5) 244 | }) 245 | }) 246 | }) 247 | 248 | it('should circle works', function() { 249 | clearRequire() 250 | let path = nps.join(__dirname, './fixture/circle') 251 | hotRequire && hotRequire.close() 252 | hotRequire = makeHotRequire(path) 253 | 254 | deepCaseWrite( 255 | path, 256 | 'module.exports = ["a", require("./b")]', 257 | 'module.exports = ["b", require("./")]', 258 | `module.exports = ["index", require("./a")]` 259 | ) 260 | 261 | // hotRequire.accept(['./', './a', './b'], (m, path) => { 262 | // console.log('path', path) 263 | // }) 264 | 265 | let exp 266 | hotRequire.accept('./', (m, path) => { 267 | exp = require(path) 268 | }) 269 | assert.equal( 270 | JSON.stringify(require(hotRequire.resolve('.'))), 271 | JSON.stringify(['index', ['a', ['b', {}]]]) 272 | ) 273 | 274 | return delay() 275 | .then(() => { 276 | deepCaseWrite( 277 | path, 278 | 'module.exports = ["a-2", require("./b")]', 279 | null, 280 | null 281 | ) 282 | }) 283 | .then(() => { 284 | return delay().then(() => { 285 | assert.equal( 286 | JSON.stringify(exp), 287 | JSON.stringify(['index', ['a-2', ['b', {}]]]) 288 | ) 289 | }) 290 | }) 291 | .then(() => { 292 | return delay().then(() => { 293 | deepCaseWrite( 294 | path, 295 | null, 296 | 'module.exports = ["b-2", require("./")]', 297 | null 298 | ) 299 | }) 300 | }) 301 | .then(() => { 302 | return delay().then(() => { 303 | assert.equal( 304 | JSON.stringify(exp), 305 | JSON.stringify(['index', ['a-2', ['b-2', {}]]]) 306 | ) 307 | }) 308 | }) 309 | .then(() => { 310 | return delay().then(() => { 311 | deepCaseWrite( 312 | path, 313 | null, 314 | null, 315 | `module.exports = ["index-2", require("./a")]` 316 | ) 317 | }) 318 | }) 319 | .then(() => { 320 | return delay().then(() => { 321 | assert.equal( 322 | JSON.stringify(exp), 323 | JSON.stringify(['index-2', ['a-2', ['b-2', {}]]]) 324 | ) 325 | }) 326 | }) 327 | }) 328 | 329 | // afterAll(() => { 330 | // deepCaseWrite() 331 | // }) 332 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file setup 3 | * @author Cuttle Cong 4 | * @date 2018/5/16 5 | * @description 6 | */ 7 | 8 | const nps = require('path') 9 | const minimist = require('minimist') 10 | const tasks = [] 11 | const afterTasks = [] 12 | 13 | function it(message, callback) { 14 | tasks.push({ message, callback }) 15 | } 16 | 17 | function afterAll(callback) { 18 | afterTasks.push({ message: 'afterAll', callback }) 19 | } 20 | 21 | function runTask(callback, done) { 22 | let rlt = callback(done) 23 | return Promise.resolve(rlt).then(() => done(), done) 24 | // _spy.t = setTimeout(_spy, 4000) 25 | } 26 | 27 | function run(tasks, opts) { 28 | let task = tasks.shift() 29 | if (!task) { 30 | return Promise.resolve('ok') 31 | } 32 | 33 | return new Promise((resolve, reject) => { 34 | let msgs = opts.message 35 | if (opts.message && !Array.isArray(opts.message)) { 36 | msgs = [opts.message] 37 | } 38 | if (msgs && !msgs.includes(task.message)) { 39 | return resolve(run(tasks, opts)) 40 | } 41 | 42 | const msg = task.message 43 | console.log('\nrunning:', msg) 44 | let hasRun = false 45 | return runTask(task.callback, function done(err) { 46 | if (hasRun) return resolve() 47 | hasRun = true 48 | console.log('done:', msg) 49 | if (err) { 50 | reject(err) 51 | } 52 | resolve(run(tasks, opts)) 53 | }) 54 | }) 55 | } 56 | 57 | global.it = it 58 | global.afterAll = afterAll 59 | global.assert = require('assert') 60 | 61 | const arg = minimist(process.argv.slice(2), { 62 | alias: { 63 | message: 'm' 64 | } 65 | }) 66 | arg._.forEach(testPath => { 67 | require(nps.resolve(testPath)) 68 | }) 69 | 70 | /** 71 | * testfiles... [-m --message "exact message"] 72 | */ 73 | run(tasks.concat(afterTasks), arg) 74 | .catch(e => { 75 | console.error(e) 76 | process.exit(1) 77 | }) 78 | .then(() => process.exit(0)) 79 | --------------------------------------------------------------------------------