Skip to content

Commit cfad875

Browse files
committed
cli: add --watch
1 parent a5d27f4 commit cfad875

22 files changed

+655
-34
lines changed

doc/api/cli.md

+28
Original file line numberDiff line numberDiff line change
@@ -1577,6 +1577,32 @@ on the number of online processors.
15771577
If the value provided is larger than V8's maximum, then the largest value
15781578
will be chosen.
15791579

1580+
### `--watch`
1581+
1582+
<!-- YAML
1583+
added: REPLACEME
1584+
-->
1585+
1586+
> Stability: 1 - Experimental
1587+
1588+
Starts the Node.js in watch mode.
1589+
When in watch mode, changes in the watched files cause node to restart.
1590+
By default, watch mode will watch the entry point
1591+
and any required module. use `--watch-path` to specify what paths to watch
1592+
1593+
This flag cannot be combined with
1594+
`--check`, `--eval`, `--interactive`, or the REPL.
1595+
1596+
### `--watch-path`
1597+
1598+
<!-- YAML
1599+
added: REPLACEME
1600+
-->
1601+
1602+
> Stability: 1 - Experimental
1603+
1604+
Specify what paths to watch in watch mode.
1605+
15801606
### `--zero-fill-buffers`
15811607

15821608
<!-- YAML
@@ -1880,6 +1906,8 @@ Node.js options that are allowed are:
18801906
* `--use-largepages`
18811907
* `--use-openssl-ca`
18821908
* `--v8-pool-size`
1909+
* `--watch-path`
1910+
* `--watch`
18831911
* `--zero-fill-buffers`
18841912

18851913
<!-- node-options-node end -->

lib/internal/assert/assertion_error.js

+17-32
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,12 @@ const { inspect } = require('internal/util/inspect');
2121
const {
2222
removeColors,
2323
} = require('internal/util');
24+
const colors = require('internal/util/colors');
2425
const {
2526
validateObject,
2627
} = require('internal/validators');
2728
const { isErrorStackTraceLimitWritable } = require('internal/errors');
2829

29-
let blue = '';
30-
let green = '';
31-
let red = '';
32-
let white = '';
3330

