Skip to content

Commit 65d5f5b

Browse files
committed
cli: add --watch
1 parent bb4dff7 commit 65d5f5b

File tree

16 files changed

+347
-33
lines changed

16 files changed

+347
-33
lines changed

lib/internal/assert/assertion_error.js

Lines changed: 17 additions & 32 deletions
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

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use strict';
2+
const {
3+
ArrayPrototypeJoin,
4+
ArrayPrototypeMap,
5+
ArrayPrototypePush,
6+
ArrayPrototypePushApply,
7+
ArrayPrototypeReduce,
8+
ArrayPrototypeSlice,
9+
} = primordials;
10+
const {
11+
prepareMainThreadExecution,
12+
markBootstrapComplete
13+
} = require('internal/process/pre_execution');
14+
const { getOptionValue } = require('internal/options');
15+
const { emitExperimentalWarning } = require('internal/util');
16+
const { FilesWatcher } = require('internal/watch_mode/files_watcher');
17+
const { green, blue, red, white } = require('internal/util/colors');
18+
19+
const { spawn } = require('child_process');
20+
const { inspect } = require('util');
21+
const { setTimeout, clearTimeout } = require('timers');
22+
const { resolve } = require('path');
23+
const { once, on } = require('events');
24+
25+
26+
prepareMainThreadExecution(false, false);
27+
markBootstrapComplete();
28+
29+
// TODO(MoLow): Make kill signal configurable
30+
const kKillSignal = 'SIGTERM';
31+
const kShouldFilterModules = getOptionValue('--watch-path').length === 0;
32+
const kWatchedPaths = getOptionValue('--watch-path').length ?
33+
ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path)) :
34+
[process.cwd()];
35+
const kCommand = ArrayPrototypeSlice(process.argv, 1);
36+
const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
37+
const args = ArrayPrototypeReduce(process.execArgv, (acc, flag, i, arr) => {
38+
if (arr[i] !== '--watch-path' && arr[i - 1] !== '--watch-path' && arr[i] !== '--watch') {
39+
acc.push(arr[i]);
40+
}
41+
return acc;
42+
}, []);
43+
ArrayPrototypePush(args, '--watch-report-ipc');
44+
ArrayPrototypePushApply(args, kCommand);
45+
46+
const watcher = new FilesWatcher({ throttle: 500, mode: kShouldFilterModules ? 'filter' : 'all' });
47+
kWatchedPaths.forEach((p) => watcher.watchPath(p));
48+
let graceTimer;
49+
let child;
50+
let exited;
51+
52+
function start() {
53+
// Spawning in detached mode so node can control when signals are forwarded
54+
exited = false;
55+
const stdio = kShouldFilterModules ? ['inherit', 'inherit', 'inherit', 'ipc'] : undefined;
56+
child = spawn(process.execPath, args, { stdio, detached: true });
57+
watcher.watchChildProcessModules(child);
58+
child.once('exit', (code) => {
59+
exited = true;
60+
if (code === 0) {
61+
process.stdout.write(`${blue}Completed running ${kCommandStr}${white}\n`);
62+
} else {
63+
process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`);
64+
}
65+
});
66+
}
67+
68+
async function killAndWait(signal = kKillSignal) {
69+
child?.removeAllListeners();
70+
if (!child || child.killed || exited) {
71+
return;
72+
}
73+
const onExit = once(child, 'exit');
74+
child.kill(signal);
75+
const { 0: exitCode } = await onExit;
76+
return exitCode;
77+
}
78+
79+
function reportGracefulTermination() {
80+
let reported = false;
81+
clearTimeout(graceTimer);
82+
graceTimer = setTimeout(() => {
83+
reported = true;
84+
process.stdout.write(`${blue}Waiting for graceful termination${white}\n`);
85+
}, 1000).unref();
86+
return () => {
87+
clearTimeout(graceTimer);
88+
if (reported) {
89+
process.stdout.write(`${green}gracefully terminated${white}\n`);
90+
}
91+
};
92+
}
93+
94+
async function stop() {
95+
watcher.clearFileFilters();
96+
const clearGraceReport = reportGracefulTermination();
97+
await killAndWait();
98+
clearGraceReport();
99+
}
100+
101+
async function restart() {
102+
// process.stdout.write('\u001Bc');
103+
process.stdout.write(`${green}Restarting ${kCommandStr}${white}\n`);
104+
await stop();
105+
start();
106+
}
107+
108+
(async () => {
109+
emitExperimentalWarning('Watch mode');
110+
start();
111+
112+
// eslint-disable-next-line no-unused-vars
113+
for await (const _ of on(watcher, 'changed')) {
114+
await restart();
115+
}
116+
})();
117+
118+
// Exiting gracefully to avoid stdout/stderr getting written after
119+
// parent process is killed.
120+
// this is fairly safe since user code cannot run in this process
121+
function signalHandler(signal) {
122+
return async () => {
123+
watcher.clear();
124+
process.exit(await killAndWait(signal));
125+
};
126+
}
127+
process.on('SIGTERM', signalHandler('SIGTERM'));
128+
process.on('SIGINT', signalHandler('SIGINT'));

lib/internal/modules/cjs/loader.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ const {
100100
const { getOptionValue } = require('internal/options');
101101
const preserveSymlinks = getOptionValue('--preserve-symlinks');
102102
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
103+
const shouldReportRequiredModules = getOptionValue('--watch-report-ipc');
103104
// Do not eagerly grab .manifest, it may be in TDZ
104105
const policy = getOptionValue('--experimental-policy') ?
105106
require('internal/process/policy') :
@@ -168,6 +169,12 @@ function updateChildren(parent, child, scan) {
168169
ArrayPrototypePush(children, child);
169170
}
170171

172+
function reportModuleToWatchMode(filename) {
173+
if (shouldReportRequiredModules && process.send) {
174+
process.send({ 'watch:require': filename });
175+
}
176+
}
177+
171178
const moduleParentCache = new SafeWeakMap();
172179
function Module(id = '', parent) {
173180
this.id = id;
@@ -776,6 +783,7 @@ Module._load = function(request, parent, isMain) {
776783
// cache key names.
777784
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
778785
const filename = relativeResolveCache[relResolveCacheIdentifier];
786+
reportModuleToWatchMode(filename);
779787
if (filename !== undefined) {
780788
const cachedModule = Module._cache[filename];
781789
if (cachedModule !== undefined) {
@@ -828,6 +836,8 @@ Module._load = function(request, parent, isMain) {
828836
module.id = '.';
829837
}
830838

839+
reportModuleToWatchMode(filename);
840+
831841
Module._cache[filename] = module;
832842
if (parent !== undefined) {
833843
relativeResolveCache[relResolveCacheIdentifier] = filename;

lib/internal/modules/esm/loader.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ class ESMLoader {
475475
getOptionValue('--inspect-brk')
476476
);
477477

478+
if (getOptionValue('--watch-report-ipc') && process.send) {
479+
process.send({ 'watch:import': url });
480+
}
481+
478482
const job = new ModuleJob(
479483
this,
480484
url,

lib/internal/util/colors.js

Lines changed: 18 additions & 0 deletions
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();

0 commit comments

Comments
 (0)