UNPKG

8.03 kBJavaScriptView Raw
1var fork = require('child_process').fork
2var chokidar = require('chokidar')
3var ipc = require('./ipc')
4var resolveMain = require('./resolveMain')
5var compiler = require('./compiler')
6var fs = require('fs')
7var tsNodeVersion = require('ts-node').VERSION
8var tsVersion = require('typescript').version
9var kill = require('tree-kill')
10var readline = require('readline')
11
12module.exports = function (script, scriptArgs, nodeArgs, opts) {
13 if (typeof script !== 'string' || script.length === 0) {
14 throw new TypeError('`script` must be a string')
15 }
16
17 if (!Array.isArray(scriptArgs)) {
18 throw new TypeError('`scriptArgs` must be an array')
19 }
20
21 if (!Array.isArray(nodeArgs)) {
22 throw new TypeError('`nodeArgs` must be an array')
23 }
24
25 // The child_process
26 var child
27
28 var wrapper = resolveMain(__dirname + '/wrap.js')
29 var main = resolveMain(script)
30 var cfg = require('./cfg')(main, opts)
31 var log = require('./log')(cfg)
32 var notify = require('./notify')(cfg, log)
33 opts.log = log
34 compiler.init(opts)
35
36 compiler.notify = notify
37 compiler.stop = stop
38 // Run ./dedupe.js as preload script
39 if (cfg.dedupe) process.env.NODE_DEV_PRELOAD = __dirname + '/dedupe'
40
41 // var watcher = filewatcher({
42 // forcePolling: opts.poll,
43 // interval: parseInt(opts.interval),
44 // debounce: parseInt(opts.debounce),
45 // recursive: process.platform !== 'linux'
46 // })
47
48 function initWatcher() {
49 var watcher = chokidar.watch([], {
50 usePolling: opts.poll,
51 interval: parseInt(opts.interval) || undefined,
52 })
53 watcher.on('change', restart)
54
55 watcher.on('fallback', function (limit) {
56 log.warn(
57 'node-dev ran out of file handles after watching %s files.',
58 limit
59 )
60 log.warn('Falling back to polling which uses more CPU.')
61 log.info('Run ulimit -n 10000 to increase the file descriptor limit.')
62 if (cfg.deps) log.info('... or add `--no-deps` to use less file handles.')
63 })
64 return watcher
65 }
66 var watcher = initWatcher()
67
68 var starting = false
69
70 // Read for "rs" from command line
71 if (opts.rs !== false) {
72 const rl = readline.createInterface({
73 input: process.stdin,
74 output: process.stdout,
75 terminal: false,
76 })
77 rl.on('line', function (line) {
78 if (line.trim() === 'rs') {
79 restart('', true)
80 }
81 })
82 }
83
84 /**
85 * Run the wrapped script.
86 */
87 var compileReqWatcher
88 function start() {
89 console.log(
90 'Using ts-node version',
91 tsNodeVersion + ', typescript version',
92 tsVersion
93 )
94 for (let watched of (opts.watch || '').split(',')) {
95 if (watched) watcher.add(watched)
96 }
97 var cmd = nodeArgs.concat(wrapper, script, scriptArgs)
98 var childHookPath = compiler.getChildHookPath()
99 cmd = (opts.priorNodeArgs || []).concat(['-r', childHookPath]).concat(cmd)
100 log.debug('Starting child process %s', cmd.join(' '))
101 child = fork(cmd[0], cmd.slice(1), {
102 cwd: process.cwd(),
103 env: process.env,
104 })
105 starting = false
106 //var compileReqWatcher = filewatcher({ forcePolling: opts.poll })
107 if (compileReqWatcher) {
108 compileReqWatcher.close()
109 }
110 compileReqWatcher = chokidar.watch([], {
111 usePolling: opts.poll,
112 interval: parseInt(opts.interval) || undefined,
113 })
114 var currentCompilePath
115 fs.writeFileSync(compiler.getCompileReqFilePath(), '')
116 compileReqWatcher.add(compiler.getCompileReqFilePath())
117 compileReqWatcher.on('change', function (file) {
118 fs.readFile(file, 'utf-8', function (err, data) {
119 if (err) {
120 log.error('Error reading compile request file', err)
121 return
122 }
123 var split = data.split('\n')
124 var compile = split[0]
125 var compiledPath = split[1]
126 if (currentCompilePath == compiledPath) return
127 currentCompilePath = compiledPath
128 // console.log('compileReqWatcher file change', compile);
129 if (compiledPath) {
130 compiler.compile({
131 compile: compile,
132 compiledPath: compiledPath,
133 })
134 }
135 })
136 })
137 child.on('message', function (message) {
138 if (!message.compiledPath || currentCompilePath === message.compiledPath)
139 return
140 currentCompilePath = message.compiledPath
141 compiler.compile(message)
142 })
143
144 child.on('exit', function (code) {
145 log.debug('Child exited with code %s', code)
146 if (!child) return
147 if (!child.respawn) process.exit(code)
148 child = undefined
149 })
150
151 if (cfg.respawn) {
152 child.respawn = true
153 }
154
155 if (compiler.tsConfigPath) {
156 watcher.add(compiler.tsConfigPath)
157 }
158
159 // Listen for `required` messages and watch the required file.
160 ipc.on(child, 'required', function (m) {
161 var isIgnored =
162 cfg.ignore.some(isPrefixOf(m.required)) ||
163 cfg.ignore.some(isRegExpMatch(m.required))
164
165 if (!isIgnored && (cfg.deps === -1 || getLevel(m.required) <= cfg.deps)) {
166 log.debug(m.required, 'added to watcher')
167 watcher.add(m.required)
168 }
169 })
170
171 // Upon errors, display a notification and tell the child to exit.
172 ipc.on(child, 'error', function (m) {
173 log.debug('Child error')
174 notify(m.error, m.message, 'error')
175 stop(m.willTerminate)
176 })
177 compiler.writeReadyFile()
178 }
179 const killChild = () => {
180 if (!child) return
181 log.debug('Sending SIGTERM kill to child pid', child.pid)
182 if (opts['tree-kill']) {
183 log.debug('Using tree-kill')
184 kill(child.pid)
185 } else {
186 child.kill('SIGTERM')
187 }
188 }
189 function stop(willTerminate) {
190 if (!child || child.stopping) {
191 return
192 }
193 child.stopping = true
194 child.respawn = true
195 if (child.connected === undefined || child.connected === true) {
196 log.debug('Disconnecting from child')
197 child.disconnect()
198 //if (!willTerminate) {
199 killChild()
200 //}
201 }
202 }
203
204 function restart(file, isManualRestart) {
205 if (file === compiler.tsConfigPath) {
206 notify('Reinitializing TS compilation')
207 compiler.init(opts)
208 }
209 compiler.clearErrorCompile()
210 /* eslint-disable no-octal-escape */
211 if (cfg.clear) process.stdout.write('\033[2J\033[H')
212 if (isManualRestart === true) {
213 notify('Restarting', 'manual restart from user')
214 } else {
215 notify('Restarting', file + ' has been modified')
216 }
217 compiler.compileChanged(file)
218 if (starting) {
219 log.debug('Already starting')
220 return
221 }
222 log.debug('Removing all watchers from files')
223 //watcher.removeAll()ya
224
225 watcher.close()
226 watcher = initWatcher()
227 starting = true
228 if (child) {
229 log.debug('Child is still running, restart upon exit')
230 child.on('exit', start)
231 stop()
232 } else {
233 log.debug('Child is already stopped, probably due to a previous error')
234 start()
235 }
236 }
237 compiler.restart = restart
238
239 // Relay SIGTERM
240 process.on('SIGTERM', function () {
241 log.debug('Process got SIGTERM')
242 killChild()
243 if (opts['restart-terminated']) {
244 var timeout = opts['restart-terminated'] || 0
245 log.info('Restarting terminated in ' + timeout + ' seconds')
246 setTimeout(() => {
247 start()
248 }, timeout)
249 } else {
250 process.exit(0)
251 }
252 })
253
254 start()
255}
256
257/**
258 * Returns the nesting-level of the given module.
259 * Will return 0 for modules from the main package or linked modules,
260 * a positive integer otherwise.
261 */
262function getLevel(mod) {
263 var p = getPrefix(mod)
264 return p.split('node_modules').length - 1
265}
266
267/**
268 * Returns the path up to the last occurence of `node_modules` or an
269 * empty string if the path does not contain a node_modules dir.
270 */
271function getPrefix(mod) {
272 var n = 'node_modules'
273 var i = mod.lastIndexOf(n)
274 return ~i ? mod.slice(0, i + n.length) : ''
275}
276
277function isPrefixOf(value) {
278 return function (prefix) {
279 return value.indexOf(prefix) === 0
280 }
281}
282
283function isRegExpMatch(value) {
284 return function (regExp) {
285 return new RegExp(regExp).test(value)
286 }
287}