diff --git a/doc/api/cli.md b/doc/api/cli.md
index 684627637b5a0b..bfadc6d36e8389 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -2596,8 +2596,14 @@ Use `--watch-path` to specify what paths to watch.
This flag cannot be combined with
`--check`, `--eval`, `--interactive`, or the REPL.
+If using `--run` and `--watch` Node.js will read the `package.json` scripts
+field each restart.
+If using `--run` and `--watch` Node.js positional arguments
+will throw an error.
+
```bash
node --watch index.js
+node --watch --run start
```
### `--watch-path`
diff --git a/doc/api/errors.md b/doc/api/errors.md
index 4d3829f78e7b95..9cbdce450cc5f6 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -2359,6 +2359,22 @@ The `package.json` [`"exports"`][] field does not export the requested subpath.
Because exports are encapsulated, private internal modules that are not exported
cannot be imported through the package resolution, unless using an absolute URL.
+
+
+### `ERR_PACKAGE_SCRIPT_MISSING`
+
+The `package.json` "scripts" field does not contain the requested script.
+In order to use `--run` ensure that the contents are an object containing a
+`"scripts"` field that contains the missing script name as a field.
+
+```json
+{
+ "scripts": {
+ "start": "node server.js"
+ }
+}
+```
+
### `ERR_PARSE_ARGS_INVALID_OPTION_VALUE`
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index f78be397d5dce2..229e1254d8f824 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1633,6 +1633,15 @@ E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => {
return `Package subpath '${subpath}' is not defined by "exports" in ${
pkgPath}package.json${base ? ` imported from ${base}` : ''}`;
}, Error);
+E('ERR_PACKAGE_SCRIPT_MISSING', (pjsonPath, script, parsedJSON, base) => {
+ if (!pjsonPath) {
+ return `unable to find package.json from directory ${base}`;
+ }
+ if (!('scripts' in parsedJSON)) {
+ return `package.json from directory ${base} (${pjsonPath}) has no "scripts" field`;
+ }
+ return `package.json from directory ${base} (${pjsonPath}) has no entry in "scripts": ${JSONStringify(script)}`;
+}, Error);
E('ERR_PARSE_ARGS_INVALID_OPTION_VALUE', '%s', TypeError);
E('ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL', "Unexpected argument '%s'. This " +
'command does not take positional arguments', TypeError);
diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js
index d70908e6e3cd9b..b344a7713e0d63 100644
--- a/lib/internal/main/watch_mode.js
+++ b/lib/internal/main/watch_mode.js
@@ -1,11 +1,12 @@
'use strict';
const {
+ ArrayPrototypeFilter,
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ArrayPrototypeMap,
- ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeSlice,
+ JSONParse,
StringPrototypeStartsWith,
} = primordials;
@@ -18,14 +19,21 @@ const {
exitCodes: { kNoFailure },
} = internalBinding('errors');
const { getOptionValue } = require('internal/options');
+const {
+ ERR_PACKAGE_SCRIPT_MISSING,
+ ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL,
+} = require('internal/errors').codes;
const { FilesWatcher } = require('internal/watch_mode/files_watcher');
const { green, blue, red, white, clear } = require('internal/util/colors');
const { spawn } = require('child_process');
const { inspect } = require('util');
const { setTimeout, clearTimeout } = require('timers');
-const { resolve } = require('path');
+const { resolve, join } = require('path');
const { once } = require('events');
+const { getNearestParentPackageJSON } = require('internal/modules/package_json_reader');
+const { readFileSync } = require('fs');
+const { emitWarning } = require('internal/process/warning');
prepareMainThreadExecution(false, false);
markBootstrapComplete();
@@ -35,23 +43,28 @@ const kKillSignal = 'SIGTERM';
const kShouldFilterModules = getOptionValue('--watch-path').length === 0;
const kWatchedPaths = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path));
const kPreserveOutput = getOptionValue('--watch-preserve-output');
+const kRun = getOptionValue('--run');
const kCommand = ArrayPrototypeSlice(process.argv, 1);
-const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
-
-const argsWithoutWatchOptions = [];
+let kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
-for (let i = 0; i < process.execArgv.length; i++) {
- const arg = process.execArgv[i];
- if (StringPrototypeStartsWith(arg, '--watch')) {
- i++;
- const nextArg = process.execArgv[i];
- if (nextArg && StringPrototypeStartsWith(nextArg, '-')) {
- ArrayPrototypePush(argsWithoutWatchOptions, nextArg);
+const argsWithoutWatchOptions = ArrayPrototypeFilter(process.execArgv, (v, i) => {
+ if (StringPrototypeStartsWith(v, '--watch')) {
+ return false;
+ }
+ if (v === '--run') {
+ return false;
+ }
+ if (i > 0 && v[0] !== '-') {
+ const prevArg = process.execArgv[i - 1];
+ if (StringPrototypeStartsWith(prevArg, '--watch')) {
+ return false;
+ }
+ if (prevArg === '--run') {
+ return false;
}
- continue;
}
- ArrayPrototypePush(argsWithoutWatchOptions, arg);
-}
+ return true;
+});
ArrayPrototypePushApply(argsWithoutWatchOptions, kCommand);
@@ -59,19 +72,64 @@ const watcher = new FilesWatcher({ debounce: 200, mode: kShouldFilterModules ? '
ArrayPrototypeForEach(kWatchedPaths, (p) => watcher.watchPath(p));
let graceTimer;
+/**
+ * @type {ChildProcess}
+ */
let child;
let exited;
function start() {
exited = false;
const stdio = kShouldFilterModules ? ['inherit', 'inherit', 'inherit', 'ipc'] : 'inherit';
- child = spawn(process.execPath, argsWithoutWatchOptions, {
- stdio,
- env: {
- ...process.env,
- WATCH_REPORT_DEPENDENCIES: '1',
- },
- });
+ if (!kRun) {
+ child = spawn(process.execPath, argsWithoutWatchOptions, {
+ stdio,
+ env: {
+ ...process.env,
+ WATCH_REPORT_DEPENDENCIES: '1',
+ },
+ });
+ } else {
+ if (kCommand.length !== 0) {
+ throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL('cannot provide positional argument to both --run and --watch');
+ }
+ // Always get new data in case it changed
+ const base = process.cwd();
+ const pkgJSON = getNearestParentPackageJSON(join(base, 'package.json'), false);
+ if (!pkgJSON) {
+ throw new ERR_PACKAGE_SCRIPT_MISSING(null, kRun, undefined, base);
+ }
+ let rawJSON;
+ try {
+ rawJSON = JSONParse(readFileSync(pkgJSON.data.pjsonPath, {
+ encoding: 'utf8',
+ }));
+ } catch (e) {
+ emitWarning(`Error parsing JSON in ${pkgJSON.data.pjsonPath}. ${e}`);
+ }
+ if (rawJSON) {
+ if (!('scripts' in rawJSON)) {
+ throw new ERR_PACKAGE_SCRIPT_MISSING(pkgJSON.data.pjsonPath, kRun, rawJSON, base);
+ }
+ if (!(kRun in rawJSON.scripts)) {
+ throw new ERR_PACKAGE_SCRIPT_MISSING(pkgJSON.data.pjsonPath, kRun, rawJSON, base);
+ }
+ const script = rawJSON.scripts[kRun];
+ const newCommandStr = inspect(script);
+ if (newCommandStr !== kCommandStr) {
+ kCommandStr = newCommandStr;
+ process.stdout.write(`${blue}Running ${kCommandStr}${white}\n`);
+ }
+ child = spawn(script, kCommand, {
+ stdio,
+ shell: true,
+ env: {
+ ...process.env,
+ WATCH_REPORT_DEPENDENCIES: '1',
+ },
+ });
+ }
+ }
watcher.watchChildProcessModules(child);
child.once('exit', (code) => {
exited = true;
diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js
index 9a9dcebb799c00..2b95f03064e5d5 100644
--- a/lib/internal/modules/package_json_reader.js
+++ b/lib/internal/modules/package_json_reader.js
@@ -77,7 +77,7 @@ function deserializePackageJSON(path, contents) {
* }} options
* @returns {import('typings/internalBinding/modules').PackageConfig}
*/
-function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
+function read(jsonPath, { base, specifier, isESM, cached = true } = kEmptyObject) {
// This function will be called by both CJS and ESM, so we need to make sure
// non-null attributes are converted to strings.
const parsed = modulesBinding.readPackageJSON(
@@ -85,6 +85,7 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
isESM,
base == null ? undefined : `${base}`,
specifier == null ? undefined : `${specifier}`,
+ cached,
);
return deserializePackageJSON(jsonPath, parsed);
@@ -107,8 +108,8 @@ function readPackage(requestPath) {
* @param {string} checkPath The path to start searching from.
* @returns {undefined | {data: import('typings/internalBinding/modules').PackageConfig, path: string}}
*/
-function getNearestParentPackageJSON(checkPath) {
- const result = modulesBinding.getNearestParentPackageJSON(checkPath);
+function getNearestParentPackageJSON(checkPath, cached = true) {
+ const result = modulesBinding.getNearestParentPackageJSON(checkPath, undefined, undefined, undefined, cached);
if (result === undefined) {
return undefined;
diff --git a/src/node.cc b/src/node.cc
index a13c5a45496fd0..ae6383a48c13fd 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -992,7 +992,8 @@ InitializeOncePerProcessInternal(const std::vector& args,
}
}
- if (!per_process::cli_options->run.empty()) {
+ if (!per_process::cli_options->run.empty() &&
+ !per_process::cli_options->per_isolate->per_env->watch_mode) {
// TODO(@anonrig): Handle NODE_NO_WARNINGS, NODE_REDIRECT_WARNINGS,
// --disable-warning and --redirect-warnings.
if (per_process::cli_options->per_isolate->per_env->warnings) {
diff --git a/src/node_modules.cc b/src/node_modules.cc
index 2e80ff4e95a5c1..f9f62b886fa35f 100644
--- a/src/node_modules.cc
+++ b/src/node_modules.cc
@@ -85,12 +85,17 @@ Local BindingData::PackageConfig::Serialize(Realm* realm) const {
}
const BindingData::PackageConfig* BindingData::GetPackageJSON(
- Realm* realm, std::string_view path, ErrorContext* error_context) {
+ Realm* realm,
+ std::string_view path,
+ ErrorContext* error_context,
+ bool cached) {
auto binding_data = realm->GetBindingData();
- auto cache_entry = binding_data->package_configs_.find(path.data());
- if (cache_entry != binding_data->package_configs_.end()) {
- return &cache_entry->second;
+ if (cached == true) {
+ auto cache_entry = binding_data->package_configs_.find(path.data());
+ if (cache_entry != binding_data->package_configs_.end()) {
+ return &cache_entry->second;
+ }
}
PackageConfig package_config{};
@@ -228,14 +233,14 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON(
}
// package_config could be quite large, so we should move it instead of
// copying it.
- auto cached = binding_data->package_configs_.insert(
+ auto after_cached = binding_data->package_configs_.insert(
{std::string(path), std::move(package_config)});
- return &cached.first->second;
+ return &after_cached.first->second;
}
void BindingData::ReadPackageJSON(const FunctionCallbackInfo& args) {
- CHECK_GE(args.Length(), 1); // path, [is_esm, base, specifier]
+ CHECK_GE(args.Length(), 1); // path, [is_esm, base, specifier, cached = true]
CHECK(args[0]->IsString()); // path
Realm* realm = Realm::GetCurrent(args);
@@ -255,6 +260,8 @@ void BindingData::ReadPackageJSON(const FunctionCallbackInfo& args) {
Utf8Value specifier(isolate, args[3]);
error_context.specifier = specifier.ToString();
}
+ // everything except exactly false default to cached
+ auto cached = !(args[4]->IsFalse());
THROW_IF_INSUFFICIENT_PERMISSIONS(
realm->env(),
@@ -264,11 +271,13 @@ void BindingData::ReadPackageJSON(const FunctionCallbackInfo& args) {
// TODO(StefanStojanovic): Remove ifdef after
// path.toNamespacedPath logic is ported to C++
#ifdef _WIN32
- auto package_json = GetPackageJSON(
- realm, "\\\\?\\" + path.ToString(), is_esm ? &error_context : nullptr);
+ auto package_json = GetPackageJSON(realm,
+ "\\\\?\\" + path.ToString(),
+ is_esm ? &error_context : nullptr,
+ cached);
#else
- auto package_json =
- GetPackageJSON(realm, path.ToString(), is_esm ? &error_context : nullptr);
+ auto package_json = GetPackageJSON(
+ realm, path.ToString(), is_esm ? &error_context : nullptr, cached);
#endif
if (package_json == nullptr) {
return;
diff --git a/src/node_modules.h b/src/node_modules.h
index 17909b2270454b..3a153d688d8c39 100644
--- a/src/node_modules.h
+++ b/src/node_modules.h
@@ -79,7 +79,8 @@ class BindingData : public SnapshotableObject {
static const PackageConfig* GetPackageJSON(
Realm* realm,
std::string_view path,
- ErrorContext* error_context = nullptr);
+ ErrorContext* error_context = nullptr,
+ bool cached = true);
static const PackageConfig* TraverseParent(
Realm* realm, const std::filesystem::path& check_path);
};
diff --git a/src/node_options.cc b/src/node_options.cc
index b47ee5fea16e2e..681dabfdd83364 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -176,7 +176,8 @@ void EnvironmentOptions::CheckOptions(std::vector* errors,
} else if (test_runner_force_exit) {
errors->push_back("either --watch or --test-force-exit "
"can be used, not both");
- } else if (!test_runner && (argv->size() < 1 || (*argv)[1].empty())) {
+ } else if (!(test_runner || !per_process::cli_options->run.empty()) &&
+ (argv->size() < 1 || (*argv)[1].empty())) {
errors->push_back("--watch requires specifying a file");
}
diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs
index d1dbd75323ddc5..1b3eaf54e34999 100644
--- a/test/sequential/test-watch-mode.mjs
+++ b/test/sequential/test-watch-mode.mjs
@@ -30,6 +30,12 @@ function createTmpFile(content = 'console.log("running");', ext = '.js', basenam
return file;
}
+function createPackageJSON(content = {}, basename = tmpdir.path) {
+ const file = path.join(basename, 'package.json');
+ writeFileSync(file, JSON.stringify(content, null, 2));
+ return file;
+}
+
async function runWriteSucceed({
file,
watchedFile,
@@ -550,6 +556,43 @@ console.log(values.random);
]);
});
+ it('should run when `--watch --run`', async () => {
+ const script = Math.random();
+ const output = Math.random();
+ const command = `echo ${output}`;
+ const file = createPackageJSON({
+ scripts: {
+ [script]: command
+ }
+ });
+ const args = ['--watch', '--run', script];
+ const { stdout, stderr } = await runWriteSucceed({ file, watchedFile: file, watchFlag: null, args, options: {
+ cwd: path.dirname(file)
+ } });
+
+ assert.strictEqual(stderr, '');
+ assert.deepStrictEqual(stdout, [
+ `Running '${command}'`,
+ `${output}`,
+ `Completed running '${command}'`,
+ ]);
+ });
+
+ it('should error when `--watch --run` with positional arguments', async () => {
+ const file = createPackageJSON({
+ scripts: {
+ start: 'echo 123'
+ }
+ });
+ const args = ['--watch', '--run', 'start', 'positional'];
+ const { stdout, stderr } = await runWriteSucceed({ file, watchedFile: file, watchFlag: null, args, options: {
+ cwd: path.dirname(file)
+ } });
+
+ assert.match(stderr, /cannot provide positional argument to both --run and --watch/);
+ assert.deepStrictEqual(stdout, []);
+ });
+
it('should run when `--watch -r ./foo.js`', async () => {
const projectDir = tmpdir.resolve('project7');
mkdirSync(projectDir);