Skip to content

Commit

Permalink
cli: add --watch
Browse files Browse the repository at this point in the history
PR-URL: nodejs#44366
Backport-PR-URL: nodejs#44815
Fixes: nodejs#40429
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
  • Loading branch information
MoLow committed Sep 29, 2022
1 parent 2e7a17d commit 0e2a359
Show file tree
Hide file tree
Showing 29 changed files with 956 additions and 42 deletions.
49 changes: 49 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,53 @@ on the number of online processors.
If the value provided is larger than V8's maximum, then the largest value
will be chosen.

### `--watch`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Starts Node.js in watch mode.
When in watch mode, changes in the watched files cause the Node.js process to
restart.
By default, watch mode will watch the entry point
and any required or imported module.
Use `--watch-path` to specify what paths to watch.

This flag cannot be combined with
`--check`, `--eval`, `--interactive`, or the REPL.

```console
$ node --watch index.js
```

### `--watch-path`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Starts Node.js in watch mode and specifies what paths to watch.
When in watch mode, changes in the watched paths cause the Node.js process to
restart.
This will turn off watching of required or imported modules, even when used in
combination with `--watch`.

This flag cannot be combined with
`--check`, `--eval`, `--interactive`, or the REPL.

```console
$ node --watch-path=./src --watch-path=./tests index.js
```

This option is only supported on macOS and Windows.
An `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` exception will be thrown
when the option is used on a platform that does not support it.

### `--zero-fill-buffers`

<!-- YAML
Expand Down Expand Up @@ -1829,6 +1876,8 @@ Node.js options that are allowed are:
* `--use-largepages`
* `--use-openssl-ca`
* `--v8-pool-size`
* `--watch-path`
* `--watch`
* `--zero-fill-buffers`

<!-- node-options-node end -->
Expand Down
49 changes: 17 additions & 32 deletions lib/internal/assert/assertion_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,12 @@ const { inspect } = require('internal/util/inspect');
const {
removeColors,
} = require('internal/util');
const colors = require('internal/util/colors');
const {
validateObject,
} = require('internal/validators');
const { isErrorStackTraceLimitWritable } = require('internal/errors');

let blue = '';
let green = '';
let red = '';
let white = '';

