From b3b89cf70d33a6e15176f9545498395cd3f89642 Mon Sep 17 00:00:00 2001 From: Archkon <180910180+Archkon@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:59:35 +0800 Subject: [PATCH] src: stop package search at empty package.json Represent package contents with std::optional so a successfully read empty file is distinct from a missing file. This prevents node --run from falling back to scripts in a parent package. Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> --- src/node_task_runner.cc | 16 ++++++++++------ test/fixtures/run-script/empty-json/package.json | 0 test/parallel/test-node-run.js | 13 +++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/run-script/empty-json/package.json diff --git a/src/node_task_runner.cc b/src/node_task_runner.cc index 2b3e005abf34..a76a0aa25e29 100644 --- a/src/node_task_runner.cc +++ b/src/node_task_runner.cc @@ -218,7 +218,7 @@ void ProcessRunner::Run() { std::optional> FindPackageJson(const std::filesystem::path& cwd) { auto package_json_path = cwd / "package.json"; - std::string raw_content; + std::optional raw_content; std::string path_env_var; auto root_path = cwd.root_path(); @@ -231,22 +231,26 @@ FindPackageJson(const std::filesystem::path& cwd) { path_env_var += ConvertPathToUTF8(node_modules_bin) + env_var_separator; } - if (raw_content.empty()) { + if (!raw_content.has_value()) { package_json_path = directory_path / "package.json"; // This is required for Windows because std::filesystem::path::c_str() // returns wchar_t* on Windows, and char* on other platforms. - std::string contents = ConvertPathToUTF8(package_json_path); - USE(ReadFileSync(&raw_content, contents.c_str()) > 0); + std::string package_json_path_string = + ConvertPathToUTF8(package_json_path); + std::string contents; + if (ReadFileSync(&contents, package_json_path_string.c_str()) >= 0) { + raw_content.emplace(std::move(contents)); + } } } // This means that there is no package.json until the root directory. // In this case, we just return nullopt, which will terminate the process.. - if (raw_content.empty()) { + if (!raw_content.has_value()) { return std::nullopt; } - return {{package_json_path, raw_content, path_env_var}}; + return {{package_json_path, std::move(raw_content.value()), path_env_var}}; } void RunTask(const std::shared_ptr& result, diff --git a/test/fixtures/run-script/empty-json/package.json b/test/fixtures/run-script/empty-json/package.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/parallel/test-node-run.js b/test/parallel/test-node-run.js index 7c1f6609f6f1..e6d33ce27863 100644 --- a/test/parallel/test-node-run.js +++ b/test/parallel/test-node-run.js @@ -254,6 +254,19 @@ describe('node --run [command]', () => { assert.strictEqual(child.code, 1); }); + it('stops searching at an empty package.json', async () => { + const packageJsonPath = fixtures.path('run-script/empty-json/package.json'); + const child = await common.spawnPromisified( + process.execPath, + [ '--run', 'test'], + { cwd: fixtures.path('run-script/empty-json') }, + ); + assert.strictEqual(child.stdout, ''); + assert.match(child.stderr, /Can't parse/); + assert(child.stderr.includes(packageJsonPath)); + assert.strictEqual(child.code, 1); + }); + it('returns error when there is no "scripts" field file', async () => { const child = await common.spawnPromisified( process.execPath,