Skip to content

Commit 94a5694

Browse files
committed
src: traverse parent folders while running --run
1 parent f05baff commit 94a5694

File tree

5 files changed

+121
-54
lines changed

5 files changed

+121
-54
lines changed

doc/api/cli.md

+11-6
Original file line numberDiff line numberDiff line change
@@ -1864,16 +1864,24 @@ changes:
18641864
- version: REPLACEME
18651865
pr-url: https://github.com/nodejs/node/pull/53058
18661866
description: NODE_RUN_PACKAGE_JSON_PATH environment variable is added.
1867+
- version: REPLACEME
1868+
pr-url: https://github.com/nodejs/node/pull/53154
1869+
description: Traverses up to the root directory and finds
1870+
a `package.json` file to run the command from, and updates
1871+
`PATH` environment variable accordingly.
18671872
-->
18681873

18691874
> Stability: 1.1 - Active development
18701875
18711876
This runs a specified command from a package.json's `"scripts"` object.
18721877
If no `"command"` is provided, it will list the available scripts.
18731878

1874-
`--run` prepends `./node_modules/.bin`, relative to the current
1875-
working directory, to the `PATH` in order to execute the binaries from
1876-
dependencies.
1879+
`--run` will traverse up to the root directory and finds a `package.json`
1880+
file to run the command from.
1881+
1882+
`--run` prepends `./node_modules/.bin` for each subdirectory of
1883+
the current directory, to the `PATH` in order to execute the binaries from
1884+
dependencies where multiple `node_modules` directories are present.
18771885

18781886
For example, the following command will run the `test` script of
18791887
the `package.json` in the current folder:
@@ -1898,9 +1906,6 @@ cases.
18981906
Some features of other `run` implementations that are intentionally excluded
18991907
are:
19001908

1901-
* Searching for `package.json` files outside the current folder.
1902-
* Prepending the `.bin` or `node_modules/.bin` paths of folders outside the
1903-
current folder.
19041909
* Running `pre` or `post` scripts in addition to the specified script.
19051910
* Defining package manager-specific environment variables.
19061911

src/node_task_runner.cc

+67-41
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,27 @@
11
#include "node_task_runner.h"
22
#include "util.h"
33

4-
#include <filesystem>
54
#include <regex> // NOLINT(build/c++11)
65

