Skip to content

Commit e8323f0

Browse files
committed
cli: add --watch
1 parent 7900f65 commit e8323f0

File tree

7 files changed

+195
-33
lines changed

7 files changed

+195
-33
lines changed

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

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
'use strict';
2+
const {
3+
ArrayPrototypeJoin,
4+
ArrayPrototypeMap,
5+
ArrayPrototypePushApply,
6+
ArrayPrototypeReduce,
7+
ArrayPrototypeSlice,
8+
ArrayPrototypeSome,
9+
StringPrototypeSplit,
10+
StringPrototypeStartsWith,
11+
} = primordials;
12+
const {
13+
prepareMainThreadExecution,
14+
markBootstrapComplete
15+
} = require('internal/process/pre_execution');
16+
const { getOptionValue } = require('internal/options');
17+
const { green, blue, red, white } = require('internal/util/colors');
18+
19+
const { spawn } = require('child_process');
20+
const { watch } = require('fs/promises');
21+
const { inspect } = require('util');
22+
const { setTimeout, clearTimeout } = require('timers');
23+
const { dirname, sep, resolve } = require('path');
24+
const { once } = require('events');
25+
26+
27+
prepareMainThreadExecution(false);
28+
markBootstrapComplete();
29+
30+
// TODO(MoLow): Make kill signal configurable
31+
const kKillSignal = 'SIGTERM';
32+
const kWatchedPaths = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path));
33+
const kCommand = ArrayPrototypeSlice(process.argv, 1);
34+
const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
35+
const args = ArrayPrototypeReduce(process.execArgv, (acc, flag, i, arr) => {
36+
if (arr[i] !== '--watch-path' && arr[i - 1] !== '--watch-path' && arr[i] !== '--watch') {
37+
acc.push(arr[i]);
38+
}
39+
return acc;
40+
}, []);
41+
ArrayPrototypePushApply(args, kCommand);
42+
43+
function isWatchedFile(filename) {
44+
if (kWatchedPaths.length > 0) {
45+
return ArrayPrototypeSome(kWatchedPaths, (path) => StringPrototypeStartsWith(filename, path));
46+
}
47+
48+
const directory = dirname(filename);
49+
if (directory === '.') {
50+
return true;
51+
}
52+
53+
const dirs = StringPrototypeSplit(directory, sep);
54+
return !ArrayPrototypeSome(dirs, (dir) => dir[0] === '.' || dir === 'node_modules');
55+
}
56+
57+
function debounce(fn, duration = 100) {
58+
let timeout;
59+
return () => {
60+
if (timeout) {
61+
clearTimeout(timeout);
62+
}
63+
64+
timeout = setTimeout(fn, duration).unref();
65+
};
66+
}
67+
68+
function exitHandler(code) {
69+
if (code === 0) {
70+
process.stdout.write(`${blue}Completed running ${kCommandStr}${white}\n`);
71+
} else {
72+
process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`);
73+
}
74+
}
75+
76+
let graceTimer;
77+
function reportGracefulTermination() {
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+
}, 1000).unref();
84+
return () => {
85+
clearTimeout(graceTimer);
86+
if (reported) {
87+
process.stdout.write(`${green}gracefully terminated${white}\n`);
88+
}
89+
};
90+
}
91+
92+
93+
let childProcess;
94+
async function run(restarting) {
95+
if (restarting) {
96+
process.stdout.write('\u001Bc');
97+
process.stdout.write(`${green}Restarting ${kCommandStr}${white}\n`);
98+
}
99+
if (childProcess && !childProcess.killed) {
100+
childProcess.removeListener('exit', exitHandler);
101+
const onExit = once(childProcess, 'exit');
102+
const clearGraceReport = reportGracefulTermination();
103+
childProcess.kill(kKillSignal);
104+
await onExit;
105+
clearGraceReport();
106+
}
107+
108+
childProcess = spawn(process.execPath, args, { stdio: ['inherit', 'inherit', 'inherit'] });
109+
childProcess.once('exit', exitHandler);
110+
}
111+
112+
const restart = debounce(() => run(true));
113+
114+
(async () => {
115+
run();
116+
const watcher = watch(process.cwd(), { recursive: true });
117+
for await (const event of watcher) {
118+
if (isWatchedFile(resolve(event.filename))) {
119+
restart();
120+
}
121+
}
122+
})();

lib/internal/util/colors.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
module.exports = {
4+
blue: '',
5+
green: '',
6+
white: '',
7+
red: '',
8+
refresh() {
9+
if (process.stderr.isTTY && process.stderr.hasColors()) {
10+
module.exports.blue = '\u001b[34m';
11+
module.exports.green = '\u001b[32m';
12+
module.exports.white = '\u001b[39m';
13+
module.exports.red = '\u001b[31m';
14+
}
15+
}
16+
};
17+
18+
module.exports.refresh();

src/inspector_agent.cc

+4
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,10 @@ bool Agent::Start(const std::string& path,
676676
const DebugOptions& options,
677677
std::shared_ptr<ExclusiveAccess<HostPort>> host_port,
678678
bool is_main) {
679+
if (!options.allow_attaching_debugger) {
680+
return false;
681+
}
682+
679683
path_ = path;
680684
debug_options_ = options;
681685
CHECK_NOT_NULL(host_port);

src/node.cc

+4
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,10 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
490490
return StartExecution(env, "internal/main/test_runner");
491491
}
492492

493+
if (env->options()->watch_mode) {
494+
return StartExecution(env, "internal/main/watch_mode");
495+
}
496+
493497
if (!first_argv.empty() && first_argv != "-") {
494498
return StartExecution(env, "internal/main/run_main_module");
495499
}

src/node_options.cc

+25-1
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,27 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
156156
errors->push_back("either --test or --interactive can be used, not both");
157157
}
158158

159+
debug_options_.allow_attaching_debugger = false;
159160
if (debug_options_.inspector_enabled) {
160161
errors->push_back("the inspector cannot be used with --test");
161162
}
162163
}
163164

165+
if (watch_mode) {
166+
if (syntax_check_only) {
167+
errors->push_back("either --watch or --check can be used, not both");
168+
}
169+
170+
if (has_eval_string) {
171+
errors->push_back("either --watch or --eval can be used, not both");
172+
}
173+
174+
if (force_repl) {
175+
errors->push_back("either --watch or --interactive can be used, not both");
176+
}
177+
debug_options_.allow_attaching_debugger = false;
178+
}
179+
164180
#if HAVE_INSPECTOR
165181
if (!cpu_prof) {
166182
if (!cpu_prof_name.empty()) {
@@ -586,7 +602,15 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
586602
"", /* undocumented, only for debugging */
587603
&EnvironmentOptions::verify_base_objects,
588604
kAllowedInEnvironment);
589-
605+
AddOption("--watch",
606+
"run in watch mode",
607+
&EnvironmentOptions::watch_mode,
608+
kAllowedInEnvironment);
609+
AddOption("--watch-path",
610+
"path to watch",
611+
&EnvironmentOptions::watch_mode_paths,
612+
kAllowedInEnvironment);
613+
Implies("--watch-path", "--watch");
590614
AddOption("--check",
591615
"syntax check script without executing",
592616
&EnvironmentOptions::syntax_check_only);

src/node_options.h

+5
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ class DebugOptions : public Options {
7171
DebugOptions(DebugOptions&&) = default;
7272
DebugOptions& operator=(DebugOptions&&) = default;
7373

74+
75+
bool allow_attaching_debugger = true;
7476
// --inspect
7577
bool inspector_enabled = false;
7678
// --debug
@@ -172,6 +174,9 @@ class EnvironmentOptions : public Options {
172174
false;
173175
#endif // DEBUG
174176

177+
bool watch_mode = false;
178+
std::vector<std::string> watch_mode_paths;
179+
175180
bool syntax_check_only = false;
176181
bool has_eval_string = false;
177182
bool experimental_wasi = false;

0 commit comments

Comments
 (0)