From d7a755153a1f3dd071d2ae8af8f577edbd717ad8 Mon Sep 17 00:00:00 2001 From: Rob Palmer Date: Wed, 18 Feb 2026 14:04:23 +0000 Subject: [PATCH 1/7] doc: remove incorrect mention of `module` in `typescript.md` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node.js/Armaro has never supported `module`-keyword namespaces. Even with `transform-types` on. This removes the incorrect mention. PR-URL: https://github.com/nodejs/node/pull/61839 Reviewed-By: René Reviewed-By: Marco Ippolito Reviewed-By: Colin Ihrig Reviewed-By: Luigi Pinca Reviewed-By: Jacob Smith --- doc/api/typescript.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/api/typescript.md b/doc/api/typescript.md index d2399f17add65d..8875d1f7771e01 100644 --- a/doc/api/typescript.md +++ b/doc/api/typescript.md @@ -146,11 +146,10 @@ The most prominent features that require transformation are: * `Enum` declarations * `namespace` with runtime code -* legacy `module` with runtime code * parameter properties * import aliases -`namespaces` and `module` that do not contain runtime code are supported. +`namespace`s that do not contain runtime code are supported. This example will work correctly: ```ts From 94f779f0444e84036fbe545fc7ecdad375f2d3f7 Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Wed, 18 Feb 2026 08:14:47 -0800 Subject: [PATCH 2/7] tools: automate updates for test/fixtures/test426 PR-URL: https://github.com/nodejs/node/pull/60978 Reviewed-By: Antoine du Hamel --- .github/workflows/tools.yml | 9 ++++++ tools/dep_updaters/update-test426-fixtures.sh | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100755 tools/dep_updaters/update-test426-fixtures.sh diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml index 95ffa9e9779f27..204d476df4b82f 100644 --- a/.github/workflows/tools.yml +++ b/.github/workflows/tools.yml @@ -38,6 +38,7 @@ on: - root-certificates - simdjson - sqlite + - test426-fixtures - undici - uvwasi - zlib @@ -255,6 +256,14 @@ jobs: cat temp-output tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true rm temp-output + - id: test426-fixtures + subsystem: test + label: test + run: | + bash tools/dep_updaters/update-test426-fixtures.sh > temp-output + cat temp-output + tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true + rm temp-output - id: undici subsystem: deps label: dependencies diff --git a/tools/dep_updaters/update-test426-fixtures.sh b/tools/dep_updaters/update-test426-fixtures.sh new file mode 100755 index 00000000000000..730e7b67e35aa1 --- /dev/null +++ b/tools/dep_updaters/update-test426-fixtures.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +set -ex + +BASE_DIR=$(cd "$(dirname "$0")/../.." && pwd) + +TARGET_DIR="$BASE_DIR/test/fixtures/test426" +README="$BASE_DIR/test/test426/README.md" +TARBALL_URL=$(curl -fsIo /dev/null -w '%header{Location}' https://github.com/tc39/source-map-tests/archive/HEAD.tar.gz) +SHA=$(basename "$TARBALL_URL") + +CURRENT_SHA=$(sed -n 's#^.*https://github.com/tc39/source-map-tests/commit/\([0-9a-f]*\).*$#\1#p' "$README") + +if [ "$CURRENT_SHA" = "$SHA" ]; then + echo "Already up-to-date" + exit 0 +fi + +rm -rf "$TARGET_DIR" +mkdir "$TARGET_DIR" +curl -f "$TARBALL_URL" | tar -xz --strip-components 1 -C "$TARGET_DIR" + +TMP_FILE=$(mktemp) +sed "s/$CURRENT_SHA/$SHA/" "$README" > "$TMP_FILE" +mv "$TMP_FILE" "$README" + +# The last line of the script should always print the new version, +# as we need to add it to $GITHUB_ENV variable. +echo "NEW_VERSION=$SHA" From 94b1f66b734a1d6f7f2f78c8e8701daf76fb9e06 Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Wed, 18 Feb 2026 11:15:02 -0500 Subject: [PATCH 3/7] build: use path-ignore in GHA coverage-windows.yml PR-URL: https://github.com/nodejs/node/pull/61811 Reviewed-By: Colin Ihrig Reviewed-By: Luigi Pinca Reviewed-By: Stefan Stojanovic --- .github/workflows/coverage-windows.yml | 68 +++++++++++++++++--------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/.github/workflows/coverage-windows.yml b/.github/workflows/coverage-windows.yml index 5b982932330f0b..b3f96835c15b16 100644 --- a/.github/workflows/coverage-windows.yml +++ b/.github/workflows/coverage-windows.yml @@ -3,31 +3,55 @@ name: Coverage Windows on: pull_request: types: [opened, synchronize, reopened, ready_for_review] - paths: - - lib/**/*.js - - vcbuild.bat - - src/**/*.cc - - src/**/*.h - - test/** - - tools/gyp/** - - tools/test.py - - .github/workflows/coverage-windows.yml - - codecov.yml - - .nycrc + paths-ignore: + - '**.md' + - '**.nix' + - eslint.config.mjs + - '**/eslint.config_partial.mjs' + - android-configure + - android-configure.py + - android-patches/** + - benchmarks/** + - doc/** + - pyproject.yml + - tsconfig.json + - test/internet/** + - tools/actions/** + - tools/bootstrap/** + - tools/dep_updaters/** + - tools/doc/** + - tools/eslint-rules/** + - tools/eslint/** + - tools/lint-md/** + - typings/** + - .** + - '!.github/workflows/coverage-windows.yml' push: branches: - main - paths: - - lib/**/*.js - - vcbuild.bat - - src/**/*.cc - - src/**/*.h - - test/** - - tools/gyp/** - - tools/test.py - - .github/workflows/coverage-windows.yml - - codecov.yml - - .nycrc + paths-ignore: + - '**.md' + - '**.nix' + - eslint.config.mjs + - '**/eslint.config_partial.mjs' + - android-configure + - android-configure.py + - android-patches/** + - benchmarks/** + - doc/** + - pyproject.yml + - tsconfig.json + - test/internet/** + - tools/actions/** + - tools/bootstrap/** + - tools/dep_updaters/** + - tools/doc/** + - tools/eslint-rules/** + - tools/eslint/** + - tools/lint-md/** + - typings/** + - .** + - '!.github/workflows/coverage-windows.yml' concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} From af0d6d424810838a6ce44e154c25d676f74fea5b Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Wed, 18 Feb 2026 22:03:33 +0100 Subject: [PATCH 4/7] sea: support ESM entry point in SEA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This uses the new StartExecutionCallbackWithModule embedder API to support ESM entrypoint in SEA via a new configuration field `"mainFormat"`. The behavior currently aligns with the embedder API and is mostly in sync with the CommonJS entry point behavior, except that support for code caching and snapshot is left for follow-ups. PR-URL: https://github.com/nodejs/node/pull/61813 Reviewed-By: Michaël Zasso Reviewed-By: Anna Henningsen --- doc/api/single-executable-applications.md | 75 +++++++++++++++---- src/node_sea.cc | 67 +++++++++++++++-- src/node_sea.h | 8 +- test/fixtures/sea/esm/sea-config.json | 6 ++ test/fixtures/sea/esm/sea.mjs | 24 ++++++ .../test-single-executable-application-esm.js | 33 ++++++++ 6 files changed, 188 insertions(+), 25 deletions(-) create mode 100644 test/fixtures/sea/esm/sea-config.json create mode 100644 test/fixtures/sea/esm/sea.mjs create mode 100644 test/sea/test-single-executable-application-esm.js diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 12049f07a620bf..d90397dfd668eb 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -31,8 +31,8 @@ into the `node` binary. During start up, the program checks if anything has been injected. If the blob is found, it executes the script in the blob. Otherwise Node.js operates as it normally does. -The single executable application feature currently only supports running a -single embedded script using the [CommonJS][] module system. +The single executable application feature supports running a +single embedded script using the [CommonJS][] or the [ECMAScript Modules][] module system. Users can create a single executable application from their bundled script with the `node` binary itself and any tool which can inject resources into the @@ -110,6 +110,7 @@ The configuration currently reads the following top-level fields: ```json { "main": "/path/to/bundled/script.js", + "mainFormat": "commonjs", // Default: "commonjs", options: "commonjs", "module" "executable": "/path/to/node/binary", // Optional, if not specified, uses the current Node.js binary "output": "/path/to/write/the/generated/executable", "disableExperimentalSEAWarning": true, // Default: false @@ -290,14 +291,12 @@ This would be equivalent to running: node --no-warnings --trace-exit /path/to/bundled/script.js user-arg1 user-arg2 ``` -## In the injected main script - -### Single-executable application API +## Single-executable application API The `node:sea` builtin allows interaction with the single-executable application from the JavaScript main script embedded into the executable. -#### `sea.isSea()` +### `sea.isSea()` + +When using `"mainFormat": "module"`, `import()` can be used to dynamically +load built-in modules. Attempting to use `import()` to load modules from +the file system will throw an error. + ### Using native addons in the injected main script Native addons can be bundled as assets into the single-executable application @@ -599,6 +641,7 @@ start a discussion at to help us document them. [CommonJS]: modules.md#modules-commonjs-modules +[ECMAScript Modules]: esm.md#modules-ecmascript-modules [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format [Generating single executable preparation blobs]: #1-generating-single-executable-preparation-blobs [Mach-O]: https://en.wikipedia.org/wiki/Mach-O diff --git a/src/node_sea.cc b/src/node_sea.cc index bb566ec2155bb6..1f340cf56b21aa 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -84,6 +84,11 @@ size_t SeaSerializer::Write(const SeaResource& sea) { static_cast(sea.exec_argv_extension)); written_total += WriteArithmetic(static_cast(sea.exec_argv_extension)); + + Debug("Write SEA main code format %u\n", + static_cast(sea.main_code_format)); + written_total += + WriteArithmetic(static_cast(sea.main_code_format)); DCHECK_EQ(written_total, SeaResource::kHeaderSize); Debug("Write SEA code path %p, size=%zu\n", @@ -161,6 +166,11 @@ SeaResource SeaDeserializer::Read() { SeaExecArgvExtension exec_argv_extension = static_cast(extension_value); Debug("Read SEA resource exec argv extension %u\n", extension_value); + + uint8_t format_value = ReadArithmetic(); + CHECK_LE(format_value, static_cast(ModuleFormat::kModule)); + ModuleFormat main_code_format = static_cast(format_value); + Debug("Read SEA main code format %u\n", format_value); CHECK_EQ(read_total, SeaResource::kHeaderSize); std::string_view code_path = @@ -219,6 +229,7 @@ SeaResource SeaDeserializer::Read() { exec_argv_extension, code_path, code, + main_code_format, code_cache, assets, exec_argv}; @@ -501,6 +512,25 @@ std::optional ParseSingleExecutableConfig( config_path); return std::nullopt; } + } else if (key == "mainFormat") { + std::string_view format_str; + if (field.value().get_string().get(format_str)) { + FPrintF(stderr, + "\"mainFormat\" field of %s is not a string\n", + config_path); + return std::nullopt; + } + if (format_str == "commonjs") { + result.main_format = ModuleFormat::kCommonJS; + } else if (format_str == "module") { + result.main_format = ModuleFormat::kModule; + } else { + FPrintF(stderr, + "\"mainFormat\" field of %s must be one of " + "\"commonjs\" or \"module\"\n", + config_path); + return std::nullopt; + } } } @@ -512,6 +542,23 @@ std::optional ParseSingleExecutableConfig( "\"useCodeCache\" is redundant when \"useSnapshot\" is true\n"); } + // TODO(joyeecheung): support ESM with useSnapshot and useCodeCache. + if (result.main_format == ModuleFormat::kModule && + static_cast(result.flags & SeaFlags::kUseSnapshot)) { + FPrintF(stderr, + "\"mainFormat\": \"module\" is not supported when " + "\"useSnapshot\" is true\n"); + return std::nullopt; + } + + if (result.main_format == ModuleFormat::kModule && + static_cast(result.flags & SeaFlags::kUseCodeCache)) { + FPrintF(stderr, + "\"mainFormat\": \"module\" is not supported when " + "\"useCodeCache\" is true\n"); + return std::nullopt; + } + if (result.main_path.empty()) { FPrintF(stderr, "\"main\" field of %s is not a non-empty string\n", @@ -709,6 +756,7 @@ ExitCode GenerateSingleExecutableBlob( builds_snapshot_from_main ? std::string_view{snapshot_blob.data(), snapshot_blob.size()} : std::string_view{main_script.data(), main_script.size()}, + config.main_format, optional_sv_code_cache, assets_view, exec_argv_view}; @@ -792,20 +840,25 @@ void GetAssetKeys(const FunctionCallbackInfo& args) { } MaybeLocal LoadSingleExecutableApplication( - const StartExecutionCallbackInfo& info) { + const StartExecutionCallbackInfoWithModule& info) { // Here we are currently relying on the fact that in NodeMainInstance::Run(), // env->context() is entered. - Local context = Isolate::GetCurrent()->GetCurrentContext(); - Environment* env = Environment::GetCurrent(context); + Environment* env = info.env(); + Local context = env->context(); SeaResource sea = FindSingleExecutableResource(); CHECK(!sea.use_snapshot()); // TODO(joyeecheung): this should be an external string. Refactor UnionBytes // and make it easy to create one based on static content on the fly. Local main_script = - ToV8Value(env->context(), sea.main_code_or_snapshot).ToLocalChecked(); - return info.run_cjs->Call( - env->context(), Null(env->isolate()), 1, &main_script); + ToV8Value(context, sea.main_code_or_snapshot).ToLocalChecked(); + Local kind = + v8::Integer::New(env->isolate(), static_cast(sea.main_code_format)); + Local resource_name = + ToV8Value(context, env->exec_path()).ToLocalChecked(); + Local args[] = {main_script, kind, resource_name}; + return info.run_module()->Call( + env->context(), Null(env->isolate()), arraysize(args), args); } bool MaybeLoadSingleExecutableApplication(Environment* env) { @@ -821,7 +874,7 @@ bool MaybeLoadSingleExecutableApplication(Environment* env) { // this check is just here to guard against the unlikely case where // the SEA preparation blob has been manually modified by someone. CHECK(!env->snapshot_deserialize_main().IsEmpty()); - LoadEnvironment(env, StartExecutionCallback{}); + LoadEnvironment(env, StartExecutionCallbackWithModule{}); return true; } diff --git a/src/node_sea.h b/src/node_sea.h index 34596972b60219..dd0b89db841eed 100644 --- a/src/node_sea.h +++ b/src/node_sea.h @@ -11,6 +11,7 @@ #include #include +#include "node.h" #include "node_exit_code.h" namespace node { @@ -43,6 +44,7 @@ struct SeaConfig { std::string executable_path; SeaFlags flags = SeaFlags::kDefault; SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv; + ModuleFormat main_format = ModuleFormat::kCommonJS; std::unordered_map assets; std::vector exec_argv; }; @@ -52,6 +54,7 @@ struct SeaResource { SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv; std::string_view code_path; std::string_view main_code_or_snapshot; + ModuleFormat main_code_format = ModuleFormat::kCommonJS; std::optional code_cache; std::unordered_map assets; std::vector exec_argv; @@ -59,8 +62,9 @@ struct SeaResource { bool use_snapshot() const; bool use_code_cache() const; - static constexpr size_t kHeaderSize = - sizeof(kMagic) + sizeof(SeaFlags) + sizeof(SeaExecArgvExtension); + static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags) + + sizeof(SeaExecArgvExtension) + + sizeof(ModuleFormat); }; bool IsSingleExecutable(); diff --git a/test/fixtures/sea/esm/sea-config.json b/test/fixtures/sea/esm/sea-config.json new file mode 100644 index 00000000000000..e5ee27ff7f4c85 --- /dev/null +++ b/test/fixtures/sea/esm/sea-config.json @@ -0,0 +1,6 @@ +{ + "main": "sea.mjs", + "output": "sea", + "mainFormat": "module", + "disableExperimentalSEAWarning": true +} diff --git a/test/fixtures/sea/esm/sea.mjs b/test/fixtures/sea/esm/sea.mjs new file mode 100644 index 00000000000000..c8c9fe0ca1d571 --- /dev/null +++ b/test/fixtures/sea/esm/sea.mjs @@ -0,0 +1,24 @@ +import assert from 'node:assert'; +import { createRequire } from 'node:module'; +import { pathToFileURL } from 'node:url'; +import { dirname } from 'node:path'; + +// Test createRequire with process.execPath. +const assert2 = createRequire(process.execPath)('node:assert'); +assert.strictEqual(assert2.strict, assert.strict); + +// Test import.meta properties. This should be in sync with the CommonJS entry +// point's corresponding values. +assert.strictEqual(import.meta.url, pathToFileURL(process.execPath).href); +assert.strictEqual(import.meta.filename, process.execPath); +assert.strictEqual(import.meta.dirname, dirname(process.execPath)); +assert.strictEqual(import.meta.main, true); +// TODO(joyeecheung): support import.meta.resolve when we also support +// require.resolve in CommonJS entry points, the behavior of the two +// should be in sync. + +// Test import() with a built-in module. +const { strict } = await import('node:assert'); +assert.strictEqual(strict, assert.strict); + +console.log('ESM SEA executed successfully'); diff --git a/test/sea/test-single-executable-application-esm.js b/test/sea/test-single-executable-application-esm.js new file mode 100644 index 00000000000000..9f7366cb0e2405 --- /dev/null +++ b/test/sea/test-single-executable-application-esm.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../common'); + +const { + buildSEA, + skipIfBuildSEAIsNotSupported, +} = require('../common/sea'); + +skipIfBuildSEAIsNotSupported(); + +// This tests the creation of a single executable application with an ESM +// entry point using the "mainFormat": "module" configuration. + +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); + +tmpdir.refresh(); + +const outputFile = buildSEA(fixtures.path('sea', 'esm')); + +spawnSyncAndExitWithoutError( + outputFile, + { + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + }, + { + stdout: /ESM SEA executed successfully/, + }); From 128d2a6f2bb0b61643bc7b047df7ae09d015fce8 Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Wed, 18 Feb 2026 16:03:47 -0500 Subject: [PATCH 5/7] deps: update llhttp to 9.3.1 PR-URL: https://github.com/nodejs/node/pull/61827 Reviewed-By: Antoine du Hamel Reviewed-By: Colin Ihrig Reviewed-By: Luigi Pinca --- deps/llhttp/.gitignore | 1 + deps/llhttp/CMakeLists.txt | 18 ++++++++---------- deps/llhttp/README.md | 6 ++++-- deps/llhttp/include/llhttp.h | 22 +++++++++++----------- deps/llhttp/src/llhttp.c | 22 +++++++++++++--------- 5 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 deps/llhttp/.gitignore diff --git a/deps/llhttp/.gitignore b/deps/llhttp/.gitignore new file mode 100644 index 00000000000000..98438a2cd3230c --- /dev/null +++ b/deps/llhttp/.gitignore @@ -0,0 +1 @@ +libllhttp.pc diff --git a/deps/llhttp/CMakeLists.txt b/deps/llhttp/CMakeLists.txt index 56f0098c2c5c49..6398043145c879 100644 --- a/deps/llhttp/CMakeLists.txt +++ b/deps/llhttp/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25.0) cmake_policy(SET CMP0069 NEW) -project(llhttp VERSION 9.3.0) +project(llhttp VERSION 9.3.1) include(GNUInstallDirs) set(CMAKE_C_STANDARD 99) @@ -24,8 +24,8 @@ endif() # Options # # Generic option -option(BUILD_SHARED_LIBS "Build shared libraries (.dll/.so)" ON) -option(BUILD_STATIC_LIBS "Build static libraries (.lib/.a)" OFF) +option(LLHTTP_BUILD_SHARED_LIBS "Build shared libraries (.dll/.so)" ON) +option(LLHTTP_BUILD_STATIC_LIBS "Build static libraries (.lib/.a)" OFF) # Source code set(LLHTTP_SOURCES @@ -80,7 +80,7 @@ function(config_library target) ) endfunction(config_library target) -if(BUILD_SHARED_LIBS) +if(LLHTTP_BUILD_SHARED_LIBS) add_library(llhttp_shared SHARED ${llhttp_src} ) @@ -88,13 +88,11 @@ if(BUILD_SHARED_LIBS) config_library(llhttp_shared) endif() -if(BUILD_STATIC_LIBS) +if(LLHTTP_BUILD_STATIC_LIBS) add_library(llhttp_static STATIC ${llhttp_src} ) - if(BUILD_SHARED_LIBS) - add_library(llhttp::llhttp ALIAS llhttp_shared) - else() + if(NOT LLHTTP_BUILD_SHARED_LIBS) add_library(llhttp::llhttp ALIAS llhttp_static) endif() config_library(llhttp_static) @@ -113,6 +111,6 @@ message(STATUS "Project configure summary:") message(STATUS "") message(STATUS " CMake build type .................: ${CMAKE_BUILD_TYPE}") message(STATUS " Install prefix ...................: ${CMAKE_INSTALL_PREFIX}") -message(STATUS " Build shared library .............: ${BUILD_SHARED_LIBS}") -message(STATUS " Build static library .............: ${BUILD_STATIC_LIBS}") +message(STATUS " Build shared library .............: ${LLHTTP_BUILD_SHARED_LIBS}") +message(STATUS " Build static library .............: ${LLHTTP_BUILD_STATIC_LIBS}") message(STATUS "") diff --git a/deps/llhttp/README.md b/deps/llhttp/README.md index 70af44ff0f5a16..008b7e622bc962 100644 --- a/deps/llhttp/README.md +++ b/deps/llhttp/README.md @@ -435,14 +435,16 @@ If you want to use this library in a CMake project as a static library, you can FetchContent_Declare(llhttp URL "https://github.com/nodejs/llhttp/archive/refs/tags/release/v8.1.0.tar.gz") -set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "") -set(BUILD_STATIC_LIBS ON CACHE INTERNAL "") +set(LLHTTP_BUILD_SHARED_LIBS OFF CACHE INTERNAL "") +set(LLHTTP_BUILD_STATIC_LIBS ON CACHE INTERNAL "") FetchContent_MakeAvailable(llhttp) # Link with the llhttp_static target target_link_libraries(${EXAMPLE_PROJECT_NAME} ${PROJECT_LIBRARIES} llhttp_static ${PROJECT_NAME}) ``` +If using a version prior to 9.3.0, the `LLHTTP_BUILD_SHARED_LIBS` and `LLHTTP_BUILD_STATIC_LIBS` options are known as `BUILD_SHARED_LIBS` and `BUILD_STATIC_LIBS` and should be used instead. + _Note that using the git repo directly (e.g., via a git repo url and tag) will not work with FetchContent_Declare because [CMakeLists.txt](./CMakeLists.txt) requires string replacements (e.g., `_RELEASE_`) before it will build._ ## Building on Windows diff --git a/deps/llhttp/include/llhttp.h b/deps/llhttp/include/llhttp.h index 60544596a9942c..194fee8c906ed3 100644 --- a/deps/llhttp/include/llhttp.h +++ b/deps/llhttp/include/llhttp.h @@ -4,7 +4,7 @@ #define LLHTTP_VERSION_MAJOR 9 #define LLHTTP_VERSION_MINOR 3 -#define LLHTTP_VERSION_PATCH 0 +#define LLHTTP_VERSION_PATCH 1 #ifndef INCLUDE_LLHTTP_ITSELF_H_ #define INCLUDE_LLHTTP_ITSELF_H_ @@ -58,10 +58,8 @@ enum llhttp_errno { HPE_OK = 0, HPE_INTERNAL = 1, HPE_STRICT = 2, - HPE_CR_EXPECTED = 25, HPE_LF_EXPECTED = 3, HPE_UNEXPECTED_CONTENT_LENGTH = 4, - HPE_UNEXPECTED_SPACE = 30, HPE_CLOSED_CONNECTION = 5, HPE_INVALID_METHOD = 6, HPE_INVALID_URL = 7, @@ -82,15 +80,17 @@ enum llhttp_errno { HPE_PAUSED_UPGRADE = 22, HPE_PAUSED_H2_UPGRADE = 23, HPE_USER = 24, + HPE_CR_EXPECTED = 25, HPE_CB_URL_COMPLETE = 26, HPE_CB_STATUS_COMPLETE = 27, - HPE_CB_METHOD_COMPLETE = 32, - HPE_CB_VERSION_COMPLETE = 33, HPE_CB_HEADER_FIELD_COMPLETE = 28, HPE_CB_HEADER_VALUE_COMPLETE = 29, + HPE_UNEXPECTED_SPACE = 30, + HPE_CB_RESET = 31, + HPE_CB_METHOD_COMPLETE = 32, + HPE_CB_VERSION_COMPLETE = 33, HPE_CB_CHUNK_EXTENSION_NAME_COMPLETE = 34, HPE_CB_CHUNK_EXTENSION_VALUE_COMPLETE = 35, - HPE_CB_RESET = 31, HPE_CB_PROTOCOL_COMPLETE = 38 }; typedef enum llhttp_errno llhttp_errno_t; @@ -294,10 +294,8 @@ typedef enum llhttp_status llhttp_status_t; XX(0, OK, OK) \ XX(1, INTERNAL, INTERNAL) \ XX(2, STRICT, STRICT) \ - XX(25, CR_EXPECTED, CR_EXPECTED) \ XX(3, LF_EXPECTED, LF_EXPECTED) \ XX(4, UNEXPECTED_CONTENT_LENGTH, UNEXPECTED_CONTENT_LENGTH) \ - XX(30, UNEXPECTED_SPACE, UNEXPECTED_SPACE) \ XX(5, CLOSED_CONNECTION, CLOSED_CONNECTION) \ XX(6, INVALID_METHOD, INVALID_METHOD) \ XX(7, INVALID_URL, INVALID_URL) \ @@ -318,15 +316,17 @@ typedef enum llhttp_status llhttp_status_t; XX(22, PAUSED_UPGRADE, PAUSED_UPGRADE) \ XX(23, PAUSED_H2_UPGRADE, PAUSED_H2_UPGRADE) \ XX(24, USER, USER) \ + XX(25, CR_EXPECTED, CR_EXPECTED) \ XX(26, CB_URL_COMPLETE, CB_URL_COMPLETE) \ XX(27, CB_STATUS_COMPLETE, CB_STATUS_COMPLETE) \ - XX(32, CB_METHOD_COMPLETE, CB_METHOD_COMPLETE) \ - XX(33, CB_VERSION_COMPLETE, CB_VERSION_COMPLETE) \ XX(28, CB_HEADER_FIELD_COMPLETE, CB_HEADER_FIELD_COMPLETE) \ XX(29, CB_HEADER_VALUE_COMPLETE, CB_HEADER_VALUE_COMPLETE) \ + XX(30, UNEXPECTED_SPACE, UNEXPECTED_SPACE) \ + XX(31, CB_RESET, CB_RESET) \ + XX(32, CB_METHOD_COMPLETE, CB_METHOD_COMPLETE) \ + XX(33, CB_VERSION_COMPLETE, CB_VERSION_COMPLETE) \ XX(34, CB_CHUNK_EXTENSION_NAME_COMPLETE, CB_CHUNK_EXTENSION_NAME_COMPLETE) \ XX(35, CB_CHUNK_EXTENSION_VALUE_COMPLETE, CB_CHUNK_EXTENSION_VALUE_COMPLETE) \ - XX(31, CB_RESET, CB_RESET) \ XX(38, CB_PROTOCOL_COMPLETE, CB_PROTOCOL_COMPLETE) \ diff --git a/deps/llhttp/src/llhttp.c b/deps/llhttp/src/llhttp.c index aa4c468209700c..515ba512abbdb0 100644 --- a/deps/llhttp/src/llhttp.c +++ b/deps/llhttp/src/llhttp.c @@ -10,7 +10,7 @@ #endif /* _MSC_VER */ #endif /* __SSE4_2__ */ -#ifdef __ARM_NEON__ +#if defined(__ARM_NEON__) || defined(__ARM_NEON) #include #endif /* __ARM_NEON__ */ @@ -1542,7 +1542,7 @@ static llparse_state_t llhttp__internal__run( 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, @@ -2625,7 +2625,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_otherwise; } #endif /* __SSE4_2__ */ - #ifdef __ARM_NEON__ + #if defined(__ARM_NEON__) || defined(__ARM_NEON) while (endp - p >= 16) { uint8x16_t input; uint8x16_t single; @@ -2639,19 +2639,23 @@ static llparse_state_t llhttp__internal__run( /* Find first character that does not match `ranges` */ single = vceqq_u8(input, vdupq_n_u8(0x9)); mask = single; - single = vandq_u16( + single = vandq_u8( vcgeq_u8(input, vdupq_n_u8(' ')), vcleq_u8(input, vdupq_n_u8('~')) ); - mask = vorrq_u16(mask, single); - single = vandq_u16( + mask = vorrq_u8(mask, single); + single = vandq_u8( vcgeq_u8(input, vdupq_n_u8(0x80)), vcleq_u8(input, vdupq_n_u8(0xff)) ); - mask = vorrq_u16(mask, single); - narrow = vshrn_n_u16(mask, 4); + mask = vorrq_u8(mask, single); + narrow = vshrn_n_u16(vreinterpretq_u16_u8(mask), 4); match_mask = ~vget_lane_u64(vreinterpret_u64_u8(narrow), 0); - match_len = __builtin_ctzll(match_mask) >> 2; + if (match_mask == 0) { + match_len = 16; + } else { + match_len = __builtin_ctzll(match_mask) >> 2; + } if (match_len != 16) { p += match_len; goto s_n_llhttp__internal__n_header_value_otherwise; From 9d64cb6144526c16725fa2abe182bb927a28e3c2 Mon Sep 17 00:00:00 2001 From: Efe Date: Thu, 19 Feb 2026 01:03:36 +0100 Subject: [PATCH 6/7] http: remove redundant keepAliveTimeoutBuffer assignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/61743 Reviewed-By: Gerhard Stöbich Reviewed-By: Luigi Pinca Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Matteo Collina Reviewed-By: Tim Perry Reviewed-By: Trivikram Kamat --- lib/_http_server.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/_http_server.js b/lib/_http_server.js index 4da10dd1edf133..1d1bc37c47e109 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -574,12 +574,6 @@ function Server(options, requestListener) { storeHTTPOptions.call(this, options); - // Optional buffer added to the keep-alive timeout when setting socket timeouts. - // Helps reduce ECONNRESET errors from clients by extending the internal timeout. - // Default is 1000ms if not specified. - const buf = options.keepAliveTimeoutBuffer; - this.keepAliveTimeoutBuffer = - (typeof buf === 'number' && NumberIsFinite(buf) && buf >= 0) ? buf : 1000; net.Server.call( this, { allowHalfOpen: true, noDelay: options.noDelay ?? true, From a73cda9f232d7bc8301ad715a3b66c28ebe6234b Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Wed, 18 Feb 2026 21:52:54 -0300 Subject: [PATCH 7/7] lib: optimize styleText when validateStream is false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit optimizes the util.styleText when validateStream is false Co-Authored-By: Bruno Rodrigues Signed-off-by: RafaelGSS PR-URL: https://github.com/nodejs/node/pull/61792 Reviewed-By: Santiago Gimeno Reviewed-By: Gürgün Dayıoğlu --- lib/util.js | 145 +++++++++++++++------------ test/parallel/test-util-styletext.js | 4 +- 2 files changed, 82 insertions(+), 67 deletions(-) diff --git a/lib/util.js b/lib/util.js index ebd486addc7eb2..5d3c0c75a28fc5 100644 --- a/lib/util.js +++ b/lib/util.js @@ -25,7 +25,6 @@ const { ArrayIsArray, ArrayPrototypePop, ArrayPrototypePush, - ArrayPrototypeReduce, Error, ErrorCaptureStackTrace, FunctionPrototypeBind, @@ -37,8 +36,6 @@ const { ObjectSetPrototypeOf, ObjectValues, ReflectApply, - RegExp, - RegExpPrototypeSymbolReplace, StringPrototypeToWellFormed, } = primordials; @@ -104,13 +101,58 @@ function lazyAbortController() { let internalDeepEqual; -/** - * @param {string} [code] - * @returns {string} - */ -function escapeStyleCode(code) { - if (code === undefined) return ''; - return `\u001b[${code}m`; +// Pre-computed ANSI escape code constants +const kEscape = '\u001b['; +const kEscapeEnd = 'm'; + +// Codes for dim (2) and bold (1) - these share close code 22 +const kDimCode = 2; +const kBoldCode = 1; + +let styleCache; + +function getStyleCache() { + if (styleCache === undefined) { + styleCache = { __proto__: null }; + const colors = inspect.colors; + for (const key of ObjectKeys(colors)) { + const codes = colors[key]; + if (codes) { + const openNum = codes[0]; + const closeNum = codes[1]; + styleCache[key] = { + __proto__: null, + openSeq: kEscape + openNum + kEscapeEnd, + closeSeq: kEscape + closeNum + kEscapeEnd, + keepClose: openNum === kDimCode || openNum === kBoldCode, + }; + } + } + } + return styleCache; +} + +function replaceCloseCode(str, closeSeq, openSeq, keepClose) { + const closeLen = closeSeq.length; + let index = str.indexOf(closeSeq); + if (index === -1) return str; + + let result = ''; + let lastIndex = 0; + const replacement = keepClose ? closeSeq + openSeq : openSeq; + + do { + const afterClose = index + closeLen; + if (afterClose < str.length) { + result += str.slice(lastIndex, index) + replacement; + lastIndex = afterClose; + } else { + break; + } + index = str.indexOf(closeSeq, lastIndex); + } while (index !== -1); + + return result + str.slice(lastIndex); } /** @@ -121,12 +163,29 @@ function escapeStyleCode(code) { * @param {Stream} [options.stream] - The stream used for validation. * @returns {string} */ -function styleText(format, text, { validateStream = true, stream = process.stdout } = {}) { +function styleText(format, text, options) { + const validateStream = options?.validateStream ?? true; + const cache = getStyleCache(); + + // Fast path: single format string with validateStream=false + if (!validateStream && typeof format === 'string' && typeof text === 'string') { + if (format === 'none') return text; + const style = cache[format]; + if (style !== undefined) { + const processed = replaceCloseCode(text, style.closeSeq, style.openSeq, style.keepClose); + return style.openSeq + processed + style.closeSeq; + } + } + validateString(text, 'text'); + if (options !== undefined) { + validateObject(options, 'options'); + } validateBoolean(validateStream, 'options.validateStream'); let skipColorize; if (validateStream) { + const stream = options?.stream ?? process.stdout; if ( !isReadableStream(stream) && !isWritableStream(stream) && @@ -134,71 +193,27 @@ function styleText(format, text, { validateStream = true, stream = process.stdou ) { throw new ERR_INVALID_ARG_TYPE('stream', ['ReadableStream', 'WritableStream', 'Stream'], stream); } - - // If the stream is falsy or should not be colorized, set skipColorize to true skipColorize = !lazyUtilColors().shouldColorize(stream); } - // If the format is not an array, convert it to an array const formatArray = ArrayIsArray(format) ? format : [format]; - const codes = []; + let openCodes = ''; + let closeCodes = ''; + let processedText = text; + for (const key of formatArray) { if (key === 'none') continue; - const formatCodes = inspect.colors[key]; - // If the format is not a valid style, throw an error - if (formatCodes == null) { + const style = cache[key]; + if (style === undefined) { validateOneOf(key, 'format', ObjectKeys(inspect.colors)); } - if (skipColorize) continue; - ArrayPrototypePush(codes, formatCodes); - } - - if (skipColorize) { - return text; + openCodes += style.openSeq; + closeCodes = style.closeSeq + closeCodes; + processedText = replaceCloseCode(processedText, style.closeSeq, style.openSeq, style.keepClose); } - // Build opening codes - let openCodes = ''; - for (let i = 0; i < codes.length; i++) { - openCodes += escapeStyleCode(codes[i][0]); - } - - // Process the text to handle nested styles - let processedText; - if (codes.length > 0) { - processedText = ArrayPrototypeReduce( - codes, - (text, code) => RegExpPrototypeSymbolReplace( - // Find the reset code - new RegExp(`\\u001b\\[${code[1]}m`, 'g'), - text, - (match, offset) => { - // Check if there's more content after this reset - if (offset + match.length < text.length) { - if ( - code[0] === inspect.colors.dim[0] || - code[0] === inspect.colors.bold[0] - ) { - // Dim and bold are not mutually exclusive, so we need to reapply - return `${match}${escapeStyleCode(code[0])}`; - } - return escapeStyleCode(code[0]); - } - return match; - }, - ), - text, - ); - } else { - processedText = text; - } - - // Build closing codes in reverse order - let closeCodes = ''; - for (let i = codes.length - 1; i >= 0; i--) { - closeCodes += escapeStyleCode(codes[i][1]); - } + if (skipColorize) return text; return `${openCodes}${processedText}${closeCodes}`; } diff --git a/test/parallel/test-util-styletext.js b/test/parallel/test-util-styletext.js index b87c5d7e82c74c..4fdf419143453c 100644 --- a/test/parallel/test-util-styletext.js +++ b/test/parallel/test-util-styletext.js @@ -22,12 +22,12 @@ const noChange = 'test'; util.styleText(invalidOption, 'test'); }, { code: 'ERR_INVALID_ARG_VALUE', - }); + }, invalidOption); assert.throws(() => { util.styleText('red', invalidOption); }, { code: 'ERR_INVALID_ARG_TYPE' - }); + }, invalidOption); }); assert.throws(() => {