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 24, 2022
1 parent 7900f65 commit e8323f0
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 33 deletions.
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
122 changes: 122 additions & 0 deletions lib/internal/main/watch_mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
'use strict';
const {
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePushApply,
ArrayPrototypeReduce,
ArrayPrototypeSlice,
ArrayPrototypeSome,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;
const {
prepareMainThreadExecution,
markBootstrapComplete
} = require('internal/process/pre_execution');
const { getOptionValue } = require('internal/options');
const { green, blue, red, white } = require('internal/util/colors');

const { spawn } = require('child_process');
const { watch } = require('fs/promises');
const { inspect } = require('util');
const { setTimeout, clearTimeout } = require('timers');
const { dirname, sep, resolve } = require('path');
const { once } = require('events');


prepareMainThreadExecution(false);
markBootstrapComplete();

// TODO(MoLow): Make kill signal configurable
const kKillSignal = 'SIGTERM';
const kWatchedPaths = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path));
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;
}, []);
ArrayPrototypePushApply(args, kCommand);

function isWatchedFile(filename) {
if (kWatchedPaths.length > 0) {
return ArrayPrototypeSome(kWatchedPaths, (path) => StringPrototypeStartsWith(filename, path));
}

const directory = dirname(filename);
if (directory === '.') {
return true;
}

const dirs = StringPrototypeSplit(directory, sep);
return !ArrayPrototypeSome(dirs, (dir) => dir[0] === '.' || dir === 'node_modules');
}

function debounce(fn, duration = 100) {
let timeout;
return () => {
if (timeout) {
clearTimeout(timeout);
}

timeout = setTimeout(fn, duration).unref();
};
}

function exitHandler(code) {
if (code === 0) {
process.stdout.write(`${blue}Completed running ${kCommandStr}${white}\n`);
} else {
process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`);
}
}

let graceTimer;
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`);
}
};
}


let childProcess;
async function run(restarting) {
if (restarting) {
process.stdout.write('\u001Bc');
process.stdout.write(`${green}Restarting ${kCommandStr}${white}\n`);
}
if (childProcess && !childProcess.killed) {
childProcess.removeListener('exit', exitHandler);
const onExit = once(childProcess, 'exit');
const clearGraceReport = reportGracefulTermination();
childProcess.kill(kKillSignal);
await onExit;
clearGraceReport();
}

childProcess = spawn(process.execPath, args, { stdio: ['inherit', 'inherit', 'inherit'] });
childProcess.once('exit', exitHandler);
}

const restart = debounce(() => run(true));

(async () => {
run();
const watcher = watch(process.cwd(), { recursive: true });
for await (const event of watcher) {
if (isWatchedFile(resolve(event.filename))) {
restart();
}
}
})();
18 changes: 18 additions & 0 deletions lib/internal/util/colors.js
Original file line number Diff line number Diff line change
@@ -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();
4 changes: 4 additions & 0 deletions src/inspector_agent.cc
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,10 @@ bool Agent::Start(const std::string& path,
const DebugOptions& options,
std::shared_ptr<ExclusiveAccess<HostPort>> host_port,
bool is_main) {
if (!options.allow_attaching_debugger) {
return false;
}

path_ = path;
debug_options_ = options;
CHECK_NOT_NULL(host_port);
Expand Down
4 changes: 4 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,10 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
return StartExecution(env, "internal/main/test_runner");
}

if (env->options()->watch_mode) {
return StartExecution(env, "internal/main/watch_mode");
}

if (!first_argv.empty() && first_argv != "-") {
return StartExecution(env, "internal/main/run_main_module");
}
Expand Down
26 changes: 25 additions & 1 deletion src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,27 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
errors->push_back("either --test or --interactive can be used, not both");
}

debug_options_.allow_attaching_debugger = false;
if (debug_options_.inspector_enabled) {
errors->push_back("the inspector cannot be used with --test");
}
}

if (watch_mode) {
if (syntax_check_only) {
errors->push_back("either --watch or --check can be used, not both");
}

if (has_eval_string) {
errors->push_back("either --watch or --eval can be used, not both");
}

if (force_repl) {
errors->push_back("either --watch or --interactive can be used, not both");
}
debug_options_.allow_attaching_debugger = false;
}

#if HAVE_INSPECTOR
if (!cpu_prof) {
if (!cpu_prof_name.empty()) {
Expand Down Expand Up @@ -586,7 +602,15 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"", /* undocumented, only for debugging */
&EnvironmentOptions::verify_base_objects,
kAllowedInEnvironment);

AddOption("--watch",
"run in watch mode",
&EnvironmentOptions::watch_mode,
kAllowedInEnvironment);
AddOption("--watch-path",
"path to watch",
&EnvironmentOptions::watch_mode_paths,
kAllowedInEnvironment);
Implies("--watch-path", "--watch");
AddOption("--check",
"syntax check script without executing",
&EnvironmentOptions::syntax_check_only);
Expand Down
5 changes: 5 additions & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ class DebugOptions : public Options {
DebugOptions(DebugOptions&&) = default;
DebugOptions& operator=(DebugOptions&&) = default;


bool allow_attaching_debugger = true;
// --inspect
bool inspector_enabled = false;
// --debug
Expand Down Expand Up @@ -172,6 +174,9 @@ class EnvironmentOptions : public Options {
false;
#endif // DEBUG

bool watch_mode = false;
std::vector<std::string> watch_mode_paths;

bool syntax_check_only = false;
bool has_eval_string = false;
bool experimental_wasi = false;
Expand Down

0 comments on commit e8323f0

Please sign in to comment.