diff --git a/src/node_task_runner.cc b/src/node_task_runner.cc index efaf6f01b3fbf9..b809162bc615f3 100644 --- a/src/node_task_runner.cc +++ b/src/node_task_runner.cc @@ -291,57 +291,180 @@ void RunTask(const std::shared_ptr& result, return; } - // If package_json object doesn't have "scripts" field, throw an error. simdjson::ondemand::object scripts_object; - if (main_object["scripts"].get_object().get(scripts_object)) { - fprintf( - stderr, "Can't find \"scripts\" field in %s\n", path.string().c_str()); - result->exit_code_ = ExitCode::kGenericUserError; - return; - } - - // If the command_id is not found in the scripts object, throw an error. - std::string_view command; - if (auto command_error = - scripts_object[command_id].get_string().get(command)) { - if (command_error == simdjson::error_code::INCORRECT_TYPE) { + bool have_scripts = main_object["scripts"].get_object().get(scripts_object) == + simdjson::error_code::SUCCESS; + + std::string exec_cmd; + if (have_scripts) { + std::string_view cmd_string; + auto err = scripts_object[command_id].get_string().get(cmd_string); + if (err == simdjson::error_code::SUCCESS) { + exec_cmd.assign(cmd_string); + ProcessRunner runner(result, + path, + command_id, + exec_cmd, + path_env_var, + positional_args); + runner.Run(); + return; + } + if (err == simdjson::error_code::INCORRECT_TYPE) { fprintf(stderr, "Script \"%.*s\" is unexpectedly not a string for %s\n\n", static_cast(command_id.size()), command_id.data(), path.string().c_str()); - } else { - fprintf(stderr, - "Missing script: \"%.*s\" for %s\n\n", - static_cast(command_id.size()), - command_id.data(), - path.string().c_str()); - fprintf(stderr, "Available scripts are:\n"); - - // Reset the object to iterate over it again - scripts_object.reset(); - simdjson::ondemand::value value; - for (auto field : scripts_object) { - std::string_view key_str; - std::string_view value_str; - if (!field.unescaped_key().get(key_str) && !field.value().get(value) && - !value.get_string().get(value_str)) { + result->exit_code_ = ExitCode::kGenericUserError; + return; + } + } + + // Try "bin" + simdjson::ondemand::value bin_value; + bool have_bin = + main_object["bin"].get(bin_value) == simdjson::error_code::SUCCESS; + + if (!have_scripts && !have_bin) { + fprintf(stderr, + "Can't find \"scripts\" or \"bin\" fields in %s\n", + path.string().c_str()); + result->exit_code_ = ExitCode::kGenericUserError; + return; + } + + if (have_bin) { + simdjson::ondemand::json_type bin_type; + if (!bin_value.type().get(bin_type)) { + std::string exec_rel; + + if (bin_type == simdjson::ondemand::json_type::string) { + // "bin": "./cli.js" + std::string_view rel; + if (!bin_value.get_string().get(rel)) { + std::string_view pkg_name; + if (!main_object["name"].get_string().get(pkg_name) && + pkg_name == command_id) { + exec_rel.assign(rel); + } else { + fprintf(stderr, "Incorrect command for %s\n", path.string().c_str()); + result->exit_code_ = ExitCode::kGenericUserError; + return; + } + } + } else if (bin_type == simdjson::ondemand::json_type::object) { + // "bin": { "foo": "./cli.js" } + simdjson::ondemand::object bin_obj; + if (bin_value.get_object().get(bin_obj) == + simdjson::error_code::SUCCESS) { + std::string_view rel; + auto err = bin_obj[command_id].get_string().get(rel); + if (err == simdjson::error_code::SUCCESS) { + exec_rel.assign(rel); + } else if (err == simdjson::error_code::INCORRECT_TYPE) { + fprintf(stderr, + "Bin \"%.*s\" is unexpectedly not a string for %s\n", + static_cast(command_id.size()), + command_id.data(), + path.string().c_str()); + result->exit_code_ = ExitCode::kGenericUserError; + return; + } + } + } else { + fprintf(stderr, + "Bin \"%.*s\" is unexpectedly not a string for %s\n", + static_cast(command_id.size()), + command_id.data(), + path.string().c_str()); + result->exit_code_ = ExitCode::kGenericUserError; + return; + } + + if (!exec_rel.empty()) { + std::filesystem::path exec_path(exec_rel); + if (exec_path.is_relative()) exec_path = path.parent_path() / exec_path; + + auto ext = exec_path.extension().string(); + bool needs_node = ext == ".js" || ext == ".mjs" || ext == ".cjs"; + + exec_cmd = + needs_node ? "node " + EscapeShell(exec_path.string()) : exec_rel; + + ProcessRunner runner( + result, path, command_id, exec_cmd, path_env_var, positional_args); + runner.Run(); + return; + } + } + } + + fprintf(stderr, + "Unknown script or bin entry \"%.*s\" for %s\n\n", + static_cast(command_id.size()), + command_id.data(), + path.string().c_str()); + + if (have_scripts) { + fprintf(stderr, "Available scripts:\n"); + scripts_object.reset(); + simdjson::ondemand::value value; + for (auto field : scripts_object) { + std::string_view key_str, value_str; + if (!field.unescaped_key().get(key_str) && !field.value().get(value) && + !value.get_string().get(value_str)) { + fprintf(stderr, + " %.*s: %.*s\n", + static_cast(key_str.size()), + key_str.data(), + static_cast(value_str.size()), + value_str.data()); + } + } + } else { + fprintf(stderr, "No scripts defined in %s\n", path.string().c_str()); + } + + if (have_bin) { + fprintf(stderr, "\nAvailable bins:\n"); + simdjson::ondemand::json_type t; + if (!bin_value.type().get(t)) { + if (t == simdjson::ondemand::json_type::string) { + std::string_view rel, pkg_name; + if (!bin_value.get_string().get(rel) && + !main_object["name"].get_string().get(pkg_name)) { fprintf(stderr, " %.*s: %.*s\n", - static_cast(key_str.size()), - key_str.data(), - static_cast(value_str.size()), - value_str.data()); + static_cast(pkg_name.size()), + pkg_name.data(), + static_cast(rel.size()), + rel.data()); + } + } else if (t == simdjson::ondemand::json_type::object) { + simdjson::ondemand::object bin_obj; + if (!bin_value.get_object().get(bin_obj)) { + bin_obj.reset(); + for (auto field : bin_obj) { + std::string_view key_str, rel; + if (!field.unescaped_key().get(key_str) && + !field.value().get_string().get(rel)) { + fprintf(stderr, + " %.*s: %.*s\n", + static_cast(key_str.size()), + key_str.data(), + static_cast(rel.size()), + rel.data()); + } + } } } } - result->exit_code_ = ExitCode::kGenericUserError; - return; + } else { + fprintf(stderr, "No bins defined in %s\n", path.string().c_str()); } - auto runner = ProcessRunner( - result, path, command_id, command, path_env_var, positional_args); - runner.Run(); + result->exit_code_ = ExitCode::kGenericUserError; } // GetPositionalArgs returns the positional arguments from the command line. diff --git a/test/fixtures/run-script/bin-string/package.json b/test/fixtures/run-script/bin-string/package.json new file mode 100644 index 00000000000000..92d73e96904c32 --- /dev/null +++ b/test/fixtures/run-script/bin-string/package.json @@ -0,0 +1,4 @@ +{ + "name": "bin-test", + "bin": "../test.js" +} diff --git a/test/fixtures/run-script/cannot-find-script/package.json b/test/fixtures/run-script/cannot-find-script-and-bin/package.json similarity index 100% rename from test/fixtures/run-script/cannot-find-script/package.json rename to test/fixtures/run-script/cannot-find-script-and-bin/package.json diff --git a/test/fixtures/run-script/invalid-bin-value/package.json b/test/fixtures/run-script/invalid-bin-value/package.json new file mode 100644 index 00000000000000..aab024c0cd33d8 --- /dev/null +++ b/test/fixtures/run-script/invalid-bin-value/package.json @@ -0,0 +1,5 @@ +{ + "bin": { + "invalid-bin": 2 + } +} diff --git a/test/fixtures/run-script/invalid-schema/package.json b/test/fixtures/run-script/invalid-schema/package.json index 59dfa8a201b587..88f247c17237d9 100644 --- a/test/fixtures/run-script/invalid-schema/package.json +++ b/test/fixtures/run-script/invalid-schema/package.json @@ -1,4 +1,5 @@ { + "bin": 1, "scripts": { "array": [], "boolean": true, diff --git a/test/fixtures/run-script/package.json b/test/fixtures/run-script/package.json index 138f47f2f97408..de23df3a9bebad 100644 --- a/test/fixtures/run-script/package.json +++ b/test/fixtures/run-script/package.json @@ -1,4 +1,10 @@ { + "bin": { + "bin-test": "./test.js", + "test": "./test.js", + "bin-ada": "ada", + "bin-ada-windows": "ada.bat" + }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "ada": "ada", diff --git a/test/fixtures/run-script/test.js b/test/fixtures/run-script/test.js new file mode 100755 index 00000000000000..267566a41b7312 --- /dev/null +++ b/test/fixtures/run-script/test.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +console.log('bin-test script'); diff --git a/test/message/node_run_non_existent.out b/test/message/node_run_non_existent.out index 06b2ab0bb16a9a..81c119637e5c21 100644 --- a/test/message/node_run_non_existent.out +++ b/test/message/node_run_non_existent.out @@ -1,6 +1,6 @@ -Missing script: "non-existent-command" for * +Unknown script or bin entry "non-existent-command" for * -Available scripts are: +Available scripts: test: echo "Error: no test specified" && exit 1 ada: ada ada-windows: ada.bat @@ -14,3 +14,9 @@ Available scripts are: special-env-variables-windows: special-env-variables.bat pwd: pwd pwd-windows: cd + +Available bins: + bin-test: ./test.js + test: ./test.js + bin-ada: ada + bin-ada-windows: ada.bat diff --git a/test/parallel/test-node-run.js b/test/parallel/test-node-run.js index 26295256849702..e605012c2e4741 100644 --- a/test/parallel/test-node-run.js +++ b/test/parallel/test-node-run.js @@ -9,6 +9,10 @@ const assert = require('node:assert'); const fixtures = require('../common/fixtures'); const envSuffix = common.isWindows ? '-windows' : ''; +const path = require('node:path'); +const nodeDir = path.dirname(process.execPath); +const env = { ...process.env, PATH: `${nodeDir}${path.delimiter}${process.env.PATH}` }; + describe('node --run [command]', () => { it('returns error on non-existent file', async () => { const child = await common.spawnPromisified( @@ -25,6 +29,7 @@ describe('node --run [command]', () => { it('runs a valid command', async () => { // Run a script that just log `no test specified` + // Scripts take precedence over bins const child = await common.spawnPromisified( process.execPath, [ '--run', 'test', '--no-warnings'], @@ -212,14 +217,76 @@ describe('node --run [command]', () => { assert.strictEqual(child.code, 1); }); - it('returns error when there is no "scripts" field file', async () => { + it('returns error when there is no "scripts" and "bin" fields in file', async () => { const child = await common.spawnPromisified( process.execPath, [ '--run', 'test'], - { cwd: fixtures.path('run-script/cannot-find-script') }, + { cwd: fixtures.path('run-script/cannot-find-script-and-bin') }, ); - assert.match(child.stderr, /Can't find "scripts" field in/); + assert.match(child.stderr, /Can't find "scripts" or "bin" fields in/); assert.strictEqual(child.stdout, ''); assert.strictEqual(child.code, 1); }); + + it('print avilables scripts and bins when command not found', async () => { + const child = await common.spawnPromisified( + process.execPath, + [ '--run', 'tmp'], + { cwd: fixtures.path('run-script') }, + ); + assert.match(child.stderr, /Unknown script or bin entry "tmp"/); + assert.match(child.stderr, /Available scripts:\n/); + assert.match(child.stderr, /ada: ada\n/); + assert.match(child.stderr, /Available bins:\n/); + assert.match(child.stderr, /bin-test: \.\/test\.js\n/); + }); + + describe('Bin scripts use cases', () => { + it('runs a bin from package.json object format', async () => { + const child = await common.spawnPromisified( + process.execPath, + ['--run', 'bin-test'], + { cwd: fixtures.path('run-script'), env }, + ); + assert.match(child.stdout, /bin-test script/); + }); + + it('handles error with invalid bin value in object format', async () => { + const child = await common.spawnPromisified( + process.execPath, + ['--run', 'invalid-bin'], + { cwd: fixtures.path('run-script/invalid-bin-value'), env }, + ); + assert.match(child.stderr, /Bin "invalid-bin" is unexpectedly not a string/); + }); + + it('runs a bin from package.json string format', async () => { + const child = await common.spawnPromisified( + process.execPath, + ['--run', 'bin-test'], + { cwd: fixtures.path('run-script/bin-string'), env }, + ); + assert.match(child.stdout, /bin-test script/); + }); + + it('handles error with invalid bin value in string format', async () => { + const child = await common.spawnPromisified( + process.execPath, + ['--run', 'invalid-bin'], + { cwd: fixtures.path('run-script/invalid-schema'), env } + ); + assert.match(child.stderr, /Bin "invalid-bin" is unexpectedly not a string/); + }); + + it('adds node_modules/.bin to path', async () => { + const child = await common.spawnPromisified( + process.execPath, + ['--run', `bin-ada${envSuffix}`], + { cwd: fixtures.path('run-script') }, + ); + assert.match(child.stdout, /06062023/); + assert.strictEqual(child.stderr, ''); + assert.strictEqual(child.code, 0); + }); + }); });