Skip to content

Commit 2f5f41c

Browse files
MoLowdanielleadams
authored andcommitted
cli: add --watch
PR-URL: #44366 Backport-PR-URL: #44815 Fixes: #40429 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 2e7a17d commit 2f5f41c

29 files changed

+956
-42
lines changed

doc/api/cli.md

+49
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,53 @@ on the number of online processors.
15291529
If the value provided is larger than V8's maximum, then the largest value
15301530
will be chosen.
15311531

1532+
### `--watch`
1533+
1534+
<!-- YAML
1535+
added: REPLACEME
1536+
-->
1537+
1538+
> Stability: 1 - Experimental
1539+
1540+
Starts Node.js in watch mode.
1541+
When in watch mode, changes in the watched files cause the Node.js process to
1542+
restart.
1543+
By default, watch mode will watch the entry point
1544+
and any required or imported module.
1545+
Use `--watch-path` to specify what paths to watch.
1546+
1547+
This flag cannot be combined with
1548+
`--check`, `--eval`, `--interactive`, or the REPL.
1549+
1550+
```console
1551+
$ node --watch index.js
1552+
```
1553+
1554+
### `--watch-path`
1555+
1556+
<!-- YAML
1557+
added: REPLACEME
1558+
-->
1559+
1560+
> Stability: 1 - Experimental
1561+
1562+
Starts Node.js in watch mode and specifies what paths to watch.
1563+
When in watch mode, changes in the watched paths cause the Node.js process to
1564+
restart.
1565+
This will turn off watching of required or imported modules, even when used in
1566+
combination with `--watch`.
1567+
1568+
This flag cannot be combined with
1569+
`--check`, `--eval`, `--interactive`, or the REPL.
1570+
1571+
```console
1572+
$ node --watch-path=./src --watch-path=./tests index.js
1573+
```
1574+
1575+
This option is only supported on macOS and Windows.
1576+
An `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` exception will be thrown
1577+
when the option is used on a platform that does not support it.
1578+
15321579
### `--zero-fill-buffers`
15331580

15341581
<!-- YAML
@@ -1829,6 +1876,8 @@ Node.js options that are allowed are:
18291876
* `--use-largepages`
18301877
* `--use-openssl-ca`
18311878
* `--v8-pool-size`
1879+
* `--watch-path`
1880+
* `--watch`
18321881
* `--zero-fill-buffers`
18331882

18341883
<!-- 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

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

lib/internal/modules/cjs/loader.js

+10
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const {
106106
const { getOptionValue } = require('internal/options');
107107
const preserveSymlinks = getOptionValue('--preserve-symlinks');
108108
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
109+
const shouldReportRequiredModules = process.env.WATCH_REPORT_DEPENDENCIES;
109110
// Do not eagerly grab .manifest, it may be in TDZ
110111
const policy = getOptionValue('--experimental-policy') ?
111112
require('internal/process/policy') :
@@ -186,6 +187,12 @@ function updateChildren(parent, child, scan) {
186187
ArrayPrototypePush(children, child);
187188
}
188189

190+
function reportModuleToWatchMode(filename) {
191+
if (shouldReportRequiredModules && process.send) {
192+
process.send({ 'watch:require': filename });
193+
}
194+
}
195+
189196
const moduleParentCache = new SafeWeakMap();
190197
function Module(id = '', parent) {
191198
this.id = id;
@@ -806,6 +813,7 @@ Module._load = function(request, parent, isMain) {
806813
// cache key names.
807814
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
808815
const filename = relativeResolveCache[relResolveCacheIdentifier];
816+
reportModuleToWatchMode(filename);
809817
if (filename !== undefined) {
810818
const cachedModule = Module._cache[filename];
811819
if (cachedModule !== undefined) {
@@ -858,6 +866,8 @@ Module._load = function(request, parent, isMain) {
858866
module.id = '.';
859867
}
860868

869+
reportModuleToWatchMode(filename);
870+
861871
Module._cache[filename] = module;
862872
if (parent !== undefined) {
863873
relativeResolveCache[relResolveCacheIdentifier] = filename;

lib/internal/modules/esm/loader.js

+4
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,10 @@ class ESMLoader {
473473
getOptionValue('--inspect-brk')
474474
);
475475

476+
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
477+
process.send({ 'watch:import': url });
478+
}
479+
476480
const job = new ModuleJob(
477481
this,
478482
url,

lib/internal/util/colors.js

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

0 commit comments

Comments
 (0)