Skip to content

Commit

Permalink
cli: add --watch
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLow committed Aug 26, 2022
1 parent a5d27f4 commit a64b5c8
Show file tree
Hide file tree
Showing 21 changed files with 654 additions and 34 deletions.
28 changes: 28 additions & 0 deletions doc/api/cli.md
Expand Up @@ -1577,6 +1577,32 @@ 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 the Node.js in watch mode.
When in watch mode, changes in the watched files cause node to restart.
By default, watch mode will watch the entry point
and any required module. use `--watch-path` to specify what paths to watch

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

### `--watch-path`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Specify what paths to watch in watch mode.

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

<!-- YAML
Expand Down Expand Up @@ -1880,6 +1906,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
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
125 changes: 125 additions & 0 deletions lib/internal/main/watch_mode.js
@@ -0,0 +1,125 @@
'use strict';
const {
ArrayPrototypeFilter,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeSlice,
} = primordials;

const {
prepareMainThreadExecution,
markBootstrapComplete
} = require('internal/process/pre_execution');
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 = getOptionValue('--watch-path').length ?
ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path)) :
[];
const kCommand = ArrayPrototypeSlice(process.argv, 1);
const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
const args = ArrayPrototypeFilter(process.execArgv, (x, i, arr) =>
x !== '--watch-path' && arr[i - 1] !== '--watch-path' && x !== '--watch');
ArrayPrototypePush(args, '--watch-report-ipc');
ArrayPrototypePushApply(args, kCommand);

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

function start() {
// Spawning in detached mode so node can control when signals are forwarded
exited = false;
const stdio = kShouldFilterModules ? ['inherit', 'inherit', 'inherit', 'ipc'] : undefined;
child = spawn(process.execPath, args, { stdio, detached: true });
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) {
child?.removeAllListeners();
if (!child || child.killed || exited) {
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');
start();

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

// 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();
process.exit(await killAndWait(signal));
};
}
process.on('SIGTERM', signalHandler('SIGTERM'));
process.on('SIGINT', signalHandler('SIGINT'));
10 changes: 10 additions & 0 deletions lib/internal/modules/cjs/loader.js
Expand Up @@ -100,6 +100,7 @@ const {
const { getOptionValue } = require('internal/options');
const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
const shouldReportRequiredModules = getOptionValue('--watch-report-ipc');
// Do not eagerly grab .manifest, it may be in TDZ
const policy = getOptionValue('--experimental-policy') ?
require('internal/process/policy') :
Expand Down Expand Up @@ -168,6 +169,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 @@ -776,6 +783,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 @@ -828,6 +836,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
Expand Up @@ -475,6 +475,10 @@ class ESMLoader {
getOptionValue('--inspect-brk')
);

if (getOptionValue('--watch-report-ipc') && process.send) {
process.send({ 'watch:import': url });
}

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

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

module.exports.refresh();

0 comments on commit a64b5c8

Please sign in to comment.