diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index 4e0fdab245e1f9..a7641a3c04d73d 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -132,26 +132,6 @@ jobs:
NODE=$(command -v node) make lint-md
env:
NODE_RELEASED_VERSIONS: ${{ steps.get-released-versions.outputs.NODE_RELEASED_VERSIONS }}
- lint-nix:
- if: github.event.pull_request.draft == false
- runs-on: ubuntu-slim
- steps:
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- with:
- persist-credentials: false
- sparse-checkout: '*.nix'
- sparse-checkout-cone-mode: false
- - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- - name: Lint Nix files
- run: |
- nix-shell -I nixpkgs=./tools/nix/pkgs.nix -p 'nixfmt-tree' --run '
- treefmt --quiet --ci
- ' && EXIT_CODE="$?" || EXIT_CODE="$?"
- if [ "$EXIT_CODE" != "0" ]
- then
- git --no-pager diff || true
- exit "$EXIT_CODE"
- fi
lint-py:
if: github.event.pull_request.draft == false
diff --git a/.github/workflows/nix-changes-comment.yml b/.github/workflows/nix-changes-comment.yml
new file mode 100644
index 00000000000000..db96ff4a09caea
--- /dev/null
+++ b/.github/workflows/nix-changes-comment.yml
@@ -0,0 +1,98 @@
+# This action requires the following secrets to be set on the repository:
+# GH_USER_TOKEN: GitHub user token, to be used by ncu and to push changes
+name: Comment on PR on Nix changes
+
+on:
+ workflow_run:
+ workflows: [Nix files edited]
+ types: [completed]
+
+permissions: {}
+
+jobs:
+ aggregate-results:
+ if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
+ name: Aggregate results
+ runs-on: ubuntu-slim
+ permissions:
+ pull-requests: write
+ steps:
+ - name: Download artefacts
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ run-id: ${{ github.event.workflow_run.id }}
+ # Using privileged token to be able to download file from a different workflow
+ github-token: ${{ secrets.GH_USER_TOKEN }}
+
+ - name: Fetch PR data
+ id: pr-data
+ run: |
+ mv requisites-*/* .
+ ls
+ if grep -nvHE '^/nix/store/[0-9a-df-np-sv-z]{32}-[A-Za-z0-9?=._+-]+$' requisites-*.list; then
+ echo 'Unexpected line format in requisites list; refusing to process.' >&2
+ exit 1
+ fi
+ echo "NUMBER=$(grep -E '^[0-9]+$' pr_number.txt || true)" >> "$GITHUB_OUTPUT"
+
+ - name: Aggregate results
+ if: ${{ steps.pr-data.outputs.NUMBER }}
+ run: |
+ set -x
+ > requisites-before.list
+ > requisites-after.list
+ TABLE=$(
+ echo '| Platform | Number of requisites | Proportion of new derivations |'
+ echo '| :-: | -: | -: |'
+ TOTAL_BEFORE=0
+ TOTAL_AFTER=0
+ TOTAL_CHANGED=0
+ QUOTE='`'
+ for PLATFORM in x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin; do
+ BEFORE=$(wc -l < "requisites-${PLATFORM}-before.list")
+ TOTAL_BEFORE=$(($TOTAL_BEFORE + $BEFORE))
+ AFTER=$(wc -l < "requisites-${PLATFORM}-after.list")
+ TOTAL_AFTER=$(($TOTAL_AFTER + $AFTER))
+ DIFF="_no changes_"
+ [ "$BEFORE" = "$AFTER" ] || DIFF="**$([ "$AFTER" < "$BEFORE" ] || echo '+')$(($AFTER - $BEFORE))**"
+ CHANGED=$(git diff --no-index "requisites-${PLATFORM}-before.list" "requisites-${PLATFORM}-after.list" | grep -c '^+[^+]')
+ TOTAL_CHANGED=$(($TOTAL_CHANGED + $CHANGED))
+ PERCENT=100
+ [ "$CHANGED" = "$AFTER" ] || {
+ PERMILLE=$((1000 * $CHANGED / $AFTER))
+ PERCENT="$((PERMILLE / 10)).$((PERMILLE % 10))"
+ }
+ echo "| ${QUOTE}${PLATFORM}${QUOTE} | $BEFORE -> $AFTER => $DIFF | $CHANGED / $AFTER = $PERCENT% |"
+ awk '{ print $0 " ('"$PLATFORM"')" }' "requisites-${PLATFORM}-before.list" >> requisites-before.list
+ awk '{ print $0 " ('"$PLATFORM"')" }' "requisites-${PLATFORM}-after.list" >> requisites-after.list
+ done
+
+ DIFF="_no changes_"
+ [ "$TOTAL_BEFORE" = "$TOTAL_AFTER" ] || DIFF="**$([ "$TOTAL_AFTER" < "$TOTAL_BEFORE" ] || echo '+')$(($TOTAL_AFTER - $TOTAL_BEFORE))**"
+ PERCENT=100
+ [ "$TOTAL_CHANGED" = "$TOTAL_AFTER" ] || {
+ PERMILLE=$((1000 * $TOTAL_CHANGED / $TOTAL_AFTER))
+ PERCENT="$((PERMILLE / 10)).$((PERMILLE % 10))"
+ }
+ echo "| **Total** | $TOTAL_BEFORE -> $TOTAL_AFTER => $DIFF | $TOTAL_CHANGED / $TOTAL_AFTER = $PERCENT% |"
+ )
+ sort -k1.45 requisites-before.list > requisites-before.sort
+ sort -k1.45 requisites-after.list > requisites-after.sort
+ if git diff -U0 --no-color --no-index requisites-before.sort requisites-after.sort > requisites.diff; then
+ echo 'No changes detected.'
+ else
+ {
+ echo "$TABLE"
+ echo
+ echo 'Changelog
'
+ echo
+ echo '```diff'
+ tail -n +5 requisites.diff
+ echo '```'
+ echo
+ echo ' '
+ } | gh pr comment -R "$GITHUB_REPOSITORY" "$PR_NUMBER" -F -
+ fi
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ steps.pr-data.outputs.NUMBER }}
diff --git a/.github/workflows/nix-changes.yml b/.github/workflows/nix-changes.yml
new file mode 100644
index 00000000000000..ddf19260359027
--- /dev/null
+++ b/.github/workflows/nix-changes.yml
@@ -0,0 +1,140 @@
+# This action uses the following secrets:
+# CACHIX_AUTH_TOKEN: Write access to nodejs.cachix.org – without it, the cache is read-only.
+name: Nix files edited
+
+on:
+ push:
+ branches:
+ - main
+ - canary
+ - v[0-9]+.x-staging
+ - v[0-9]+.x
+ paths:
+ - '**.nix'
+ - .github/workflows/nix-changes.yml
+ pull_request:
+ paths:
+ - '**.nix'
+ - .github/workflows/nix-changes.yml
+ types: [opened, synchronize, reopened, ready_for_review]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ diff:
+ if: github.event.pull_request.draft == false
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: ubuntu-24.04
+ system: x86_64-linux
+ - runner: ubuntu-24.04-arm
+ system: aarch64-linux
+ - runner: macos-15-intel
+ system: x86_64-darwin
+ - runner: macos-latest
+ system: aarch64-darwin
+ name: '${{ matrix.system }}: dependencies diff'
+ runs-on: ${{ matrix.runner }}
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ fetch-depth: 2
+ persist-credentials: false
+ sparse-checkout: '*.nix'
+ sparse-checkout-cone-mode: false
+
+ - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
+ with:
+ extra_nix_config: sandbox = true
+
+ - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
+ with:
+ authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
+ name: nodejs
+
+ - name: Compute requisites after change
+ shell: bash # See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference, we want the pipefail option.
+ run: |
+ nix-store --query --references "$(
+ nix-instantiate -I "nixpkgs=./tools/nix/pkgs.nix" shell.nix \
+ --arg devTools "
+ (import ./tools/nix/devTools.nix {})
+ ++ builtins.attrValues (
+ { inherit (import {}) nixfmt-tree sccache; }
+ // import ./tools/nix/openssl-matrix.nix {}
+ )")" \
+ | xargs nix-store --realise \
+ | xargs nix-store --query --requisites \
+ | sort -k1.45 \
+ > requisites-${{ matrix.system }}-after.list
+
+ - name: Compute requisites before change
+ shell: bash # See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference, we want the pipefail option.
+ run: |
+ git reset HEAD^ --hard
+ nix-store --query --references "$(
+ nix-instantiate -I "nixpkgs=./tools/nix/pkgs.nix" shell.nix \
+ --arg devTools "
+ (import ./tools/nix/devTools.nix {})
+ ++ builtins.attrValues (
+ { inherit (import {}) nixfmt-tree sccache; }
+ // import ./tools/nix/openssl-matrix.nix {}
+ )")" \
+ | xargs nix-store --realise \
+ | xargs nix-store --query --requisites \
+ | sort -k1.45 \
+ > requisites-${{ matrix.system }}-before.list
+
+ - name: Output diff
+ run: |
+ {
+ if diff_output="$(git diff -U0 --no-color --no-index requisites-${{ matrix.system }}-before.list requisites-${{ matrix.system }}-after.list)"; then
+ echo 'No changes detected.'
+ else
+ echo '```diff'
+ echo "$diff_output" | tail -n +5
+ echo '```'
+ fi
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Store PR number
+ if: ${{ github.event.pull_request && matrix.system == 'x86_64-linux' }}
+ run: echo "${{ github.event.pull_request.number }}" > pr_number.txt
+
+ - name: Upload requisites lists
+ if: ${{ github.event.pull_request }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: requisites-${{ matrix.system }}
+ path: |
+ pr_number.*
+ requisites-${{ matrix.system }}-before.list
+ requisites-${{ matrix.system }}-after.list
+
+ lint-nix:
+ if: github.event.pull_request.draft == false
+ runs-on: ubuntu-slim
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ persist-credentials: false
+ sparse-checkout: '*.nix'
+ sparse-checkout-cone-mode: false
+ - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
+ - name: Lint Nix files
+ run: |
+ nix-shell -I nixpkgs=./tools/nix/pkgs.nix -p 'nixfmt-tree' --run '
+ treefmt --quiet --ci
+ ' && EXIT_CODE="$?" || EXIT_CODE="$?"
+ if [ "$EXIT_CODE" != "0" ]
+ then
+ git --no-pager diff || true
+ exit "$EXIT_CODE"
+ fi
diff --git a/lib/internal/blob.js b/lib/internal/blob.js
index ad84312683d010..68071a5f5645e7 100644
--- a/lib/internal/blob.js
+++ b/lib/internal/blob.js
@@ -122,7 +122,7 @@ function getSource(source, endings) {
source = new Uint8Array(source);
} else if (!isArrayBufferView(source)) {
if (endings === 'native')
- source = RegExpPrototypeSymbolReplace(/\n|\r\n/g, source, EOL);
+ source = RegExpPrototypeSymbolReplace(/\r\n|\r|\n/g, source, EOL);
source = enc.encode(source);
}
diff --git a/lib/internal/mime.js b/lib/internal/mime.js
index de691a745031e0..0c5bbc3325aaec 100644
--- a/lib/internal/mime.js
+++ b/lib/internal/mime.js
@@ -148,12 +148,13 @@ class MIMEParams {
*/
delete(name) {
this.#parse();
- this.#data.delete(name);
+ this.#data.delete(toASCIILower(`${name}`));
}
get(name) {
this.#parse();
const data = this.#data;
+ name = toASCIILower(`${name}`);
if (data.has(name)) {
return data.get(name);
}
@@ -162,13 +163,13 @@ class MIMEParams {
has(name) {
this.#parse();
- return this.#data.has(name);
+ return this.#data.has(toASCIILower(`${name}`));
}
set(name, value) {
this.#parse();
const data = this.#data;
- name = `${name}`;
+ name = toASCIILower(`${name}`);
value = `${value}`;
const invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT);
if (name.length === 0 || invalidNameIndex !== -1) {
diff --git a/test/parallel/test-blob.js b/test/parallel/test-blob.js
index cbe7a42ada467f..e20ebbb86404bc 100644
--- a/test/parallel/test-blob.js
+++ b/test/parallel/test-blob.js
@@ -407,6 +407,24 @@ assert.throws(() => new Blob({}), {
const b = new Blob(['hello\n'], { endings: 'native' });
assert.strictEqual(b.size, EOL.length + 5);
+ // The WHATWG "convert line endings to native" algorithm normalizes every
+ // standalone "\r", "\n", and "\r\n" sequence to the native line ending.
+ // Refs: https://w3c.github.io/FileAPI/#convert-line-endings-to-native
+ (async () => {
+ const cases = [
+ ['a\rb', `a${EOL}b`],
+ ['a\nb', `a${EOL}b`],
+ ['a\r\nb', `a${EOL}b`],
+ ['a\r\rb', `a${EOL}${EOL}b`],
+ ['a\n\rb', `a${EOL}${EOL}b`],
+ ['\r\n\r', `${EOL}${EOL}`],
+ ];
+ for (const [input, expected] of cases) {
+ const blob = new Blob([input], { endings: 'native' });
+ assert.strictEqual(await blob.text(), expected);
+ }
+ })().then(common.mustCall());
+
[1, {}, 'foo'].forEach((endings) => {
assert.throws(() => new Blob([], { endings }), {
code: 'ERR_INVALID_ARG_VALUE',
diff --git a/test/parallel/test-mime-api.js b/test/parallel/test-mime-api.js
index 30272e5aa64df5..dffead31850b47 100644
--- a/test/parallel/test-mime-api.js
+++ b/test/parallel/test-mime-api.js
@@ -158,3 +158,30 @@ assert.throws(() => params.set(`x${NOT_HTTP_TOKEN_CODE_POINT}`, 'x'), /parameter
assert.throws(() => params.set('x', `${NOT_HTTP_QUOTED_STRING_CODE_POINT};`), /parameter value/i);
assert.throws(() => params.set('x', `${NOT_HTTP_QUOTED_STRING_CODE_POINT}x`), /parameter value/i);
assert.throws(() => params.set('x', `x${NOT_HTTP_QUOTED_STRING_CODE_POINT}`), /parameter value/i);
+
+// MIME parameter names are case-insensitive. The parser lower-cases them, so
+// the MIMEParams accessors must lower-case their argument too; otherwise a
+// parsed parameter is unreachable by the casing the caller actually used and
+// `set()` can create case-colliding duplicate parameters.
+// Refs: https://mimesniff.spec.whatwg.org/ (parameter names are ASCII-lowercased)
+{
+ const mime = new MIMEType('text/plain;Charset=value');
+ assert.strictEqual(mime.params.get('Charset'), 'value');
+ assert.strictEqual(mime.params.get('charset'), 'value');
+ assert.strictEqual(mime.params.get('CHARSET'), 'value');
+ assert.strictEqual(mime.params.has('Charset'), true);
+ assert.strictEqual(`${mime.params}`, 'charset=value');
+
+ // `set()` with a mixed-case name overwrites the same parameter rather than
+ // adding a second, case-colliding one.
+ const params = new MIMEType('text/plain').params;
+ params.set('Foo', 'a');
+ params.set('foo', 'b');
+ assert.deepStrictEqual([...params], [['foo', 'b']]);
+ assert.strictEqual(`${params}`, 'foo=b');
+
+ // `delete()` is case-insensitive as well.
+ params.delete('FOO');
+ assert.strictEqual(params.has('foo'), false);
+ assert.deepStrictEqual([...params], []);
+}
diff --git a/tools/dep_updaters/update-nixpkgs-pin.sh b/tools/dep_updaters/update-nixpkgs-pin.sh
index eb5fde1526ab0c..a025676e20a7b5 100755
--- a/tools/dep_updaters/update-nixpkgs-pin.sh
+++ b/tools/dep_updaters/update-nixpkgs-pin.sh
@@ -1,10 +1,11 @@
#!/bin/sh
set -ex
# Shell script to update Nixpkgs pin in the source tree to the most recent
-# version on the unstable channel.
+# version on the unstable channel, and the 26.05 one for Intel Mac support.
BASE_DIR=$(cd "$(dirname "$0")/../.." && pwd)
NIXPKGS_PIN_FILE="$BASE_DIR/tools/nix/pkgs.nix"
+NIXPKGS_COMPAT_PIN_FILE="$BASE_DIR/tools/nix/pkgs-26.05.nix"
OPENSSL_MATRIX_FILE="$BASE_DIR/tools/nix/openssl-matrix.nix"
NIXPKGS_REPO=$(grep 'repo =' "$NIXPKGS_PIN_FILE" | awk -F'"' '{ print $2 }')
@@ -19,12 +20,27 @@ NEW_VERSION=$(echo "$NEW_UPSTREAM_SHA1" | head -c 35)
compare_dependency_version "nixpkgs-unstable" "$CURRENT_VERSION_SHA1" "$NEW_UPSTREAM_SHA1"
-CURRENT_TARBALL_HASH=$(grep 'sha256 =' "$NIXPKGS_PIN_FILE" | awk -F'"' '{ print $2 }')
-NEW_TARBALL_HASH=$(nix-prefetch-url --unpack "$NIXPKGS_REPO/archive/$NEW_UPSTREAM_SHA1.tar.gz")
+update_pkgs_file() {
+ PIN_FILE=$1
+ PREVIOUS_SHA1=$2
+ UPSTREAM_SHA1=$3
-TMP_FILE=$(mktemp)
-sed "s/$CURRENT_VERSION_SHA1/$NEW_UPSTREAM_SHA1/;s/$CURRENT_TARBALL_HASH/$NEW_TARBALL_HASH/" "$NIXPKGS_PIN_FILE" > "$TMP_FILE"
-mv "$TMP_FILE" "$NIXPKGS_PIN_FILE"
+ CURRENT_TARBALL_HASH=$(grep 'sha256 =' "$PIN_FILE" | awk -F'"' '{ print $2 }')
+ NEW_TARBALL_HASH=$(nix-prefetch-url --unpack "$NIXPKGS_REPO/archive/$UPSTREAM_SHA1.tar.gz")
+
+ TMP_FILE=$(mktemp)
+ sed "s/$PREVIOUS_SHA1/$UPSTREAM_SHA1/;s/$CURRENT_TARBALL_HASH/$NEW_TARBALL_HASH/" "$PIN_FILE" > "$TMP_FILE"
+ mv "$TMP_FILE" "$PIN_FILE"
+}
+
+update_pkgs_file "$NIXPKGS_PIN_FILE" "$CURRENT_VERSION_SHA1" "$NEW_UPSTREAM_SHA1"
+
+# Unstable channel no longer supports Intel architecture for macOS. We can use the 26.05 channel
+# to keep testing on that platform for a little longer.
+# TODO: remove this when 26.05 is EOL (end of 2026)
+COMPAT_VERSION_SHA1=$(grep 'rev =' "$NIXPKGS_COMPAT_PIN_FILE" | awk -F'"' '{ print $2 }')
+COMPAT_UPSTREAM_SHA1=$(git ls-remote "$NIXPKGS_REPO.git" nixpkgs-26.05-darwin | awk '{print $1}')
+update_pkgs_file "$NIXPKGS_COMPAT_PIN_FILE" "$COMPAT_VERSION_SHA1" "$COMPAT_UPSTREAM_SHA1"
nix-instantiate -I "nixpkgs=$NIXPKGS_PIN_FILE" --eval --strict --json -E "
let
diff --git a/tools/nix/pkgs-26.05.nix b/tools/nix/pkgs-26.05.nix
new file mode 100644
index 00000000000000..cbee28de68e2f4
--- /dev/null
+++ b/tools/nix/pkgs-26.05.nix
@@ -0,0 +1,13 @@
+arg:
+let
+ repo = "https://github.com/NixOS/nixpkgs";
+ rev = "fad15a0c9ebf6432dcba932743decbf8905cb024";
+ nixpkgs = import (builtins.fetchTarball {
+ url = "${repo}/archive/${rev}.tar.gz";
+ sha256 = "0q845gkz18nz463cwmkhvficl3jpncdvvnx8xlrj5gdp4c285rcn";
+ }) arg;
+in
+# Unstable channel no longer supports Intel architecture for macOS. We can use the 26.05 channel
+# to keep testing on that platform for a little longer.
+# TODO: remove this file when 26.05 is EOL (end of 2026)
+nixpkgs
diff --git a/tools/nix/pkgs.nix b/tools/nix/pkgs.nix
index 09678d12da7fc5..62d48ecc98c776 100644
--- a/tools/nix/pkgs.nix
+++ b/tools/nix/pkgs.nix
@@ -7,4 +7,7 @@ let
sha256 = "0ffi2k8hllbgqw9vvdfaxw88qz53gl7myll4lwri6ynq5l0lnvbc";
}) arg;
in
-nixpkgs
+# Unstable channel no longer supports Intel architecture for macOS. We can use the 26.05 channel
+# to keep testing on that platform for a little longer.
+# TODO: remove this when 26.05 is EOL (end of 2026)
+if builtins.currentSystem == "x86_64-darwin" then (import ./pkgs-26.05.nix arg) else nixpkgs