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 25, 2022
1 parent bb4dff7 commit 65d5f5b
Show file tree
Hide file tree
Showing 16 changed files with 347 additions and 33 deletions.
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
128 changes: 128 additions & 0 deletions lib/internal/main/watch_mode.js
@@ -0,0 +1,128 @@
'use strict';
const {
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeReduce,
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 } = 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)) :
[process.cwd()];
const kCommand = ArrayPrototypeSlice(process.argv, 1);
const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
const args = ArrayPrototypeReduce(process.execArgv, (acc, flag, i, arr) => {
if (arr[i] !== '--watch-path' && arr[i - 1] !== '--watch-path' && arr[i] !== '--watch') {
acc.push(arr[i]);
}
return acc;
}, []);
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() {
let reported = false;
clearTimeout(graceTimer);
graceTimer = setTimeout(() => {
reported = true;
process.stdout.write(`${blue}Waiting for graceful termination${white}\n`);
}, 1000).unref();
return () => {
clearTimeout(graceTimer);
if (reported) {
process.stdout.write(`${green}gracefully terminated${white}\n`);
}
};
}

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

async function restart() {
// process.stdout.write('\u001Bc');
process.stdout.write(`${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
18 changes: 18 additions & 0 deletions lib/internal/util/colors.js
@@ -0,0 +1,18 @@
'use strict';

module.exports = {
blue: '',
green: '',
white: '',
red: '',
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.refresh();

0 comments on commit 65d5f5b

Please sign in to comment.