3431
const kReadableOperator = {
3532
deepStrictEqual: 'Expected values to be strictly deep-equal:',
@@ -169,7 +166,7 @@ function createErrDiff(actual, expected, operator) {
169166
// Only remove lines in case it makes sense to collapse those.
170167
// TODO: Accept env to always show the full error.
171168
if (actualLines.length > 50) {
172-
actualLines[46] = `${blue}...${white}`;
169+
actualLines[46] = `${colors.blue}...${colors.white}`;
173170
while (actualLines.length > 47) {
174171
ArrayPrototypePop(actualLines);
175172
}
@@ -182,7 +179,7 @@ function createErrDiff(actual, expected, operator) {
182179
// There were at least five identical lines at the end. Mark a couple of
183180
// skipped.
184181
if (i >= 5) {
185-
end = `\n${blue}...${white}${end}`;
182+
end = `\n${colors.blue}...${colors.white}${end}`;
186183
skipped = true;
187184
}
188185
if (other !== '') {
@@ -193,15 +190,15 @@ function createErrDiff(actual, expected, operator) {
193190
let printedLines = 0;
194191
let identical = 0;
195192
const msg = kReadableOperator[operator] +
196-
`\n${green}+ actual${white} ${red}- expected${white}`;
197-
const skippedMsg = ` ${blue}...${white} Lines skipped`;
193+
`\n${colors.green}+ actual${colors.white} ${colors.red}- expected${colors.white}`;
194+
const skippedMsg = ` ${colors.blue}...${colors.white} Lines skipped`;
198195

199196
let lines = actualLines;
200-
let plusMinus = `${green}+${white}`;
197+
let plusMinus = `${colors.green}+${colors.white}`;
201198
let maxLength = expectedLines.length;
202199
if (actualLines.length < maxLines) {
203200
lines = expectedLines;
204-
plusMinus = `${red}-${white}`;
201+
plusMinus = `${colors.red}-${colors.white}`;
205202
maxLength = actualLines.length;
206203
}
207204

@@ -216,7 +213,7 @@ function createErrDiff(actual, expected, operator) {
216213
res += `\n ${lines[i - 3]}`;
217214
printedLines++;
218215
} else {
219-
res += `\n${blue}...${white}`;
216+
res += `\n${colors.blue}...${colors.white}`;
220217
skipped = true;
221218
}
222219
}
@@ -272,7 +269,7 @@ function createErrDiff(actual, expected, operator) {
272269
res += `\n ${actualLines[i - 3]}`;
273270
printedLines++;
274271
} else {
275-
res += `\n${blue}...${white}`;
272+
res += `\n${colors.blue}...${colors.white}`;
276273
skipped = true;
277274
}
278275
}
@@ -286,8 +283,8 @@ function createErrDiff(actual, expected, operator) {
286283
identical = 0;
287284
// Add the actual line to the result and cache the expected diverging
288285
// line so consecutive diverging lines show up as +++--- and not +-+-+-.
289-
res += `\n${green}+${white} ${actualLine}`;
290-
other += `\n${red}-${white} ${expectedLine}`;
286+
res += `\n${colors.green}+${colors.white} ${actualLine}`;
287+
other += `\n${colors.red}-${colors.white} ${expectedLine}`;
291288
printedLines += 2;
292289
// Lines are identical
293290
} else {
@@ -306,8 +303,8 @@ function createErrDiff(actual, expected, operator) {
306303
}
307304
// Inspected object to big (Show ~50 rows max)
308305
if (printedLines > 50 && i < maxLines - 2) {
309-
return `${msg}${skippedMsg}\n${res}\n${blue}...${white}${other}\n` +
310-
`${blue}...${white}`;
306+
return `${msg}${skippedMsg}\n${res}\n${colors.blue}...${colors.white}${other}\n` +
307+
`${colors.blue}...${colors.white}`;
311308
}
312309
}
313310

@@ -347,21 +344,9 @@ class AssertionError extends Error {
347344
if (message != null) {
348345
super(String(message));
349346
} else {
350-
if (process.stderr.isTTY) {
351-
// Reset on each call to make sure we handle dynamically set environment
352-
// variables correct.
353-
if (process.stderr.hasColors()) {
354-
blue = '\u001b[34m';
355-
green = '\u001b[32m';
356-
white = '\u001b[39m';
357-
red = '\u001b[31m';
358-
} else {
359-
blue = '';
360-
green = '';
361-
white = '';
362-
red = '';
363-
}
364-
}
347+
// Reset colors on each call to make sure we handle dynamically set environment
348+
// variables correct.
349+
colors.refresh();
365350
// Prevent the error stack from being visible by duplicating the error
366351
// in a very close way to the original in case both sides are actually
367352
// instances of Error.
@@ -393,7 +378,7 @@ class AssertionError extends Error {
393378
// Only remove lines in case it makes sense to collapse those.
394379
// TODO: Accept env to always show the full error.
395380
if (res.length > 50) {
396-
res[46] = `${blue}...${white}`;
381+
res[46] = `${colors.blue}...${colors.white}`;
397382
while (res.length > 47) {
398383
ArrayPrototypePop(res);
399384
}

lib/internal/main/watch_mode.js

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
'use strict';
2+
const {
3+
ArrayPrototypeFilter,
4+
ArrayPrototypeJoin,
5+
ArrayPrototypeMap,
6+
ArrayPrototypePush,
7+
ArrayPrototypePushApply,
8+
ArrayPrototypeSlice,
9+
} = primordials;
10+
11+
const {
12+
prepareMainThreadExecution,
13+
markBootstrapComplete
14+
} = require('internal/process/pre_execution');
15+
const { getOptionValue } = require('internal/options');
16+
const { emitExperimentalWarning } = require('internal/util');
17+
const { FilesWatcher } = require('internal/watch_mode/files_watcher');
18+
const { green, blue, red, white, clear } = require('internal/util/colors');
19+
20+
const { spawn } = require('child_process');
21+
const { inspect } = require('util');
22+
const { setTimeout, clearTimeout } = require('timers');
23+
const { resolve } = require('path');
24+
const { once, on } = require('events');
25+
26+
27+
prepareMainThreadExecution(false, false);
28+
markBootstrapComplete();
29+
30+
// TODO(MoLow): Make kill signal configurable
31+
const kKillSignal = 'SIGTERM';
32+
const kShouldFilterModules = getOptionValue('--watch-path').length === 0;
33+
const kWatchedPaths = getOptionValue('--watch-path').length ?
34+
ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path)) :
35+
[];
36+
const kCommand = ArrayPrototypeSlice(process.argv, 1);
37+
const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
38+
const args = ArrayPrototypeFilter(process.execArgv, (x, i, arr) =>
39+
x !== '--watch-path' && arr[i - 1] !== '--watch-path' && x !== '--watch');
40+
ArrayPrototypePush(args, '--watch-report-ipc');
41+
ArrayPrototypePushApply(args, kCommand);
42+
43+
const watcher = new FilesWatcher({ throttle: 500, mode: kShouldFilterModules ? 'filter' : 'all' });
44+
kWatchedPaths.forEach((p) => watcher.watchPath(p));
45+
let graceTimer;
46+
let child;
47+
let exited;
48+
49+
function start() {
50+
// Spawning in detached mode so node can control when signals are forwarded
51+
exited = false;
52+
const stdio = kShouldFilterModules ? ['inherit', 'inherit', 'inherit', 'ipc'] : undefined;
53+
child = spawn(process.execPath, args, { stdio, detached: true });
54+
watcher.watchChildProcessModules(child);
55+
child.once('exit', (code) => {
56+
exited = true;
57+
if (code === 0) {
58+
process.stdout.write(`${blue}Completed running ${kCommandStr}${white}\n`);
59+
} else {
60+
process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`);
61+
}
62+
});
63+
}
64+
65+
async function killAndWait(signal = kKillSignal) {
66+
child?.removeAllListeners();
67+
if (!child || child.killed || exited) {
68+
return;
69+
}
70+
const onExit = once(child, 'exit');
71+
child.kill(signal);
72+
const { 0: exitCode } = await onExit;
73+
return exitCode;
74+
}
75+
76+
function reportGracefulTermination() {
77+
// Log if process takes more than 500ms to stop
78+
let reported = false;
79+
clearTimeout(graceTimer);
80+
graceTimer = setTimeout(() => {
81+
reported = true;
82+
process.stdout.write(`${blue}Waiting for graceful termination...${white}\n`);
83+
}, 500).unref();
84+
return () => {
85+
clearTimeout(graceTimer);
86+
if (reported) {
87+
process.stdout.write(`${clear}${green}Gracefully restarted ${kCommandStr}${white}\n`);
88+
}
89+
};
90+
}
91+
92+
async function stop() {
93+
watcher.clearFileFilters();
94+
const clearGraceReport = reportGracefulTermination();
95+
await killAndWait();
96+
clearGraceReport();
97+
}
98+
99+
async function restart() {
100+
process.stdout.write(`${clear}${green}Restarting ${kCommandStr}${white}\n`);
101+
await stop();
102+
start();
103+
}
104+
105+
(async () => {
106+
emitExperimentalWarning('Watch mode');
107+
start();
108+
109+
// eslint-disable-next-line no-unused-vars
110+
for await (const _ of on(watcher, 'changed')) {
111+
await restart();
112+
}
113+
})();
114+
115+
// Exiting gracefully to avoid stdout/stderr getting written after
116+
// parent process is killed.
117+
// this is fairly safe since user code cannot run in this process
118+
function signalHandler(signal) {
119+
return async () => {
120+
watcher.clear();
121+
process.exit(await killAndWait(signal));
122+
};
123+
}
124+
process.on('SIGTERM', signalHandler('SIGTERM'));
125+
process.on('SIGINT', signalHandler('SIGINT'));

lib/internal/modules/cjs/loader.js

+10
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ const {
100100
const { getOptionValue } = require('internal/options');
101101
const preserveSymlinks = getOptionValue('--preserve-symlinks');
102102
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
103+
const shouldReportRequiredModules = getOptionValue('--watch-report-ipc');
103104
// Do not eagerly grab .manifest, it may be in TDZ
104105
const policy = getOptionValue('--experimental-policy') ?
105106
require('internal/process/policy') :
@@ -168,6 +169,12 @@ function updateChildren(parent, child, scan) {
168169
ArrayPrototypePush(children, child);
169170
}
170171

172+
function reportModuleToWatchMode(filename) {
173+
if (shouldReportRequiredModules && process.send) {
174+
process.send({ 'watch:require': filename });
175+
}
176+
}
177+
171178
const moduleParentCache = new SafeWeakMap();
172179
function Module(id = '', parent) {
173180
this.id = id;
@@ -776,6 +783,7 @@ Module._load = function(request, parent, isMain) {
776783
// cache key names.
777784
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
778785
const filename = relativeResolveCache[relResolveCacheIdentifier];
786+
reportModuleToWatchMode(filename);
779787
if (filename !== undefined) {
780788
const cachedModule = Module._cache[filename];
781789
if (cachedModule !== undefined) {
@@ -828,6 +836,8 @@ Module._load = function(request, parent, isMain) {
828836
module.id = '.';
829837
}
830838

839+
reportModuleToWatchMode(filename);
840+
831841
Module._cache[filename] = module;
832842
if (parent !== undefined) {
833843
relativeResolveCache[relResolveCacheIdentifier] = filename;

lib/internal/modules/esm/loader.js

+4
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ class ESMLoader {
475475
getOptionValue('--inspect-brk')
476476
);
477477

478+
if (getOptionValue('--watch-report-ipc') && process.send) {
479+
process.send({ 'watch:import': url });
480+
}
481+
478482
const job = new ModuleJob(
479483
this,
480484
url,

lib/internal/util/colors.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
3+
module.exports = {
4+
blue: '',
5+
green: '',
6+
white: '',
7+
red: '',
8+
clear: '',
9+
refresh() {
10+
if (process.stderr.isTTY && process.stderr.hasColors()) {
11+
module.exports.blue = '\u001b[34m';
12+
module.exports.green = '\u001b[32m';
13+
module.exports.white = '\u001b[39m';
14+
module.exports.red = '\u001b[31m';
15+
module.exports.clear = '\u001bc';
16+
}
17+
}
18+
};
19+
20+
module.exports.refresh();

0 commit comments

Comments
 (0)