diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js new file mode 100644 index 00000000000000..a9f03be7b29e21 --- /dev/null +++ b/lib/internal/main/watch_mode.js @@ -0,0 +1,85 @@ +'use strict'; +const { + ArrayPrototypeConcat, + ArrayPrototypeMap, + ArrayPrototypeReduce, + ArrayPrototypeSlice, + ArrayPrototypeSome, + StringPrototypeSplit, + StringPrototypeStartsWith, +} = primordials; +const { + prepareMainThreadExecution, + markBootstrapComplete +} = require('internal/process/pre_execution'); +const { getOptionValue } = require('internal/options'); + +const { spawn } = require('child_process'); +const { watch } = require('fs/promises'); +const { setTimeout, clearTimeout } = require('timers'); +const { dirname, sep, resolve } = require('path'); + + +prepareMainThreadExecution(false); +markBootstrapComplete(); + +const kWatchedFiles = ArrayPrototypeMap(getOptionValue('--watch-file'), (file) => resolve(file)); +const args = ArrayPrototypeReduce(ArrayPrototypeConcat( + ArrayPrototypeSlice(process.execArgv), + ArrayPrototypeSlice(process.argv, 1), +), (acc, flag, i, arr) => { + if (arr[i] !== '--watch-file' && arr[i - 1] !== '--watch-file' && arr[i] !== '--watch') { + acc.push(arr[i]); + } + return acc; +}, []); + +function isWatchedFile(filename) { + if (kWatchedFiles.length > 0) { + return ArrayPrototypeSome(kWatchedFiles, (file) => StringPrototypeStartsWith(filename, file)); + } + + 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(); + }; +} + +let childProcess; +function run(restarting) { + if (childProcess && !childProcess.killed) { + childProcess.kill(); + } + if (restarting) { + process.stdout.write('\u001Bc'); + process.stdout.write('\u001b[32mrestarting process\u001b[39m\n'); + } + + childProcess = spawn(process.execPath, args, { stdio: ['inherit', 'inherit', 'inherit'] }); +} + +const restart = debounce(run.bind(null, true)); + +(async () => { + run(); + const watcher = watch(process.cwd(), { recursive: true }); + for await (const event of watcher) { + if (isWatchedFile(resolve(event.filename))) { + restart(); + } + } +})(); diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 34bb11e7d7122c..64e0f01b2acead 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -676,6 +676,10 @@ bool Agent::Start(const std::string& path, const DebugOptions& options, std::shared_ptr> host_port, bool is_main) { + if (!options.allow_attaching_debugger) { + return false; + } + path_ = path; debug_options_ = options; CHECK_NOT_NULL(host_port); diff --git a/src/node.cc b/src/node.cc index 2891c18bb9aa9a..454fb1f04fb4ab 100644 --- a/src/node.cc +++ b/src/node.cc @@ -490,6 +490,10 @@ MaybeLocal 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"); } diff --git a/src/node_options.cc b/src/node_options.cc index 0869cbb974be86..ad92c293a2079d 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -156,11 +156,27 @@ void EnvironmentOptions::CheckOptions(std::vector* 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()) { @@ -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-file", + "files to watch", + &EnvironmentOptions::watch_mode_files, + kAllowedInEnvironment); + Implies("--watch-file", "--watch"); AddOption("--check", "syntax check script without executing", &EnvironmentOptions::syntax_check_only); diff --git a/src/node_options.h b/src/node_options.h index ca43192d85a4b4..625a4d69bf36ef 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -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 @@ -172,6 +174,9 @@ class EnvironmentOptions : public Options { false; #endif // DEBUG + bool watch_mode = false; + std::vector watch_mode_files; + bool syntax_check_only = false; bool has_eval_string = false; bool experimental_wasi = false;