const kReadableOperator = {
deepStrictEqual: 'Expected values to be strictly deep-equal:',
Expand Down Expand Up @@ -169,7 +166,7 @@ function createErrDiff(actual, expected, operator) {
// Only remove lines in case it makes sense to collapse those.
// TODO: Accept env to always show the full error.
if (actualLines.length > 50) {
actualLines[46] = `${blue}...${white}`;
actualLines[46] = `${colors.blue}...${colors.white}`;
while (actualLines.length > 47) {
ArrayPrototypePop(actualLines);
}
Expand All @@ -182,7 +179,7 @@ function createErrDiff(actual, expected, operator) {
// There were at least five identical lines at the end. Mark a couple of
// skipped.
if (i >= 5) {
end = `\n${blue}...${white}${end}`;
end = `\n${colors.blue}...${colors.white}${end}`;
skipped = true;
}
if (other !== '') {
Expand All @@ -193,15 +190,15 @@ function createErrDiff(actual, expected, operator) {
let printedLines = 0;
let identical = 0;
const msg = kReadableOperator[operator] +
`\n${green}+ actual${white} ${red}- expected${white}`;
const skippedMsg = ` ${blue}...${white} Lines skipped`;
`\n${colors.green}+ actual${colors.white} ${colors.red}- expected${colors.white}`;
const skippedMsg = ` ${colors.blue}...${colors.white} Lines skipped`;

let lines = actualLines;
let plusMinus = `${green}+${white}`;
let plusMinus = `${colors.green}+${colors.white}`;
let maxLength = expectedLines.length;
if (actualLines.length < maxLines) {
lines = expectedLines;
plusMinus = `${red}-${white}`;
plusMinus = `${colors.red}-${colors.white}`;
maxLength = actualLines.length;
}

Expand All @@ -216,7 +213,7 @@ function createErrDiff(actual, expected, operator) {
res += `\n ${lines[i - 3]}`;
printedLines++;
} else {
res += `\n${blue}...${white}`;
res += `\n${colors.blue}...${colors.white}`;
skipped = true;
}
}
Expand Down Expand Up @@ -272,7 +269,7 @@ function createErrDiff(actual, expected, operator) {
res += `\n ${actualLines[i - 3]}`;
printedLines++;
} else {
res += `\n${blue}...${white}`;
res += `\n${colors.blue}...${colors.white}`;
skipped = true;
}
}
Expand All @@ -286,8 +283,8 @@ function createErrDiff(actual, expected, operator) {
identical = 0;
// Add the actual line to the result and cache the expected diverging
// line so consecutive diverging lines show up as +++--- and not +-+-+-.
res += `\n${green}+${white} ${actualLine}`;
other += `\n${red}-${white} ${expectedLine}`;
res += `\n${colors.green}+${colors.white} ${actualLine}`;
other += `\n${colors.red}-${colors.white} ${expectedLine}`;
printedLines += 2;
// Lines are identical
} else {
Expand All @@ -306,8 +303,8 @@ function createErrDiff(actual, expected, operator) {
}
// Inspected object to big (Show ~50 rows max)
if (printedLines > 50 && i < maxLines - 2) {
return `${msg}${skippedMsg}\n${res}\n${blue}...${white}${other}\n` +
`${blue}...${white}`;
return `${msg}${skippedMsg}\n${res}\n${colors.blue}...${colors.white}${other}\n` +
`${colors.blue}...${colors.white}`;
}
}

Expand Down Expand Up @@ -347,21 +344,9 @@ class AssertionError extends Error {
if (message != null) {
super(String(message));
} else {
if (process.stderr.isTTY) {
// Reset on each call to make sure we handle dynamically set environment
// variables correct.
if (process.stderr.hasColors()) {
blue = '\u001b[34m';
green = '\u001b[32m';
white = '\u001b[39m';
red = '\u001b[31m';
} else {
blue = '';
green = '';
white = '';
red = '';
}
}
// Reset colors on each call to make sure we handle dynamically set environment
// variables correct.
colors.refresh();
// Prevent the error stack from being visible by duplicating the error
// in a very close way to the original in case both sides are actually
// instances of Error.
Expand Down Expand Up @@ -393,7 +378,7 @@ class AssertionError extends Error {
// Only remove lines in case it makes sense to collapse those.
// TODO: Accept env to always show the full error.
if (res.length > 50) {
res[46] = `${blue}...${white}`;
res[46] = `${colors.blue}...${colors.white}`;
while (res.length > 47) {
ArrayPrototypePop(res);
}
Expand Down
132 changes: 132 additions & 0 deletions lib/internal/main/watch_mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use strict';
const {
ArrayPrototypeFilter,
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePushApply,
ArrayPrototypeSlice,
} = primordials;

const {
prepareMainThreadExecution,
markBootstrapComplete
} = require('internal/process/pre_execution');
const { triggerUncaughtException } = internalBinding('errors');
const { getOptionValue } = require('internal/options');
const { emitExperimentalWarning } = require('internal/util');
const { FilesWatcher } = require('internal/watch_mode/files_watcher');
const { green, blue, red, white, clear } = require('internal/util/colors');

const { spawn } = require('child_process');
const { inspect } = require('util');
const { setTimeout, clearTimeout } = require('timers');
const { resolve } = require('path');
const { once, on } = require('events');


prepareMainThreadExecution(false, false);
markBootstrapComplete();

// TODO(MoLow): Make kill signal configurable
const kKillSignal = 'SIGTERM';
const kShouldFilterModules = getOptionValue('--watch-path').length === 0;
const kWatchedPaths = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path));
const kCommand = ArrayPrototypeSlice(process.argv, 1);
const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
const args = ArrayPrototypeFilter(process.execArgv, (arg, i, arr) =>
arg !== '--watch-path' && arr[i - 1] !== '--watch-path' && arg !== '--watch');
ArrayPrototypePushApply(args, kCommand);

const watcher = new FilesWatcher({ throttle: 500, mode: kShouldFilterModules ? 'filter' : 'all' });
ArrayPrototypeForEach(kWatchedPaths, (p) => watcher.watchPath(p));

let graceTimer;
let child;
let exited;

function start() {
exited = false;
const stdio = kShouldFilterModules ? ['inherit', 'inherit', 'inherit', 'ipc'] : undefined;
child = spawn(process.execPath, args, { stdio, env: { ...process.env, WATCH_REPORT_DEPENDENCIES: '1' } });
watcher.watchChildProcessModules(child);
child.once('exit', (code) => {
exited = true;
if (code === 0) {
process.stdout.write(`${blue}Completed running ${kCommandStr}${white}\n`);
} else {
process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`);
}
});
}

async function killAndWait(signal = kKillSignal, force = false) {
child?.removeAllListeners();
if (!child) {
return;
}
if ((child.killed || exited) && !force) {
return;
}
const onExit = once(child, 'exit');
child.kill(signal);
const { 0: exitCode } = await onExit;
return exitCode;
}

function reportGracefulTermination() {
// Log if process takes more than 500ms to stop.
let reported = false;
clearTimeout(graceTimer);
graceTimer = setTimeout(() => {
reported = true;
process.stdout.write(`${blue}Waiting for graceful termination...${white}\n`);
}, 500).unref();
return () => {
clearTimeout(graceTimer);
if (reported) {
process.stdout.write(`${clear}${green}Gracefully restarted ${kCommandStr}${white}\n`);
}
};
}

async function stop() {
watcher.clearFileFilters();
const clearGraceReport = reportGracefulTermination();
await killAndWait();
clearGraceReport();
}

async function restart() {
process.stdout.write(`${clear}${green}Restarting ${kCommandStr}${white}\n`);
await stop();
start();
}

(async () => {
emitExperimentalWarning('Watch mode');

try {
start();

// eslint-disable-next-line no-unused-vars
for await (const _ of on(watcher, 'changed')) {
await restart();
}
} catch (error) {
triggerUncaughtException(error, true /* fromPromise */);
}
})();

// Exiting gracefully to avoid stdout/stderr getting written after
// parent process is killed.
// this is fairly safe since user code cannot run in this process
function signalHandler(signal) {
return async () => {
watcher.clear();
const exitCode = await killAndWait(signal, true);
process.exit(exitCode ?? 0);
};
}
process.on('SIGTERM', signalHandler('SIGTERM'));
process.on('SIGINT', signalHandler('SIGINT'));
10 changes: 10 additions & 0 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ const {
const { getOptionValue } = require('internal/options');
const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
const shouldReportRequiredModules = process.env.WATCH_REPORT_DEPENDENCIES;
// Do not eagerly grab .manifest, it may be in TDZ
const policy = getOptionValue('--experimental-policy') ?
require('internal/process/policy') :
Expand Down Expand Up @@ -186,6 +187,12 @@ function updateChildren(parent, child, scan) {
ArrayPrototypePush(children, child);
}

function reportModuleToWatchMode(filename) {
if (shouldReportRequiredModules && process.send) {
process.send({ 'watch:require': filename });
}
}

const moduleParentCache = new SafeWeakMap();
function Module(id = '', parent) {
this.id = id;
Expand Down Expand Up @@ -806,6 +813,7 @@ Module._load = function(request, parent, isMain) {
// cache key names.
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
const filename = relativeResolveCache[relResolveCacheIdentifier];
reportModuleToWatchMode(filename);
if (filename !== undefined) {
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
Expand Down Expand Up @@ -858,6 +866,8 @@ Module._load = function(request, parent, isMain) {
module.id = '.';
}

reportModuleToWatchMode(filename);

Module._cache[filename] = module;
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
Expand Down
4 changes: 4 additions & 0 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,10 @@ class ESMLoader {
getOptionValue('--inspect-brk')
);

if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
process.send({ 'watch:import': url });
}

const job = new ModuleJob(
this,
url,
Expand Down
23 changes: 23 additions & 0 deletions lib/internal/util/colors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

module.exports = {
blue: '',
green: '',
white: '',
red: '',
clear: '',
hasColors: false,
refresh() {
if (process.stderr.isTTY) {
const hasColors = process.stderr.hasColors();
module.exports.blue = hasColors ? '\u001b[34m' : '';
module.exports.green = hasColors ? '\u001b[32m' : '';
module.exports.white = hasColors ? '\u001b[39m' : '';
module.exports.red = hasColors ? '\u001b[31m' : '';
module.exports.clear = hasColors ? '\u001bc' : '';
module.exports.hasColors = hasColors;
}
}
};

module.exports.refresh();
Loading

0 comments on commit 0e2a359

Please sign in to comment.