76
namespace node::task_runner {
87

98
#ifdef _WIN32
10-
static constexpr const char* bin_path = "\\node_modules\\.bin";
9+
static constexpr const char* env_var_separator = ";";
1110
#else
12-
static constexpr const char* bin_path = "/node_modules/.bin";
11+
static constexpr const char* env_var_separator = ":";
1312
#endif // _WIN32
1413

1514
ProcessRunner::ProcessRunner(std::shared_ptr<InitializationResultImpl> result,
16-
std::string_view package_json_path,
15+
const std::filesystem::path& package_json_path,
1716
std::string_view script_name,
1817
std::string_view command,
18+
std::string_view path_env_var,
1919
const PositionalArgs& positional_args) {
2020
memset(&options_, 0, sizeof(uv_process_options_t));
2121

22-
// Get the current working directory.
23-
char cwd[PATH_MAX_BYTES];
24-
size_t cwd_size = PATH_MAX_BYTES;
25-
CHECK_EQ(uv_cwd(cwd, &cwd_size), 0);
26-
CHECK_GT(cwd_size, 0);
27-
28-
#ifdef _WIN32
29-
std::string current_bin_path = cwd + std::string(bin_path) + ";";
30-
#else
31-
std::string current_bin_path = cwd + std::string(bin_path) + ":";
32-
#endif // _WIN32
22+
package_json_path_ = std::filesystem::path(package_json_path);
23+
script_name_ = std::string(script_name);
24+
path_env_var_ = path_env_var;
3325

3426
// Inherit stdin, stdout, and stderr from the parent process.
3527
options_.stdio_count = 3;
@@ -54,10 +46,7 @@ ProcessRunner::ProcessRunner(std::shared_ptr<InitializationResultImpl> result,
5446
// callback.
5547
process_.data = this;
5648

57-
SetEnvironmentVariables(current_bin_path,
58-
std::string_view(cwd, cwd_size),
59-
package_json_path,
60-
script_name);
49+
SetEnvironmentVariables();
6150

6251
std::string command_str(command);
6352

@@ -106,10 +95,7 @@ ProcessRunner::ProcessRunner(std::shared_ptr<InitializationResultImpl> result,
10695
options_.args[argc] = nullptr;
10796
}
10897

109-
void ProcessRunner::SetEnvironmentVariables(const std::string& current_bin_path,
110-
std::string_view cwd,
111-
std::string_view package_json_path,
112-
std::string_view script_name) {
98+
void ProcessRunner::SetEnvironmentVariables() {
11399
uv_env_item_t* env_items;
114100
int env_count;
115101
CHECK_EQ(0, uv_os_environ(&env_items, &env_count));
@@ -130,28 +116,21 @@ void ProcessRunner::SetEnvironmentVariables(const std::string& current_bin_path,
130116
#endif // _WIN32
131117

132118
if (StringEqualNoCase(name.c_str(), "path")) {
133-
// Add bin_path to the beginning of the PATH
134-
value = current_bin_path + value;
119+
// Add path env variable to the beginning of the PATH
120+
value = path_env_var_ + value;
135121
}
136122
env_vars_.push_back(name + "=" + value);
137123
}
138124
uv_os_free_environ(env_items, env_count);
139125

140126
// Add NODE_RUN_SCRIPT_NAME environment variable to the environment
141127
// to indicate which script is being run.
142-
env_vars_.push_back("NODE_RUN_SCRIPT_NAME=" + std::string(script_name));
128+
env_vars_.push_back("NODE_RUN_SCRIPT_NAME=" + script_name_);
143129

144130
// Add NODE_RUN_PACKAGE_JSON_PATH environment variable to the environment to
145131
// indicate which package.json is being processed.
146-
if (std::filesystem::path(package_json_path).is_absolute()) {
147-
// TODO(anonrig): Traverse up the directory tree until we find a
148-
// package.json
149-
env_vars_.push_back("NODE_RUN_PACKAGE_JSON_PATH=" +
150-
std::string(package_json_path));
151-
} else {
152-
auto path = std::filesystem::path(cwd) / std::string(package_json_path);
153-
env_vars_.push_back("NODE_RUN_PACKAGE_JSON_PATH=" + path.string());
154-
}
132+
env_vars_.push_back("NODE_RUN_PACKAGE_JSON_PATH=" +
133+
package_json_path_.string());
155134

156135
env = std::unique_ptr<char*[]>(new char*[env_vars_.size() + 1]);
157136
options_.env = env.get();
@@ -240,19 +219,66 @@ void ProcessRunner::Run() {
240219
uv_run(loop_, UV_RUN_DEFAULT);
241220
}
242221

222+
std::optional<std::tuple<std::filesystem::path, std::string, std::string>>
223+
FindPackageJson(const std::filesystem::path& cwd) {
224+
// Two different paths are created only for improving readability.
225+
// Otherwise, it is not needed.
226+
auto directory_path = cwd;
227+
auto package_json_path = directory_path / "package.json";
228+
std::string raw_content;
229+
std::string path_env_var =
230+
(directory_path / "node_modules" / ".bin").string() + env_var_separator;
231+
232+
USE(ReadFileSync(&raw_content, package_json_path.c_str()));
233+
234+
do {
235+
directory_path = directory_path.parent_path();
236+
237+
// Always append "node_modules/.bin" to the env var.
238+
// This is in par with existing package managers.
239+
path_env_var +=
240+
(directory_path / "node_modules" / ".bin").string() + env_var_separator;
241+
242+
// No need to exclude BOM since simdjson will skip it.
243+
if (raw_content.empty()) {
244+
package_json_path = directory_path / "package.json";
245+
USE(ReadFileSync(&raw_content, package_json_path.c_str()));
246+
}
247+
248+
// Traverse up to the root directory.
249+
// Note: root directory parent path is the root directory itself.
250+
// Hence, "/".parent_path() == "/".
251+
} while (directory_path != directory_path.parent_path());
252+
253+
// This means that there is no package.json until the root directory.
254+
// In this case, we just return nullopt, which will terminate the process..
255+
if (raw_content.empty()) {
256+
return std::nullopt;
257+
}
258+
259+
return std::tuple(package_json_path, raw_content, path_env_var);
260+
}
261+
243262
void RunTask(std::shared_ptr<InitializationResultImpl> result,
244263
std::string_view command_id,
245264
const std::vector<std::string_view>& positional_args) {
246-
std::string_view path = "package.json";
247-
std::string raw_json;
265+
auto cwd = std::filesystem::current_path();
266+
auto package_json = FindPackageJson(cwd);
248267

249-
// No need to exclude BOM since simdjson will skip it.
250-
if (ReadFileSync(&raw_json, path.data()) < 0) {
268+
if (!package_json.has_value()) {
251269
fprintf(stderr, "Can't read package.json\n");
252270
result->exit_code_ = ExitCode::kGenericUserError;
253271
return;
254272
}
255273

274+
// Path to the package.json file.
275+
auto path = std::get<0>(*package_json);
276+
// Raw content of the package.json file.
277+
auto raw_json = std::get<1>(*package_json);
278+
// This represents the `PATH` environment variable.
279+
// It always ends with ";" or ":" depending on the platform.
280+
auto path_env_var = std::get<2>(*package_json);
281+
256282
simdjson::ondemand::parser json_parser;
257283
simdjson::ondemand::document document;
258284
simdjson::ondemand::object main_object;
@@ -302,8 +328,8 @@ void RunTask(std::shared_ptr<InitializationResultImpl> result,
302328
return;
303329
}
304330

305-
auto runner =
306-
ProcessRunner(result, path, command_id, command, positional_args);
331+
auto runner = ProcessRunner(
332+
result, path, command_id, command, path_env_var, positional_args);
307333
runner.Run();
308334
}
309335

src/node_task_runner.h

+28-6
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
#include "spawn_sync.h"
99
#include "uv.h"
1010

11+
#include <filesystem>
1112
#include <optional>
1213
#include <string_view>
14+
#include <tuple>
1315

1416
namespace node {
1517
namespace task_runner {
@@ -23,9 +25,10 @@ using PositionalArgs = std::vector<std::string_view>;
2325
class ProcessRunner {
2426
public:
2527
ProcessRunner(std::shared_ptr<InitializationResultImpl> result,
26-
std::string_view package_json_path,
28+
const std::filesystem::path& package_json_path,
2729
std::string_view script_name,
28-
std::string_view command_id,
30+
std::string_view command,
31+
std::string_view path_env_var,
2932
const PositionalArgs& positional_args);
3033
void Run();
3134
static void ExitCallback(uv_process_t* req,
@@ -45,18 +48,37 @@ class ProcessRunner {
4548

4649
// OnExit is the callback function that is called when the process exits.
4750
void OnExit(int64_t exit_status, int term_signal);
48-
void SetEnvironmentVariables(const std::string& bin_path,
49-
std::string_view cwd,
50-
std::string_view package_json_path,
51-
std::string_view script_name);
51+
void SetEnvironmentVariables();
5252

5353
#ifdef _WIN32
5454
std::string file_ = "cmd.exe";
5555
#else
5656
std::string file_ = "/bin/sh";
5757
#endif // _WIN32
58+
59+
// Represents the absolute path to the package.json file.
60+
std::filesystem::path package_json_path_;
61+
// Represents the name of the script that is being run.
62+
std::string script_name_;
63+
// Represents PATH environment variable that contains
64+
// all subdirectory paths appended with node_modules/.bin suffix.
65+
std::string path_env_var_;
5866
};
5967

68+
// This function traverses up to the root directory.
69+
// While traversing up, if it finds a package.json file, it reads its content.
70+
// If it cannot find a package.json file, it returns std::nullopt.
71+
// Otherwise, it returns a tuple of:
72+
// - the path to the package.json file
73+
// - package.json file content
74+
// - `path_env_var` variable
75+
//
76+
// For example, on POSIX, it returns the following for `path_env_var`,
77+
// if the current directory is `/anonrig`:
78+
// `/anonrig/node_modules/.bin:/node_modules/.bin`
79+
std::optional<std::tuple<std::filesystem::path, std::string, std::string>>
80+
FindPackageJson(const std::filesystem::path& cwd);
81+
6082
void RunTask(std::shared_ptr<InitializationResultImpl> result,
6183
std::string_view command_id,
6284
const PositionalArgs& positional_args);

test/fixtures/run-script/sub-directory/.gitkeep

Whitespace-only changes.

test/parallel/test-node-run.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,15 @@ describe('node run [command]', () => {
7070
assert.strictEqual(child.code, 0);
7171
});
7272

73-
it('should set PATH environment variable to node_modules/.bin', async () => {
73+
it('should set PATH environment variable with paths appended with node_modules/.bin', async () => {
7474
const child = await common.spawnPromisified(
7575
process.execPath,
7676
[ '--no-warnings', '--run', `path-env${envSuffix}`],
7777
{ cwd: fixtures.path('run-script') },
7878
);
7979
assert.ok(child.stdout.includes(fixtures.path('run-script/node_modules/.bin')));
80+
// The following test ensures that we traverse up to the root directory.
81+
assert.ok(child.stdout.includes(fixtures.path('node_modules/.bin')));
8082
assert.strictEqual(child.stderr, '');
8183
assert.strictEqual(child.code, 0);
8284
});
@@ -94,4 +96,16 @@ describe('node run [command]', () => {
9496
assert.strictEqual(child.stderr, '');
9597
assert.strictEqual(child.code, 0);
9698
});
99+
100+
it('will search subdirectories for a package.json file', async () => {
101+
const packageJsonPath = fixtures.path('run-script/package.json');
102+
const child = await common.spawnPromisified(
103+
process.execPath,
104+
[ '--no-warnings', '--run', `special-env-variables${envSuffix}`],
105+
{ cwd: fixtures.path('run-script/sub-directory') },
106+
);
107+
assert.ok(child.stdout.includes(packageJsonPath));
108+
assert.strictEqual(child.stderr, '');
109+
assert.strictEqual(child.code, 0);
110+
});
97111
});

0 commit comments

Comments
 (0)