cli: ensure --run has proper pwd

PR-URL: https://github.com/nodejs/node/pull/54949
Refs: https://github.com/nodejs/node/pull/53600
Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
Reviewed-By: Vinícius Lourenço Claro Cardoso <contact@viniciusl.com.br>
This commit is contained in:
Yagiz Nizipli 2024-09-18 08:18:40 -04:00 committed by GitHub
parent e42ca5c1a9
commit 0c5fa57bc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 179 additions and 35 deletions

View File

@ -2110,6 +2110,8 @@ the current directory, to the `PATH` in order to execute the binaries from
different folders where multiple `node_modules` directories are present, if different folders where multiple `node_modules` directories are present, if
`ancestor-folder/node_modules/.bin` is a directory. `ancestor-folder/node_modules/.bin` is a directory.
`--run` executes the command in the directory containing the related `package.json`.
For example, the following command will run the `test` script of For example, the following command will run the `test` script of
the `package.json` in the current folder: the `package.json` in the current folder:

View File

@ -149,7 +149,7 @@ std::string EscapeShell(const std::string_view input) {
#endif #endif
} }
static const std::string_view forbidden_characters = static constexpr std::string_view forbidden_characters =
"[\t\n\r \"#$&'()*;<>?\\\\`|~]"; "[\t\n\r \"#$&'()*;<>?\\\\`|~]";
// Check if input contains any forbidden characters // Check if input contains any forbidden characters
@ -191,7 +191,7 @@ std::string EscapeShell(const std::string_view input) {
void ProcessRunner::ExitCallback(uv_process_t* handle, void ProcessRunner::ExitCallback(uv_process_t* handle,
int64_t exit_status, int64_t exit_status,
int term_signal) { int term_signal) {
auto self = reinterpret_cast<ProcessRunner*>(handle->data); const auto self = static_cast<ProcessRunner*>(handle->data);
uv_close(reinterpret_cast<uv_handle_t*>(handle), nullptr); uv_close(reinterpret_cast<uv_handle_t*>(handle), nullptr);
self->OnExit(exit_status, term_signal); self->OnExit(exit_status, term_signal);
} }
@ -205,6 +205,9 @@ void ProcessRunner::OnExit(int64_t exit_status, int term_signal) {
} }
void ProcessRunner::Run() { void ProcessRunner::Run() {
// keeps the string alive until destructor
cwd = package_json_path_.parent_path().string();
options_.cwd = cwd.c_str();
if (int r = uv_spawn(loop_, &process_, &options_)) { if (int r = uv_spawn(loop_, &process_, &options_)) {
fprintf(stderr, "Error: %s\n", uv_strerror(r)); fprintf(stderr, "Error: %s\n", uv_strerror(r));
} }
@ -246,14 +249,16 @@ FindPackageJson(const std::filesystem::path& cwd) {
return {{package_json_path, raw_content, path_env_var}}; return {{package_json_path, raw_content, path_env_var}};
} }
void RunTask(std::shared_ptr<InitializationResultImpl> result, void RunTask(const std::shared_ptr<InitializationResultImpl>& result,
std::string_view command_id, std::string_view command_id,
const std::vector<std::string_view>& positional_args) { const std::vector<std::string_view>& positional_args) {
auto cwd = std::filesystem::current_path(); auto cwd = std::filesystem::current_path();
auto package_json = FindPackageJson(cwd); auto package_json = FindPackageJson(cwd);
if (!package_json.has_value()) { if (!package_json.has_value()) {
fprintf(stderr, "Can't read package.json\n"); fprintf(stderr,
"Can't find package.json for directory %s\n",
cwd.string().c_str());
result->exit_code_ = ExitCode::kGenericUserError; result->exit_code_ = ExitCode::kGenericUserError;
return; return;
} }
@ -267,11 +272,21 @@ void RunTask(std::shared_ptr<InitializationResultImpl> result,
simdjson::ondemand::parser json_parser; simdjson::ondemand::parser json_parser;
simdjson::ondemand::document document; simdjson::ondemand::document document;
simdjson::ondemand::object main_object; simdjson::ondemand::object main_object;
simdjson::error_code error = json_parser.iterate(raw_json).get(document);
if (json_parser.iterate(raw_json).get(document)) {
fprintf(stderr, "Can't parse %s\n", path.string().c_str());
result->exit_code_ = ExitCode::kGenericUserError;
return;
}
// If document is not an object, throw an error. // If document is not an object, throw an error.
if (error || document.get_object().get(main_object)) { if (auto root_error = document.get_object().get(main_object)) {
fprintf(stderr, "Can't parse package.json\n"); if (root_error == simdjson::error_code::INCORRECT_TYPE) {
fprintf(stderr,
"Root value unexpected not an object for %s\n\n",
path.string().c_str());
} else {
fprintf(stderr, "Can't parse %s\n", path.string().c_str());
}
result->exit_code_ = ExitCode::kGenericUserError; result->exit_code_ = ExitCode::kGenericUserError;
return; return;
} }
@ -279,34 +294,45 @@ void RunTask(std::shared_ptr<InitializationResultImpl> result,
// If package_json object doesn't have "scripts" field, throw an error. // If package_json object doesn't have "scripts" field, throw an error.
simdjson::ondemand::object scripts_object; simdjson::ondemand::object scripts_object;
if (main_object["scripts"].get_object().get(scripts_object)) { if (main_object["scripts"].get_object().get(scripts_object)) {
fprintf(stderr, "Can't find \"scripts\" field in package.json\n"); fprintf(
stderr, "Can't find \"scripts\" field in %s\n", path.string().c_str());
result->exit_code_ = ExitCode::kGenericUserError; result->exit_code_ = ExitCode::kGenericUserError;
return; return;
} }
// If the command_id is not found in the scripts object, throw an error. // If the command_id is not found in the scripts object, throw an error.
std::string_view command; std::string_view command;
if (scripts_object[command_id].get_string().get(command)) { if (auto command_error =
fprintf(stderr, scripts_object[command_id].get_string().get(command)) {
"Missing script: \"%.*s\"\n\n", if (command_error == simdjson::error_code::INCORRECT_TYPE) {
static_cast<int>(command_id.size()), fprintf(stderr,
command_id.data()); "Script \"%.*s\" is unexpectedly not a string for %s\n\n",
fprintf(stderr, "Available scripts are:\n"); static_cast<int>(command_id.size()),
command_id.data(),
path.string().c_str());
} else {
fprintf(stderr,
"Missing script: \"%.*s\" for %s\n\n",
static_cast<int>(command_id.size()),
command_id.data(),
path.string().c_str());
fprintf(stderr, "Available scripts are:\n");
// Reset the object to iterate over it again // Reset the object to iterate over it again
scripts_object.reset(); scripts_object.reset();
simdjson::ondemand::value value; simdjson::ondemand::value value;
for (auto field : scripts_object) { for (auto field : scripts_object) {
std::string_view key_str; std::string_view key_str;
std::string_view value_str; std::string_view value_str;
if (!field.unescaped_key().get(key_str) && !field.value().get(value) && if (!field.unescaped_key().get(key_str) && !field.value().get(value) &&
!value.get_string().get(value_str)) { !value.get_string().get(value_str)) {
fprintf(stderr, fprintf(stderr,
" %.*s: %.*s\n", " %.*s: %.*s\n",
static_cast<int>(key_str.size()), static_cast<int>(key_str.size()),
key_str.data(), key_str.data(),
static_cast<int>(value_str.size()), static_cast<int>(value_str.size()),
value_str.data()); value_str.data());
}
} }
} }
result->exit_code_ = ExitCode::kGenericUserError; result->exit_code_ = ExitCode::kGenericUserError;

View File

@ -44,6 +44,7 @@ class ProcessRunner {
std::vector<std::string> env_vars_{}; std::vector<std::string> env_vars_{};
std::unique_ptr<char* []> env {}; // memory for options_.env std::unique_ptr<char* []> env {}; // memory for options_.env
std::unique_ptr<char* []> arg {}; // memory for options_.args std::unique_ptr<char* []> arg {}; // memory for options_.args
std::string cwd;
// OnExit is the callback function that is called when the process exits. // OnExit is the callback function that is called when the process exits.
void OnExit(int64_t exit_status, int term_signal); void OnExit(int64_t exit_status, int term_signal);
@ -78,7 +79,7 @@ class ProcessRunner {
std::optional<std::tuple<std::filesystem::path, std::string, std::string>> std::optional<std::tuple<std::filesystem::path, std::string, std::string>>
FindPackageJson(const std::filesystem::path& cwd); FindPackageJson(const std::filesystem::path& cwd);
void RunTask(std::shared_ptr<InitializationResultImpl> result, void RunTask(const std::shared_ptr<InitializationResultImpl>& result,
std::string_view command_id, std::string_view command_id,
const PositionalArgs& positional_args); const PositionalArgs& positional_args);
PositionalArgs GetPositionalArgs(const std::vector<std::string>& args); PositionalArgs GetPositionalArgs(const std::vector<std::string>& args);

View File

@ -0,0 +1,3 @@
{
"scripts": {},
}

View File

@ -0,0 +1,9 @@
{
"scripts": {
"array": [],
"boolean": true,
"null": null,
"number": 1.0,
"object": {}
}
}

View File

@ -0,0 +1 @@
{}

View File

@ -10,6 +10,8 @@
"path-env": "path-env", "path-env": "path-env",
"path-env-windows": "path-env.bat", "path-env-windows": "path-env.bat",
"special-env-variables": "special-env-variables", "special-env-variables": "special-env-variables",
"special-env-variables-windows": "special-env-variables.bat" "special-env-variables-windows": "special-env-variables.bat",
"pwd": "pwd",
"pwd-windows": "cd"
} }
} }

View File

@ -1,4 +1,4 @@
Missing script: "non-existent-command" Missing script: "non-existent-command" for *
Available scripts are: Available scripts are:
test: echo "Error: no test specified" && exit 1 test: echo "Error: no test specified" && exit 1
@ -12,3 +12,5 @@ Available scripts are:
path-env-windows: path-env.bat path-env-windows: path-env.bat
special-env-variables: special-env-variables special-env-variables: special-env-variables
special-env-variables-windows: special-env-variables.bat special-env-variables-windows: special-env-variables.bat
pwd: pwd
pwd-windows: cd

View File

@ -1,6 +1,8 @@
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.requireNoPackageJSONAbove();
const { it, describe } = require('node:test'); const { it, describe } = require('node:test');
const assert = require('node:assert'); const assert = require('node:assert');
@ -15,7 +17,6 @@ describe('node --run [command]', () => {
{ cwd: __dirname }, { cwd: __dirname },
); );
assert.match(child.stderr, /ExperimentalWarning: Task runner is an experimental feature and might change at any time/); assert.match(child.stderr, /ExperimentalWarning: Task runner is an experimental feature and might change at any time/);
assert.match(child.stderr, /Can't read package\.json/);
assert.strictEqual(child.stdout, ''); assert.strictEqual(child.stdout, '');
assert.strictEqual(child.code, 1); assert.strictEqual(child.code, 1);
}); });
@ -26,7 +27,9 @@ describe('node --run [command]', () => {
[ '--no-warnings', '--run', 'test'], [ '--no-warnings', '--run', 'test'],
{ cwd: __dirname }, { cwd: __dirname },
); );
assert.match(child.stderr, /Can't read package\.json/); assert.match(child.stderr, /Can't find package\.json[\s\S]*/);
// Ensure we show the path that starting path for the search
assert(child.stderr.includes(__dirname));
assert.strictEqual(child.stdout, ''); assert.strictEqual(child.stdout, '');
assert.strictEqual(child.code, 1); assert.strictEqual(child.code, 1);
}); });
@ -53,6 +56,101 @@ describe('node --run [command]', () => {
assert.strictEqual(child.code, 0); assert.strictEqual(child.code, 0);
}); });
it('chdirs into package directory', async () => {
const child = await common.spawnPromisified(
process.execPath,
[ '--no-warnings', '--run', `pwd${envSuffix}`],
{ cwd: fixtures.path('run-script/sub-directory') },
);
assert.strictEqual(child.stdout.trim(), fixtures.path('run-script'));
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
});
it('includes actionable info when possible', async () => {
{
const child = await common.spawnPromisified(
process.execPath,
[ '--no-warnings', '--run', 'missing'],
{ cwd: fixtures.path('run-script') },
);
assert.strictEqual(child.stdout, '');
assert(child.stderr.includes(fixtures.path('run-script/package.json')));
assert(child.stderr.includes('no test specified'));
assert.strictEqual(child.code, 1);
}
{
const child = await common.spawnPromisified(
process.execPath,
[ '--no-warnings', '--run', 'test'],
{ cwd: fixtures.path('run-script/missing-scripts') },
);
assert.strictEqual(child.stdout, '');
assert(child.stderr.includes(fixtures.path('run-script/missing-scripts/package.json')));
assert.strictEqual(child.code, 1);
}
{
const child = await common.spawnPromisified(
process.execPath,
[ '--no-warnings', '--run', 'test'],
{ cwd: fixtures.path('run-script/invalid-json') },
);
assert.strictEqual(child.stdout, '');
assert(child.stderr.includes(fixtures.path('run-script/invalid-json/package.json')));
assert.strictEqual(child.code, 1);
}
{
const child = await common.spawnPromisified(
process.execPath,
[ '--no-warnings', '--run', 'array'],
{ cwd: fixtures.path('run-script/invalid-schema') },
);
assert.strictEqual(child.stdout, '');
assert(child.stderr.includes(fixtures.path('run-script/invalid-schema/package.json')));
assert.strictEqual(child.code, 1);
}
{
const child = await common.spawnPromisified(
process.execPath,
[ '--no-warnings', '--run', 'boolean'],
{ cwd: fixtures.path('run-script/invalid-schema') },
);
assert.strictEqual(child.stdout, '');
assert(child.stderr.includes(fixtures.path('run-script/invalid-schema/package.json')));
assert.strictEqual(child.code, 1);
}
{
const child = await common.spawnPromisified(
process.execPath,
[ '--no-warnings', '--run', 'null'],
{ cwd: fixtures.path('run-script/invalid-schema') },
);
assert.strictEqual(child.stdout, '');
assert(child.stderr.includes(fixtures.path('run-script/invalid-schema/package.json')));
assert.strictEqual(child.code, 1);
}
{
const child = await common.spawnPromisified(
process.execPath,
[ '--no-warnings', '--run', 'number'],
{ cwd: fixtures.path('run-script/invalid-schema') },
);
assert.strictEqual(child.stdout, '');
assert(child.stderr.includes(fixtures.path('run-script/invalid-schema/package.json')));
assert.strictEqual(child.code, 1);
}
{
const child = await common.spawnPromisified(
process.execPath,
[ '--no-warnings', '--run', 'object'],
{ cwd: fixtures.path('run-script/invalid-schema') },
);
assert.strictEqual(child.stdout, '');
assert(child.stderr.includes(fixtures.path('run-script/invalid-schema/package.json')));
assert.strictEqual(child.code, 1);
}
});
it('appends positional arguments', async () => { it('appends positional arguments', async () => {
const child = await common.spawnPromisified( const child = await common.spawnPromisified(
process.execPath, process.execPath,
@ -120,7 +218,7 @@ describe('node --run [command]', () => {
[ '--no-warnings', '--run', 'test'], [ '--no-warnings', '--run', 'test'],
{ cwd: fixtures.path('run-script/cannot-parse') }, { cwd: fixtures.path('run-script/cannot-parse') },
); );
assert.match(child.stderr, /Can't parse package\.json/); assert.match(child.stderr, /Can't parse/);
assert.strictEqual(child.stdout, ''); assert.strictEqual(child.stdout, '');
assert.strictEqual(child.code, 1); assert.strictEqual(child.code, 1);
}); });
@ -131,7 +229,7 @@ describe('node --run [command]', () => {
[ '--no-warnings', '--run', 'test'], [ '--no-warnings', '--run', 'test'],
{ cwd: fixtures.path('run-script/cannot-find-script') }, { cwd: fixtures.path('run-script/cannot-find-script') },
); );
assert.match(child.stderr, /Can't find "scripts" field in package\.json/); assert.match(child.stderr, /Can't find "scripts" field in/);
assert.strictEqual(child.stdout, ''); assert.strictEqual(child.stdout, '');
assert.strictEqual(child.code, 1); assert.strictEqual(child.code, 1);
}); });