Skip to content

Commit 1e0dcd1

Browse files
MoLowrichardlau
authored andcommitted
cli: add --watch
PR-URL: #44366 Backport-PR-URL: #44976 Fixes: #40429 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 6f541d9 commit 1e0dcd1

29 files changed

+953
-42
lines changed

doc/api/cli.md

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

1430+
### `--watch`
1431+
1432+
<!-- YAML
1433+
added: REPLACEME
1434+
-->
1435+
1436+
> Stability: 1 - Experimental
1437+
1438+
Starts Node.js in watch mode.
1439+
When in watch mode, changes in the watched files cause the Node.js process to
1440+
restart.
1441+
By default, watch mode will watch the entry point
1442+
and any required or imported module.
1443+
Use `--watch-path` to specify what paths to watch.
1444+
1445+
This flag cannot be combined with
1446+
`--check`, `--eval`, `--interactive`, or the REPL.
1447+
1448+
```console
1449+
$ node --watch index.js
1450+
```
1451+
1452+
### `--watch-path`
1453+
1454+
<!-- YAML
1455+
added: REPLACEME
1456+
-->
1457+
1458+
> Stability: 1 - Experimental
1459+
1460+
Starts Node.js in watch mode and specifies what paths to watch.
1461+
When in watch mode, changes in the watched paths cause the Node.js process to
1462+
restart.
1463+
This will turn off watching of required or imported modules, even when used in
1464+
combination with `--watch`.
1465+
1466+
This flag cannot be combined with
1467+
`--check`, `--eval`, `--interactive`, or the REPL.
1468+
1469+
```console
1470+
$ node --watch-path=./src --watch-path=./tests index.js
1471+
```
1472+
1473+
This option is only supported on macOS and Windows.
1474+
An `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` exception will be thrown
1475+
when the option is used on a platform that does not support it.
1476+
14301477
### `--zero-fill-buffers`
14311478

14321479
<!-- YAML
@@ -1724,6 +1771,8 @@ Node.js options that are allowed are:
17241771
* `--use-largepages`
17251772
* `--use-openssl-ca`
17261773
* `--v8-pool-size`
1774+
* `--watch-path`
1775+
* `--watch`
17271776
* `--zero-fill-buffers`
17281777

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

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
} = require('internal/bootstrap/pre_execution');
14+
const { triggerUncaughtException } = internalBinding('errors');
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 = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path));
34+
const kCommand = ArrayPrototypeSlice(process.argv, 1);
35+
const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
36+
const args = ArrayPrototypeFilter(process.execArgv, (arg, i, arr) =>
37+
arg !== '--watch-path' && arr[i - 1] !== '--watch-path' && arg !== '--watch');
38+
ArrayPrototypePushApply(args, kCommand);
39+
40+
const watcher = new FilesWatcher({ throttle: 500, mode: kShouldFilterModules ? 'filter' : 'all' });
41+
ArrayPrototypeForEach(kWatchedPaths, (p) => watcher.watchPath(p));
42+
43+
let graceTimer;
44+
let child;
45+
let exited;
46+
47+
function start() {
48+
exited = false;
49+
const stdio = kShouldFilterModules ? ['inherit', 'inherit', 'inherit', 'ipc'] : undefined;
50+
child = spawn(process.execPath, args, { stdio, env: { ...process.env, WATCH_REPORT_DEPENDENCIES: '1' } });
51+
watcher.watchChildProcessModules(child);
52+
child.once('exit', (code) => {
53+
exited = true;
54+
if (code === 0) {
55+
process.stdout.write(`${blue}Completed running ${kCommandStr}${white}\n`);
56+
} else {
57+
process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`);
58+
}
59+
});
60+
}
61+
62+
async function killAndWait(signal = kKillSignal, force = false) {
63+
child?.removeAllListeners();
64+
if (!child) {
65+
return;
66+
}
67+
if ((child.killed || exited) && !force) {
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+
108+
try {
109+
start();
110+
111+
// eslint-disable-next-line no-unused-vars
112+
for await (const _ of on(watcher, 'changed')) {
113+
await restart();
114+
}
115+
} catch (error) {
116+
triggerUncaughtException(error, true /* fromPromise */);
117+
}
118+
})();
119+
120+
// Exiting gracefully to avoid stdout/stderr getting written after
121+
// parent process is killed.
122+
// this is fairly safe since user code cannot run in this process
123+
function signalHandler(signal) {
124+
return async () => {
125+
watcher.clear();
126+
const exitCode = await killAndWait(signal, true);
127+
process.exit(exitCode ?? 0);
128+
};
129+
}
130+
process.on('SIGTERM', signalHandler('SIGTERM'));
131+
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)