| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 133cae0 commit 5a41bcf
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1957,16 +1957,25 @@ changes: | |||
| 1957 | 1957 | - version: REPLACEME | |
| 1958 | 1958 | pr-url: https://github.com/nodejs/node/pull/53058 | |
| 1959 | 1959 | description: NODE_RUN_PACKAGE_JSON_PATH environment variable is added. | |
| 1960 | + - version: REPLACEME | ||
| 1961 | + pr-url: https://github.com/nodejs/node/pull/53154 | ||
| 1962 | + description: Traverses up to the root directory and finds | ||
| 1963 | + a `package.json` file to run the command from, and updates | ||
| 1964 | + `PATH` environment variable accordingly. | ||
| 1960 | 1965 | --> | |
| 1961 | 1966 | ||
| 1962 | 1967 | > Stability: 1.1 - Active development | |
| 1963 | 1968 | ||
| 1964 | 1969 | This runs a specified command from a package.json's `"scripts"` object. | |
| 1965 | 1970 | If no `"command"` is provided, it will list the available scripts. | |
| 1966 | 1971 | ||
| 1967 | - `--run` prepends `./node_modules/.bin`, relative to the current | ||
| 1968 | - working directory, to the `PATH` in order to execute the binaries from | ||
| 1969 | - dependencies. | ||
| 1972 | + `--run` will traverse up to the root directory and finds a `package.json` | ||
| 1973 | + file to run the command from. | ||
| 1974 | + | ||
| 1975 | + `--run` prepends `./node_modules/.bin` for each ancestor of | ||
| 1976 | + the current directory, to the `PATH` in order to execute the binaries from | ||
| 1977 | + different folders where multiple `node_modules` directories are present, if | ||
| 1978 | + `ancestor-folder/node_modules/.bin` is a directory. | ||
| 1970 | 1979 | ||
| 1971 | 1980 | For example, the following command will run the `test` script of | |
| 1972 | 1981 | the `package.json` in the current folder: | |
@@ -1991,9 +2000,6 @@ cases. | |||
| 1991 | 2000 | Some features of other `run` implementations that are intentionally excluded | |
| 1992 | 2001 | are: | |
| 1993 | 2002 | ||
| 1994 | - * Searching for `package.json` files outside the current folder. | ||
| 1995 | - * Prepending the `.bin` or `node_modules/.bin` paths of folders outside the | ||
| 1996 | - current folder. | ||
| 1997 | 2003 | * Running `pre` or `post` scripts in addition to the specified script. | |
| 1998 | 2004 | * Defining package manager-specific environment variables. | |
| 1999 | 2005 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,36 +1,28 @@ | |||
| 1 | 1 | #include "node_task_runner.h" | |
| 2 | 2 | #include "util.h" | |
| 3 | 3 | ||
| 4 | - #include <filesystem> | ||
| 5 | 4 | #include <regex> // NOLINT(build/c++11) | |
| 6 | 5 | ||
| 7 | 6 | namespace node::task_runner { | |
| 8 | 7 | ||
| 9 | 8 | #ifdef _WIN32 | |
| 10 | - static constexpr const char* bin_path = "\\node_modules\\.bin"; | ||
| 9 | + static constexpr const char* env_var_separator = ";"; | ||
| 11 | 10 | #else | |
| 12 | - static constexpr const char* bin_path = "/node_modules/.bin"; | ||
| 11 | + static constexpr const char* env_var_separator = ":"; | ||
| 13 | 12 | #endif // _WIN32 | |
| 14 | 13 | ||
| 15 | 14 | ProcessRunner::ProcessRunner(std::shared_ptr<InitializationResultImpl> result, | |
| 16 | - std::string_view package_json_path, | ||
| 15 | + const std::filesystem::path& package_json_path, | ||
| 17 | 16 | std::string_view script_name, | |
| 18 | 17 | std::string_view command, | |
| 19 | - const PositionalArgs& positional_args) { | ||
| 18 | + std::string_view path_env_var, | ||
| 19 | + const PositionalArgs& positional_args) | ||
| 20 | + : init_result(std::move(result)), | ||
| 21 | + package_json_path_(package_json_path), | ||
| 22 | + script_name_(script_name), | ||
| 23 | + path_env_var_(path_env_var) { | ||
| 20 | 24 | memset(&options_, 0, sizeof(uv_process_options_t)); | |
| 21 | 25 | ||
| 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 | ||
| 33 | - | ||
| 34 | 26 | // Inherit stdin, stdout, and stderr from the parent process. | |
| 35 | 27 | options_.stdio_count = 3; | |
| 36 | 28 | child_stdio[0].flags = UV_INHERIT_FD; | |
@@ -46,18 +38,13 @@ ProcessRunner::ProcessRunner(std::shared_ptr<InitializationResultImpl> result, | |||
| 46 | 38 | options_.flags |= UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; | |
| 47 | 39 | #endif | |
| 48 | 40 | ||
| 49 | - init_result = std::move(result); | ||
| 50 | - | ||
| 51 | 41 | // Set the process handle data to this class instance. | |
| 52 | 42 | // This is used to access the class instance from the OnExit callback. | |
| 53 | 43 | // It is required because libuv doesn't allow passing lambda functions as a | |
| 54 | 44 | // callback. | |
| 55 | 45 | process_.data = this; | |
| 56 | 46 | ||
| 57 | - SetEnvironmentVariables(current_bin_path, | ||
| 58 | - std::string_view(cwd, cwd_size), | ||
| 59 | - package_json_path, | ||
| 60 | - script_name); | ||
| 47 | + SetEnvironmentVariables(); | ||
| 61 | 48 | ||
| 62 | 49 | std::string command_str(command); | |
| 63 | 50 | ||
@@ -73,12 +60,7 @@ ProcessRunner::ProcessRunner(std::shared_ptr<InitializationResultImpl> result, | |||
| 73 | 60 | } | |
| 74 | 61 | ||
| 75 | 62 | #ifdef _WIN32 | |
| 76 | - // We check whether file_ ends with cmd.exe in a case-insensitive manner. | ||
| 77 | - // C++20 provides ends_with, but we roll our own for compatibility. | ||
| 78 | - const char* cmdexe = "cmd.exe"; | ||
| 79 | - if (file_.size() >= strlen(cmdexe) && | ||
| 80 | - StringEqualNoCase(cmdexe, | ||
| 81 | - file_.c_str() + file_.size() - strlen(cmdexe))) { | ||
| 63 | + if (file_.ends_with("cmd.exe")) { | ||
| 82 | 64 | // If the file is cmd.exe, use the following command line arguments: | |
| 83 | 65 | // "/c" Carries out the command and exit. | |
| 84 | 66 | // "/d" Disables execution of AutoRun commands. | |
@@ -106,10 +88,7 @@ ProcessRunner::ProcessRunner(std::shared_ptr<InitializationResultImpl> result, | |||
| 106 | 88 | options_.args[argc] = nullptr; | |
| 107 | 89 | } | |
| 108 | 90 | ||
| 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) { | ||
| 91 | + void ProcessRunner::SetEnvironmentVariables() { | ||
| 113 | 92 | uv_env_item_t* env_items; | |
| 114 | 93 | int env_count; | |
| 115 | 94 | CHECK_EQ(0, uv_os_environ(&env_items, &env_count)); | |
@@ -130,28 +109,21 @@ void ProcessRunner::SetEnvironmentVariables(const std::string& current_bin_path, | |||
| 130 | 109 | #endif // _WIN32 | |
| 131 | 110 | ||
| 132 | 111 | if (StringEqualNoCase(name.c_str(), "path")) { | |
| 133 | - // Add bin_path to the beginning of the PATH | ||
| 134 | - value = current_bin_path + value; | ||
| 112 | + // Add path env variable to the beginning of the PATH | ||
| 113 | + value = path_env_var_ + value; | ||
| 135 | 114 | } | |
| 136 | 115 | env_vars_.push_back(name + "=" + value); | |
| 137 | 116 | } | |
| 138 | 117 | uv_os_free_environ(env_items, env_count); | |
| 139 | 118 | ||
| 140 | 119 | // Add NODE_RUN_SCRIPT_NAME environment variable to the environment | |
| 141 | 120 | // to indicate which script is being run. | |
| 142 | - env_vars_.push_back("NODE_RUN_SCRIPT_NAME=" + std::string(script_name)); | ||
| 121 | + env_vars_.push_back("NODE_RUN_SCRIPT_NAME=" + script_name_); | ||
| 143 | 122 | ||
| 144 | 123 | // Add NODE_RUN_PACKAGE_JSON_PATH environment variable to the environment to | |
| 145 | 124 | // 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 | - } | ||
| 125 | + env_vars_.push_back("NODE_RUN_PACKAGE_JSON_PATH=" + | ||
| 126 | + package_json_path_.string()); | ||
| 155 | 127 | ||
| 156 | 128 | env = std::unique_ptr<char*[]>(new char*[env_vars_.size() + 1]); | |
| 157 | 129 | options_.env = env.get(); | |
@@ -240,19 +212,58 @@ void ProcessRunner::Run() { | |||
| 240 | 212 | uv_run(loop_, UV_RUN_DEFAULT); | |
| 241 | 213 | } | |
| 242 | 214 | ||
| 215 | + std::optional<std::tuple<std::filesystem::path, std::string, std::string>> | ||
| 216 | + FindPackageJson(const std::filesystem::path& cwd) { | ||
| 217 | + auto package_json_path = cwd / "package.json"; | ||
| 218 | + std::string raw_content; | ||
| 219 | + std::string path_env_var; | ||
| 220 | + auto root_path = cwd.root_path(); | ||
| 221 | + | ||
| 222 | + for (auto directory_path = cwd; | ||
| 223 | + !std::filesystem::equivalent(root_path, directory_path); | ||
| 224 | + directory_path = directory_path.parent_path()) { | ||
| 225 | + // Append "path/node_modules/.bin" to the env var, if it is a directory. | ||
| 226 | + auto node_modules_bin = directory_path / "node_modules" / ".bin"; | ||
| 227 | + if (std::filesystem::is_directory(node_modules_bin)) { | ||
| 228 | + path_env_var += node_modules_bin.string() + env_var_separator; | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + if (raw_content.empty()) { | ||
| 232 | + package_json_path = directory_path / "package.json"; | ||
| 233 | + // This is required for Windows because std::filesystem::path::c_str() | ||
| 234 | + // returns wchar_t* on Windows, and char* on other platforms. | ||
| 235 | + std::string contents = package_json_path.string(); | ||
| 236 | + USE(ReadFileSync(&raw_content, contents.c_str()) > 0); | ||
| 237 | + } | ||
| 238 | + } | ||
| 239 | + | ||
| 240 | + // This means that there is no package.json until the root directory. | ||
| 241 | + // In this case, we just return nullopt, which will terminate the process.. | ||
| 242 | + if (raw_content.empty()) { | ||
| 243 | + return std::nullopt; | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + return {{package_json_path, raw_content, path_env_var}}; | ||
| 247 | + } | ||
| 248 | + | ||
| 243 | 249 | void RunTask(std::shared_ptr<InitializationResultImpl> result, | |
| 244 | 250 | std::string_view command_id, | |
| 245 | 251 | const std::vector<std::string_view>& positional_args) { | |
| 246 | - std::string_view path = "package.json"; | ||
| 247 | - std::string raw_json; | ||
| 252 | + auto cwd = std::filesystem::current_path(); | ||
| 253 | + auto package_json = FindPackageJson(cwd); | ||
| 248 | 254 | ||
| 249 | - // No need to exclude BOM since simdjson will skip it. | ||
| 250 | - if (ReadFileSync(&raw_json, path.data()) < 0) { | ||
| 255 | + if (!package_json.has_value()) { | ||
| 251 | 256 | fprintf(stderr, "Can't read package.json\n"); | |
| 252 | 257 | result->exit_code_ = ExitCode::kGenericUserError; | |
| 253 | 258 | return; | |
| 254 | 259 | } | |
| 255 | 260 | ||
| 261 | + // - path: Path to the package.json file. | ||
| 262 | + // - raw_json: Raw content of the package.json file. | ||
| 263 | + // - path_env_var: This represents the `PATH` environment variable. | ||
| 264 | + // It always ends with ";" or ":" depending on the platform. | ||
| 265 | + auto [path, raw_json, path_env_var] = *package_json; | ||
| 266 | + | ||
| 256 | 267 | simdjson::ondemand::parser json_parser; | |
| 257 | 268 | simdjson::ondemand::document document; | |
| 258 | 269 | simdjson::ondemand::object main_object; | |
@@ -302,8 +313,8 @@ void RunTask(std::shared_ptr<InitializationResultImpl> result, | |||
| 302 | 313 | return; | |
| 303 | 314 | } | |
| 304 | 315 | ||
| 305 | - auto runner = | ||
| 306 | - ProcessRunner(result, path, command_id, command, positional_args); | ||
| 316 | + auto runner = ProcessRunner( | ||
| 317 | + result, path, command_id, command, path_env_var, positional_args); | ||
| 307 | 318 | runner.Run(); | |
| 308 | 319 | } | |
| 309 | 320 | ||
@@ -317,10 +328,11 @@ PositionalArgs GetPositionalArgs(const std::vector<std::string>& args) { | |||
| 317 | 328 | if (auto dash_dash = std::find(args.begin(), args.end(), "--"); | |
| 318 | 329 | dash_dash != args.end()) { | |
| 319 | 330 | PositionalArgs positional_args{}; | |
| 331 | + positional_args.reserve(args.size() - (dash_dash - args.begin())); | ||
| 320 | 332 | for (auto it = dash_dash + 1; it != args.end(); ++it) { | |
| 321 | 333 | // SAFETY: The following code is safe because the lifetime of the | |
| 322 | 334 | // arguments is guaranteed to be valid until the end of the task runner. | |
| 323 | - positional_args.push_back(std::string_view(it->c_str(), it->size())); | ||
| 335 | + positional_args.emplace_back(it->c_str(), it->size()); | ||
| 324 | 336 | } | |
| 325 | 337 | return positional_args; | |
| 326 | 338 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,11 +8,12 @@ | |||
| 8 | 8 | #include "spawn_sync.h" | |
| 9 | 9 | #include "uv.h" | |
| 10 | 10 | ||
| 11 | + #include <filesystem> | ||
| 11 | 12 | #include <optional> | |
| 12 | 13 | #include <string_view> | |
| 14 | + #include <tuple> | ||
| 13 | 15 | ||
| 14 | - namespace node { | ||
| 15 | - namespace task_runner { | ||
| 16 | + namespace node::task_runner { | ||
| 16 | 17 | ||
| 17 | 18 | using PositionalArgs = std::vector<std::string_view>; | |
| 18 | 19 | ||
@@ -23,9 +24,10 @@ using PositionalArgs = std::vector<std::string_view>; | |||
| 23 | 24 | class ProcessRunner { | |
| 24 | 25 | public: | |
| 25 | 26 | ProcessRunner(std::shared_ptr<InitializationResultImpl> result, | |
| 26 | - std::string_view package_json_path, | ||
| 27 | + const std::filesystem::path& package_json_path, | ||
| 27 | 28 | std::string_view script_name, | |
| 28 | - std::string_view command_id, | ||
| 29 | + std::string_view command, | ||
| 30 | + std::string_view path_env_var, | ||
| 29 | 31 | const PositionalArgs& positional_args); | |
| 30 | 32 | void Run(); | |
| 31 | 33 | static void ExitCallback(uv_process_t* req, | |
@@ -45,26 +47,44 @@ class ProcessRunner { | |||
| 45 | 47 | ||
| 46 | 48 | // OnExit is the callback function that is called when the process exits. | |
| 47 | 49 | 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); | ||
| 50 | + void SetEnvironmentVariables(); | ||
| 52 | 51 | ||
| 53 | 52 | #ifdef _WIN32 | |
| 54 | 53 | std::string file_ = "cmd.exe"; | |
| 55 | 54 | #else | |
| 56 | 55 | std::string file_ = "/bin/sh"; | |
| 57 | 56 | #endif // _WIN32 | |
| 57 | + | ||
| 58 | + // Represents the absolute path to the package.json file. | ||
| 59 | + std::filesystem::path package_json_path_; | ||
| 60 | + // Represents the name of the script that is being run. | ||
| 61 | + std::string script_name_; | ||
| 62 | + // Represents PATH environment variable that contains | ||
| 63 | + // all subdirectory paths appended with node_modules/.bin suffix. | ||
| 64 | + std::string path_env_var_; | ||
| 58 | 65 | }; | |
| 59 | 66 | ||
| 67 | + // This function traverses up to the root directory. | ||
| 68 | + // While traversing up, if it finds a package.json file, it reads its content. | ||
| 69 | + // If it cannot find a package.json file, it returns std::nullopt. | ||
| 70 | + // Otherwise, it returns a tuple of: | ||
| 71 | + // - the path to the package.json file | ||
| 72 | + // - package.json file content | ||
| 73 | + // - `path_env_var` variable | ||
| 74 | + // | ||
| 75 | + // For example, on POSIX, it returns the following for `path_env_var`, | ||
| 76 | + // if the current directory is `/anonrig`: | ||
| 77 | + // `/anonrig/node_modules/.bin:/node_modules/.bin` | ||
| 78 | + std::optional<std::tuple<std::filesystem::path, std::string, std::string>> | ||
| 79 | + FindPackageJson(const std::filesystem::path& cwd); | ||
| 80 | + | ||
| 60 | 81 | void RunTask(std::shared_ptr<InitializationResultImpl> result, | |
| 61 | 82 | std::string_view command_id, | |
| 62 | 83 | const PositionalArgs& positional_args); | |
| 63 | 84 | PositionalArgs GetPositionalArgs(const std::vector<std::string>& args); | |
| 64 | - std::string EscapeShell(const std::string_view command); | ||
| 85 | + std::string EscapeShell(std::string_view command); | ||
| 65 | 86 | ||
| 66 | - } // namespace task_runner | ||
| 67 | - } // namespace node | ||
| 87 | + } // namespace node::task_runner | ||
| 68 | 88 | ||
| 69 | 89 | #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS | |
| 70 | 90 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,4 +1,3 @@ | |||
| 1 | - // Flags: --expose-internals | ||
| 2 | 1 | 'use strict'; | |
| 3 | 2 | ||
| 4 | 3 | const common = require('../common'); | |
@@ -8,7 +7,7 @@ const assert = require('node:assert'); | |||
| 8 | 7 | const fixtures = require('../common/fixtures'); | |
| 9 | 8 | const envSuffix = common.isWindows ? '-windows' : ''; | |
| 10 | 9 | ||
| 11 | - describe('node run [command]', () => { | ||
| 10 | + describe('node --run [command]', () => { | ||
| 12 | 11 | it('should emit experimental warning', async () => { | |
| 13 | 12 | const child = await common.spawnPromisified( | |
| 14 | 13 | process.execPath, | |
@@ -70,13 +69,21 @@ describe('node run [command]', () => { | |||
| 70 | 69 | assert.strictEqual(child.code, 0); | |
| 71 | 70 | }); | |
| 72 | 71 | ||
| 73 | - it('should set PATH environment variable to node_modules/.bin', async () => { | ||
| 72 | + it('should set PATH environment variable with paths appended with node_modules/.bin', async () => { | ||
| 74 | 73 | const child = await common.spawnPromisified( | |
| 75 | 74 | process.execPath, | |
| 76 | 75 | [ '--no-warnings', '--run', `path-env${envSuffix}`], | |
| 77 | - { cwd: fixtures.path('run-script') }, | ||
| 76 | + { cwd: fixtures.path('run-script/sub-directory') }, | ||
| 78 | 77 | ); | |
| 79 | 78 | assert.ok(child.stdout.includes(fixtures.path('run-script/node_modules/.bin'))); | |
| 79 | + | ||
| 80 | + // The following test ensures that we do not add paths that does not contain | ||
| 81 | + // "node_modules/.bin" | ||
| 82 | + assert.ok(!child.stdout.includes(fixtures.path('node_modules/.bin'))); | ||
| 83 | + | ||
| 84 | + // The following test ensures that we add paths that contains "node_modules/.bin" | ||
| 85 | + assert.ok(child.stdout.includes(fixtures.path('run-script/sub-directory/node_modules/.bin'))); | ||
| 86 | + | ||
| 80 | 87 | assert.strictEqual(child.stderr, ''); | |
| 81 | 88 | assert.strictEqual(child.code, 0); | |
| 82 | 89 | }); | |
@@ -94,4 +101,16 @@ describe('node run [command]', () => { | |||
| 94 | 101 | assert.strictEqual(child.stderr, ''); | |
| 95 | 102 | assert.strictEqual(child.code, 0); | |
| 96 | 103 | }); | |
| 104 | + | ||
| 105 | + it('will search parent directories for a package.json file', async () => { | ||
| 106 | + const packageJsonPath = fixtures.path('run-script/package.json'); | ||
| 107 | + const child = await common.spawnPromisified( | ||
| 108 | + process.execPath, | ||
| 109 | + [ '--no-warnings', '--run', `special-env-variables${envSuffix}`], | ||
| 110 | + { cwd: fixtures.path('run-script/sub-directory') }, | ||
| 111 | + ); | ||
| 112 | + assert.ok(child.stdout.includes(packageJsonPath)); | ||
| 113 | + assert.strictEqual(child.stderr, ''); | ||
| 114 | + assert.strictEqual(child.code, 0); | ||
| 115 | + }); | ||
| 97 | 116 | }); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments