mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
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:
parent
e42ca5c1a9
commit
0c5fa57bc7
@ -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
|
||||
`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
|
||||
the `package.json` in the current folder:
|
||||
|
||||
|
@ -149,7 +149,7 @@ std::string EscapeShell(const std::string_view input) {
|
||||
#endif
|
||||
}
|
||||
|
||||
static const std::string_view forbidden_characters =
|
||||
static constexpr std::string_view forbidden_characters =
|
||||
"[\t\n\r \"#$&'()*;<>?\\\\`|~]";
|
||||
|
||||
// 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,
|
||||
int64_t exit_status,
|
||||
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);
|
||||
self->OnExit(exit_status, term_signal);
|
||||
}
|
||||
@ -205,6 +205,9 @@ void ProcessRunner::OnExit(int64_t exit_status, int term_signal) {
|
||||
}
|
||||
|
||||
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_)) {
|
||||
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}};
|
||||
}
|
||||
|
||||
void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
||||
void RunTask(const std::shared_ptr<InitializationResultImpl>& result,
|
||||
std::string_view command_id,
|
||||
const std::vector<std::string_view>& positional_args) {
|
||||
auto cwd = std::filesystem::current_path();
|
||||
auto package_json = FindPackageJson(cwd);
|
||||
|
||||
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;
|
||||
return;
|
||||
}
|
||||
@ -267,11 +272,21 @@ void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
||||
simdjson::ondemand::parser json_parser;
|
||||
simdjson::ondemand::document document;
|
||||
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 (error || document.get_object().get(main_object)) {
|
||||
fprintf(stderr, "Can't parse package.json\n");
|
||||
if (auto root_error = document.get_object().get(main_object)) {
|
||||
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;
|
||||
return;
|
||||
}
|
||||
@ -279,18 +294,28 @@ void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
||||
// 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 package.json\n");
|
||||
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 (scripts_object[command_id].get_string().get(command)) {
|
||||
if (auto command_error =
|
||||
scripts_object[command_id].get_string().get(command)) {
|
||||
if (command_error == simdjson::error_code::INCORRECT_TYPE) {
|
||||
fprintf(stderr,
|
||||
"Missing script: \"%.*s\"\n\n",
|
||||
"Script \"%.*s\" is unexpectedly not a string for %s\n\n",
|
||||
static_cast<int>(command_id.size()),
|
||||
command_id.data());
|
||||
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
|
||||
@ -309,6 +334,7 @@ void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
||||
value_str.data());
|
||||
}
|
||||
}
|
||||
}
|
||||
result->exit_code_ = ExitCode::kGenericUserError;
|
||||
return;
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ class ProcessRunner {
|
||||
std::vector<std::string> env_vars_{};
|
||||
std::unique_ptr<char* []> env {}; // memory for options_.env
|
||||
std::unique_ptr<char* []> arg {}; // memory for options_.args
|
||||
std::string cwd;
|
||||
|
||||
// OnExit is the callback function that is called when the process exits.
|
||||
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>>
|
||||
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,
|
||||
const PositionalArgs& positional_args);
|
||||
PositionalArgs GetPositionalArgs(const std::vector<std::string>& args);
|
||||
|
3
test/fixtures/run-script/invalid-json/package.json
vendored
Normal file
3
test/fixtures/run-script/invalid-json/package.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"scripts": {},
|
||||
}
|
9
test/fixtures/run-script/invalid-schema/package.json
vendored
Normal file
9
test/fixtures/run-script/invalid-schema/package.json
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"scripts": {
|
||||
"array": [],
|
||||
"boolean": true,
|
||||
"null": null,
|
||||
"number": 1.0,
|
||||
"object": {}
|
||||
}
|
||||
}
|
1
test/fixtures/run-script/missing-scripts/package.json
vendored
Normal file
1
test/fixtures/run-script/missing-scripts/package.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{}
|
4
test/fixtures/run-script/package.json
vendored
4
test/fixtures/run-script/package.json
vendored
@ -10,6 +10,8 @@
|
||||
"path-env": "path-env",
|
||||
"path-env-windows": "path-env.bat",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
Missing script: "non-existent-command"
|
||||
Missing script: "non-existent-command" for *
|
||||
|
||||
Available scripts are:
|
||||
test: echo "Error: no test specified" && exit 1
|
||||
@ -12,3 +12,5 @@ Available scripts are:
|
||||
path-env-windows: path-env.bat
|
||||
special-env-variables: special-env-variables
|
||||
special-env-variables-windows: special-env-variables.bat
|
||||
pwd: pwd
|
||||
pwd-windows: cd
|
||||
|
@ -1,6 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
common.requireNoPackageJSONAbove();
|
||||
|
||||
const { it, describe } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
|
||||
@ -15,7 +17,6 @@ describe('node --run [command]', () => {
|
||||
{ cwd: __dirname },
|
||||
);
|
||||
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.code, 1);
|
||||
});
|
||||
@ -26,7 +27,9 @@ describe('node --run [command]', () => {
|
||||
[ '--no-warnings', '--run', 'test'],
|
||||
{ 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.code, 1);
|
||||
});
|
||||
@ -53,6 +56,101 @@ describe('node --run [command]', () => {
|
||||
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 () => {
|
||||
const child = await common.spawnPromisified(
|
||||
process.execPath,
|
||||
@ -120,7 +218,7 @@ describe('node --run [command]', () => {
|
||||
[ '--no-warnings', '--run', 'test'],
|
||||
{ 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.code, 1);
|
||||
});
|
||||
@ -131,7 +229,7 @@ describe('node --run [command]', () => {
|
||||
[ '--no-warnings', '--run', 'test'],
|
||||
{ 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.code, 1);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user