diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c6cf4ac5367bb..ad71ee7c44fd4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,15 +1,15 @@ { - ".": "11.5.2", - "workspaces/arborist": "9.1.3", + ".": "11.6.0", + "workspaces/arborist": "9.1.4", "workspaces/libnpmaccess": "10.0.1", - "workspaces/libnpmdiff": "8.0.6", - "workspaces/libnpmexec": "10.1.5", - "workspaces/libnpmfund": "7.0.6", + "workspaces/libnpmdiff": "8.0.7", + "workspaces/libnpmexec": "10.1.6", + "workspaces/libnpmfund": "7.0.7", "workspaces/libnpmorg": "8.0.0", - "workspaces/libnpmpack": "9.0.6", + "workspaces/libnpmpack": "9.0.7", "workspaces/libnpmpublish": "11.1.0", "workspaces/libnpmsearch": "9.0.0", "workspaces/libnpmteam": "8.0.1", "workspaces/libnpmversion": "8.0.1", - "workspaces/config": "10.3.1" + "workspaces/config": "10.4.0" } diff --git a/AUTHORS b/AUTHORS index 2034d1e5631e9..f59176cb9bdc1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -971,3 +971,7 @@ sam crochet tarekwfa0110 <109884541+tarekwfa0110@users.noreply.github.com> Marc Bernard Gareth Jones <3151613+G-Rath@users.noreply.github.com> +Aaron Jensen +Jeepsboucher <42554351+Jeepsboucher@users.noreply.github.com> +Arkadiusz Czekajski +Liam Mitchell diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b95117e98482..c2410ffc175bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [11.6.0](https://github.com/npm/cli/compare/v11.5.2...v11.6.0) (2025-09-03) +### Features +* [`bdcc10d`](https://github.com/npm/cli/commit/bdcc10d9f848940987b3d326ccd4673fab2bcfef) [#8359](https://github.com/npm/cli/pull/8359) add support for optional env var replacements in .npmrc (#8359) (@aczekajski, @owlstronaut) +### Bug Fixes +* [`dd4cee9`](https://github.com/npm/cli/commit/dd4cee9026c8e2dd5e4c28fd45ac8bceae74fb89) [#8539](https://github.com/npm/cli/pull/8539) powershell: improve argument parsing (#8539) (@alexsch01) +* [`5f18557`](https://github.com/npm/cli/commit/5f1855778b5e376c5f1389e0ee5f204dc86c4d32) [#8532](https://github.com/npm/cli/pull/8532) powershell: fix issue with modified InvocationName (#8532) (@alexsch01) +* [`9e5abf1`](https://github.com/npm/cli/commit/9e5abf19b93359881b2035bc371e09794a1dad01) [#8529](https://github.com/npm/cli/pull/8529) add redaction to log format egress (#8529) (@wraithgar) +* [`75ce64a`](https://github.com/npm/cli/commit/75ce64a5b21b806be203b97f35a48497b4afcb56) [#8524](https://github.com/npm/cli/pull/8524) revert handle signal exits gracefully (#8524) (@owlstronaut) +* [`5d82d0b`](https://github.com/npm/cli/commit/5d82d0b4a4bd1424031fb68b4df740c1bbe5b172) [#8469](https://github.com/npm/cli/pull/8469) ps1 scripts in powershell 5.1 (#8469) (@splatteredbits) + + +### Dependencies + +* [workspace](https://github.com/npm/cli/releases/tag/arborist-v9.1.4): `@npmcli/arborist@9.1.4` +* [workspace](https://github.com/npm/cli/releases/tag/config-v10.4.0): `@npmcli/config@10.4.0` +* [workspace](https://github.com/npm/cli/releases/tag/libnpmdiff-v8.0.7): `libnpmdiff@8.0.7` +* [workspace](https://github.com/npm/cli/releases/tag/libnpmexec-v10.1.6): `libnpmexec@10.1.6` +* [workspace](https://github.com/npm/cli/releases/tag/libnpmfund-v7.0.7): `libnpmfund@7.0.7` +* [workspace](https://github.com/npm/cli/releases/tag/libnpmpack-v9.0.7): `libnpmpack@9.0.7` + ## [11.5.2](https://github.com/npm/cli/compare/v11.5.1...v11.5.2) (2025-07-30) ### Bug Fixes * [`7d900c4`](https://github.com/npm/cli/commit/7d900c4656cfffc8cca93240c6cda4b441fbbfaa) [#8467](https://github.com/npm/cli/pull/8467) oidc visibility check for provenance (#8467) (@reggi, @wraithgar) diff --git a/bin/npm.ps1 b/bin/npm.ps1 index 5993adaf55662..efed03fe5655e 100644 --- a/bin/npm.ps1 +++ b/bin/npm.ps1 @@ -1,5 +1,7 @@ #!/usr/bin/env pwsh +Set-StrictMode -Version 'Latest' + $NODE_EXE="$PSScriptRoot/node.exe" if (-not (Test-Path $NODE_EXE)) { $NODE_EXE="$PSScriptRoot/node" @@ -27,7 +29,7 @@ if ($MyInvocation.ExpectingInput) { # takes pipeline input } elseif (-not $MyInvocation.Line) { # used "-File" argument & $NODE_EXE $NPM_CLI_JS $args } else { # used "-Command" argument - if ($MyInvocation.Statement) { + if (($MyInvocation | Get-Member -Name 'Statement') -and $MyInvocation.Statement) { $NPM_ORIGINAL_COMMAND = $MyInvocation.Statement } else { $NPM_ORIGINAL_COMMAND = ( @@ -38,9 +40,9 @@ if ($MyInvocation.ExpectingInput) { # takes pipeline input $NODE_EXE = $NODE_EXE.Replace("``", "````") $NPM_CLI_JS = $NPM_CLI_JS.Replace("``", "````") - $NPM_NO_REDIRECTS_COMMAND = [Management.Automation.Language.Parser]::ParseInput($NPM_ORIGINAL_COMMAND, [ref] $null, [ref] $null). - EndBlock.Statements.PipelineElements.CommandElements.Extent.Text -join ' ' - $NPM_ARGS = $NPM_NO_REDIRECTS_COMMAND.Substring($MyInvocation.InvocationName.Length).Trim() + $NPM_COMMAND_ARRAY = [Management.Automation.Language.Parser]::ParseInput($NPM_ORIGINAL_COMMAND, [ref] $null, [ref] $null). + EndBlock.Statements.PipelineElements.CommandElements.Extent.Text + $NPM_ARGS = ($NPM_COMMAND_ARRAY | Select-Object -Skip 1) -join ' ' Invoke-Expression "& `"$NODE_EXE`" `"$NPM_CLI_JS`" $NPM_ARGS" } diff --git a/bin/npx.ps1 b/bin/npx.ps1 index cc1aa047bdc21..3fe7b5435763a 100644 --- a/bin/npx.ps1 +++ b/bin/npx.ps1 @@ -1,5 +1,7 @@ #!/usr/bin/env pwsh +Set-StrictMode -Version 'Latest' + $NODE_EXE="$PSScriptRoot/node.exe" if (-not (Test-Path $NODE_EXE)) { $NODE_EXE="$PSScriptRoot/node" @@ -27,7 +29,7 @@ if ($MyInvocation.ExpectingInput) { # takes pipeline input } elseif (-not $MyInvocation.Line) { # used "-File" argument & $NODE_EXE $NPX_CLI_JS $args } else { # used "-Command" argument - if ($MyInvocation.Statement) { + if (($MyInvocation | Get-Member -Name 'Statement') -and $MyInvocation.Statement) { $NPX_ORIGINAL_COMMAND = $MyInvocation.Statement } else { $NPX_ORIGINAL_COMMAND = ( @@ -38,9 +40,9 @@ if ($MyInvocation.ExpectingInput) { # takes pipeline input $NODE_EXE = $NODE_EXE.Replace("``", "````") $NPX_CLI_JS = $NPX_CLI_JS.Replace("``", "````") - $NPX_NO_REDIRECTS_COMMAND = [Management.Automation.Language.Parser]::ParseInput($NPX_ORIGINAL_COMMAND, [ref] $null, [ref] $null). - EndBlock.Statements.PipelineElements.CommandElements.Extent.Text -join ' ' - $NPX_ARGS = $NPX_NO_REDIRECTS_COMMAND.Substring($MyInvocation.InvocationName.Length).Trim() + $NPX_COMMAND_ARRAY = [Management.Automation.Language.Parser]::ParseInput($NPX_ORIGINAL_COMMAND, [ref] $null, [ref] $null). + EndBlock.Statements.PipelineElements.CommandElements.Extent.Text + $NPX_ARGS = ($NPX_COMMAND_ARRAY | Select-Object -Skip 1) -join ' ' Invoke-Expression "& `"$NODE_EXE`" `"$NPX_CLI_JS`" $NPX_ARGS" } diff --git a/docs/lib/content/configuring-npm/npmrc.md b/docs/lib/content/configuring-npm/npmrc.md index cd31ae886f132..47e126f3c3ab0 100644 --- a/docs/lib/content/configuring-npm/npmrc.md +++ b/docs/lib/content/configuring-npm/npmrc.md @@ -25,11 +25,14 @@ The four relevant files are: * npm builtin config file (`/path/to/npm/npmrc`) All npm config files are an ini-formatted list of `key = value` parameters. -Environment variables can be replaced using `${VARIABLE_NAME}`. For +Environment variables can be replaced using `${VARIABLE_NAME}`. By default +if the variable is not defined, it is left unreplaced. By adding `?` after +variable name they can be forced to evaluate to an empty string instead. For example: ```bash cache = ${HOME}/.npm-packages +node-options = "${NODE_OPTIONS?} --use-system-ca" ``` Each of these files is loaded, and config options are resolved in priority diff --git a/lib/cli/exit-handler.js b/lib/cli/exit-handler.js index efb09138aec28..e76b08c80a635 100644 --- a/lib/cli/exit-handler.js +++ b/lib/cli/exit-handler.js @@ -43,16 +43,6 @@ class ExitHandler { registerUncaughtHandlers () { this.#process.on('uncaughtException', this.#handleExit) this.#process.on('unhandledRejection', this.#handleExit) - - // Handle signals that might bypass normal exit flow - // These signals can cause the process to exit without calling the exit handler - const signalsToHandle = ['SIGTERM', 'SIGINT', 'SIGHUP'] - for (const signal of signalsToHandle) { - this.#process.on(signal, () => { - // Call the exit handler to ensure proper cleanup - this.#handleExit(new Error(`Process received ${signal}`)) - }) - } } exit (err) { @@ -67,17 +57,6 @@ class ExitHandler { this.#process.off('exit', this.#handleProcesExitAndReset) this.#process.off('uncaughtException', this.#handleExit) this.#process.off('unhandledRejection', this.#handleExit) - - const signalsToCleanup = ['SIGTERM', 'SIGINT', 'SIGHUP'] - for (const signal of signalsToCleanup) { - try { - this.#process.off(signal, this.#handleExit) - } catch (err) { - // Ignore errors during cleanup - this is defensive programming for edge cases - // where the process object might be in an unexpected state during shutdown - } - } - if (this.#loaded) { this.#npm.unload() } diff --git a/lib/utils/format.js b/lib/utils/format.js index aaecfe1ba0e7a..9216c7918678a 100644 --- a/lib/utils/format.js +++ b/lib/utils/format.js @@ -1,4 +1,7 @@ +// All logging goes through here, both to console and log files + const { formatWithOptions: baseFormatWithOptions } = require('node:util') +const { redactLog } = require('@npmcli/redact') // These are most assuredly not a mistake // https://eslint.org/docs/latest/rules/no-control-regex @@ -40,7 +43,7 @@ function STRIP_C01 (str) { const formatWithOptions = ({ prefix: prefixes = [], eol = '\n', ...options }, ...args) => { const prefix = prefixes.filter(p => p != null).join(' ') - const formatted = STRIP_C01(baseFormatWithOptions(options, ...args)) + const formatted = redactLog(STRIP_C01(baseFormatWithOptions(options, ...args))) // Splitting could be changed to only `\n` once we are sure we only emit unix newlines. // The eol param to this function will put the correct newlines in place for the returned string. const lines = formatted.split(/\r?\n/) diff --git a/package-lock.json b/package-lock.json index 585eb9b7943d8..47eb40016b90e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "npm", - "version": "11.5.2", + "version": "11.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "npm", - "version": "11.5.2", + "version": "11.6.0", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -85,8 +85,8 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.3", - "@npmcli/config": "^10.3.1", + "@npmcli/arborist": "^9.1.4", + "@npmcli/config": "^10.4.0", "@npmcli/fs": "^4.0.0", "@npmcli/map-workspaces": "^4.0.2", "@npmcli/package-json": "^6.2.0", @@ -110,11 +110,11 @@ "is-cidr": "^5.1.1", "json-parse-even-better-errors": "^4.0.0", "libnpmaccess": "^10.0.1", - "libnpmdiff": "^8.0.6", - "libnpmexec": "^10.1.5", - "libnpmfund": "^7.0.6", + "libnpmdiff": "^8.0.7", + "libnpmexec": "^10.1.6", + "libnpmfund": "^7.0.7", "libnpmorg": "^8.0.0", - "libnpmpack": "^9.0.6", + "libnpmpack": "^9.0.7", "libnpmpublish": "^11.1.0", "libnpmsearch": "^9.0.0", "libnpmteam": "^8.0.1", @@ -18820,7 +18820,7 @@ }, "workspaces/arborist": { "name": "@npmcli/arborist", - "version": "9.1.3", + "version": "9.1.4", "license": "ISC", "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", @@ -18878,7 +18878,7 @@ }, "workspaces/config": { "name": "@npmcli/config", - "version": "10.3.1", + "version": "10.4.0", "license": "ISC", "dependencies": { "@npmcli/map-workspaces": "^4.0.1", @@ -18918,10 +18918,10 @@ } }, "workspaces/libnpmdiff": { - "version": "8.0.6", + "version": "8.0.7", "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.3", + "@npmcli/arborist": "^9.1.4", "@npmcli/installed-package-contents": "^3.0.0", "binary-extensions": "^3.0.0", "diff": "^7.0.0", @@ -18940,10 +18940,10 @@ } }, "workspaces/libnpmexec": { - "version": "10.1.5", + "version": "10.1.6", "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.3", + "@npmcli/arborist": "^9.1.4", "@npmcli/package-json": "^6.1.1", "@npmcli/run-script": "^9.0.1", "ci-info": "^4.0.0", @@ -18970,10 +18970,10 @@ } }, "workspaces/libnpmfund": { - "version": "7.0.6", + "version": "7.0.7", "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.3" + "@npmcli/arborist": "^9.1.4" }, "devDependencies": { "@npmcli/eslint-config": "^5.0.1", @@ -19003,10 +19003,10 @@ } }, "workspaces/libnpmpack": { - "version": "9.0.6", + "version": "9.0.7", "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.3", + "@npmcli/arborist": "^9.1.4", "@npmcli/run-script": "^9.0.1", "npm-package-arg": "^12.0.0", "pacote": "^21.0.0" diff --git a/package.json b/package.json index b31a981f52068..76ebe1ab9c6c7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "11.5.2", + "version": "11.6.0", "name": "npm", "description": "a package manager for JavaScript", "workspaces": [ @@ -52,8 +52,8 @@ }, "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.3", - "@npmcli/config": "^10.3.1", + "@npmcli/arborist": "^9.1.4", + "@npmcli/config": "^10.4.0", "@npmcli/fs": "^4.0.0", "@npmcli/map-workspaces": "^4.0.2", "@npmcli/package-json": "^6.2.0", @@ -77,11 +77,11 @@ "is-cidr": "^5.1.1", "json-parse-even-better-errors": "^4.0.0", "libnpmaccess": "^10.0.1", - "libnpmdiff": "^8.0.6", - "libnpmexec": "^10.1.5", - "libnpmfund": "^7.0.6", + "libnpmdiff": "^8.0.7", + "libnpmexec": "^10.1.6", + "libnpmfund": "^7.0.7", "libnpmorg": "^8.0.0", - "libnpmpack": "^9.0.6", + "libnpmpack": "^9.0.7", "libnpmpublish": "^11.1.0", "libnpmsearch": "^9.0.0", "libnpmteam": "^8.0.1", diff --git a/test/lib/cli/exit-handler.js b/test/lib/cli/exit-handler.js index f8b112beab0a2..484704c735279 100644 --- a/test/lib/cli/exit-handler.js +++ b/test/lib/cli/exit-handler.js @@ -4,7 +4,7 @@ const EventEmitter = require('node:events') const os = require('node:os') const t = require('tap') const fsMiniPass = require('fs-minipass') -const { output, time, log } = require('proc-log') +const { output, time } = require('proc-log') const errorMessage = require('../../../lib/utils/error-message.js') const ExecCommand = require('../../../lib/commands/exec.js') const { load: loadMockNpm } = require('../../fixtures/mock-npm') @@ -707,136 +707,3 @@ t.test('do no fancy handling for shellouts', async t => { }) }) }) - -t.test('container scenarios that trigger exit handler bug', async t => { - t.test('process.exit() called before exit handler cleanup', async (t) => { - // Simulates when npm process exits directly without going through proper cleanup - - let exitHandlerNeverCalledLogged = false - let npmBugReportLogged = false - - await mockExitHandler(t, { - config: { loglevel: 'notice' }, - }) - - // Override log.error to capture the specific error messages - const originalLogError = log.error - log.error = (prefix, msg) => { - if (msg === 'Exit handler never called!') { - exitHandlerNeverCalledLogged = true - } - if (msg === 'This is an error with npm itself. Please report this error at:') { - npmBugReportLogged = true - } - return originalLogError(prefix, msg) - } - - t.teardown(() => { - log.error = originalLogError - }) - - // This happens when containers are stopped/killed before npm can clean up properly - process.emit('exit', 1) - - // Verify the bug is detected and logged correctly - t.equal(exitHandlerNeverCalledLogged, true, 'should log "Exit handler never called!" error') - t.equal(npmBugReportLogged, true, 'should log npm bug report message') - }) - - t.test('SIGTERM signal is handled properly', (t) => { - // This test verifies that our fix handles SIGTERM signals - - const ExitHandler = tmock(t, '{LIB}/cli/exit-handler.js') - const exitHandler = new ExitHandler({ process }) - - const initialSigtermCount = process.listeners('SIGTERM').length - const initialSigintCount = process.listeners('SIGINT').length - const initialSighupCount = process.listeners('SIGHUP').length - - // Register signal handlers - exitHandler.registerUncaughtHandlers() - - const finalSigtermCount = process.listeners('SIGTERM').length - const finalSigintCount = process.listeners('SIGINT').length - const finalSighupCount = process.listeners('SIGHUP').length - - // Verify the fix: signal handlers should be registered - t.ok(finalSigtermCount > initialSigtermCount, 'SIGTERM handler should be registered') - t.ok(finalSigintCount > initialSigintCount, 'SIGINT handler should be registered') - t.ok(finalSighupCount > initialSighupCount, 'SIGHUP handler should be registered') - - // Clean up listeners to avoid affecting other tests - const sigtermListeners = process.listeners('SIGTERM') - const sigintListeners = process.listeners('SIGINT') - const sighupListeners = process.listeners('SIGHUP') - - for (const listener of sigtermListeners) { - process.removeListener('SIGTERM', listener) - } - for (const listener of sigintListeners) { - process.removeListener('SIGINT', listener) - } - for (const listener of sighupListeners) { - process.removeListener('SIGHUP', listener) - } - - t.end() - }) - - t.test('signal handler execution', async (t) => { - const ExitHandler = tmock(t, '{LIB}/cli/exit-handler.js') - const exitHandler = new ExitHandler({ process }) - - // Register signal handlers - exitHandler.registerUncaughtHandlers() - - process.emit('SIGTERM') - process.emit('SIGINT') - process.emit('SIGHUP') - - // Clean up listeners - process.removeAllListeners('SIGTERM') - process.removeAllListeners('SIGINT') - process.removeAllListeners('SIGHUP') - - t.pass('signal handlers executed successfully') - t.end() - }) - - t.test('hanging async operation interrupted by signal', async (t) => { - // This test simulates the scenario where npm hangs on a long operation and receives SIGTERM/SIGKILL before it can complete - - let exitHandlerNeverCalledLogged = false - - const { exitHandler } = await mockExitHandler(t, { - config: { loglevel: 'notice' }, - }) - - // Override log.error to detect the bug message - const originalLogError = log.error - log.error = (prefix, msg) => { - if (msg === 'Exit handler never called!') { - exitHandlerNeverCalledLogged = true - } - return originalLogError(prefix, msg) - } - - t.teardown(() => { - log.error = originalLogError - }) - - // Track if exit handler was called properly - let exitHandlerCalled = false - exitHandler.exit = () => { - exitHandlerCalled = true - } - - // Simulate sending signal to the process without proper cleanup - // This mimics what happens when a container is terminated - process.emit('exit', 1) - - // Verify the bug conditions - t.equal(exitHandlerCalled, false, 'exit handler should not be called in this scenario') - t.equal(exitHandlerNeverCalledLogged, true, 'should detect and log the exit handler bug') - }) -}) diff --git a/test/lib/utils/display.js b/test/lib/utils/display.js index 78bffa0221d03..26f52b17a8528 100644 --- a/test/lib/utils/display.js +++ b/test/lib/utils/display.js @@ -37,7 +37,9 @@ t.test('can log cleanly', async (t) => { const { log, logs } = await mockDisplay(t) log.error('', 'test\x00message') + log.info('', 'fetch DELETE 200 https://registry.npmjs.org/-/user/token/npm_000000000000000000000000000000000000 477ms') t.match(logs.error, ['test^@message']) + t.match(logs.info, ['fetch DELETE 200 https://registry.npmjs.org/-/user/token/npm_*** 477ms']) }) t.test('can handle special eresolves', async (t) => { diff --git a/workspaces/arborist/CHANGELOG.md b/workspaces/arborist/CHANGELOG.md index 64faff64629f1..36bf08f22ee8a 100644 --- a/workspaces/arborist/CHANGELOG.md +++ b/workspaces/arborist/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [9.1.4](https://github.com/npm/cli/compare/arborist-v9.1.3...arborist-v9.1.4) (2025-09-03) +### Bug Fixes +* [`208c06e`](https://github.com/npm/cli/commit/208c06e91a187b03d6bdd75bff4e4285b365750c) [#8448](https://github.com/npm/cli/pull/8448) peer edge crash due to no parent or detached node (#8448) (@milaninfy) +* [`3b54e9c`](https://github.com/npm/cli/commit/3b54e9c59c6dba342d2931cce6458a755e55960e) [#8534](https://github.com/npm/cli/pull/8534) installLinks works with transitive external file dependencies (#8534) (@owlstronaut) +* [`ed71acb`](https://github.com/npm/cli/commit/ed71acb89fc3883e735987cc9be77efc2daff26a) [#8473](https://github.com/npm/cli/pull/8473) arborist: #8472 Keeps the registry protocol when modifying resolve URL (#8473) (@Jeepsboucher, Jean-Philippe Boucher) +### Chores +* [`619d43e`](https://github.com/npm/cli/commit/619d43e54ef7408d4ee6b38a776262b5132829b6) [#8540](https://github.com/npm/cli/pull/8540) fix pruner and reify tests for optional peer deps (#8540) (@liamcmitchell, Liam Mitchell) + ## [9.1.3](https://github.com/npm/cli/compare/arborist-v9.1.2...arborist-v9.1.3) (2025-07-24) ### Bug Fixes * [`6dbe21a`](https://github.com/npm/cli/commit/6dbe21ab659c4e32657fec63fc58bb3f4992f4f1) [#8436](https://github.com/npm/cli/pull/8436) local transitive dependencies with --install-links=true (@owlstronaut) diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index 1edd0b643b60d..281f62b116bd3 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -1238,15 +1238,19 @@ This is a one-time fix-up, please be patient... // Check if the target is within the project root isProjectInternalFileSpec = targetPath.startsWith(resolvedProjectRoot + sep) || targetPath === resolvedProjectRoot } + + // When using --install-links, we need to handle transitive file dependencies specially + // If the parent was installed (not linked) due to --install-links, and this is a file: dep, we should also install it rather than link it + const parentWasInstalled = parent && !parent.isLink && parent.resolved?.startsWith('file:') + const isTransitiveFileDep = spec.type === 'directory' && parentWasInstalled && installLinks + // Decide whether to link or copy the dependency - const shouldLink = isWorkspace || isProjectInternalFileSpec || !installLinks + const shouldLink = (isWorkspace || isProjectInternalFileSpec || !installLinks) && !isTransitiveFileDep if (spec.type === 'directory' && shouldLink) { return this.#linkFromSpec(name, spec, parent, edge) } - // if the spec matches a workspace name, then see if the workspace node will - // satisfy the edge. if it does, we return the workspace node to make sure it - // takes priority. + // if the spec matches a workspace name, then see if the workspace node will satisfy the edge. if it does, we return the workspace node to make sure it takes priority. if (isWorkspace) { const existingNode = this.idealTree.edgesOut.get(spec.name).to if (existingNode && existingNode.isWorkspace && existingNode.satisfies(edge)) { @@ -1254,6 +1258,15 @@ This is a one-time fix-up, please be patient... } } + // For file: dependencies that we're installing (not linking), ensure proper resolution + if (isTransitiveFileDep && edge) { + // For transitive file deps, resolve relative to the parent's original source location + const parentOriginalPath = parent.resolved.slice(5) // Remove 'file:' prefix + const relativePath = edge.rawSpec.slice(5) // Remove 'file:' prefix + const absolutePath = resolve(parentOriginalPath, relativePath) + spec = npa.resolve(name, `file:${absolutePath}`) + } + // spec isn't a directory, and either isn't a workspace or the workspace we have // doesn't satisfy the edge. try to fetch a manifest and build a node from that. return this.#fetchManifest(spec) @@ -1306,6 +1319,12 @@ This is a one-time fix-up, please be patient... .sort(({ name: a }, { name: b }) => localeCompare(a, b)) for (const edge of peerEdges) { + // node.parent gets mutated during loop execution due to recursive #nodeFromEdge calls. + // When a compatible peer is found (e.g. a@1.1.0 replaces a@1.2.0), the original node loses its parent. + // if node is detached/removed from the tree, or has no parent, so no need to check remaining edgesOut for that node. + if (!node.parent) { + break + } // already placed this one, and we're happy with it. if (edge.valid && edge.to) { continue diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index 7f3fa461b0667..5da8e72bfa567 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -885,6 +885,7 @@ module.exports = cls => class Reifier extends cls { // Replace the host with the registry host while keeping the path intact resolvedURL.hostname = registryURL.hostname resolvedURL.port = registryURL.port + resolvedURL.protocol = registryURL.protocol // Make sure we don't double-include the path if it's already there const registryPath = registryURL.pathname.replace(/\/$/, '') diff --git a/workspaces/arborist/package.json b/workspaces/arborist/package.json index 3f9282e99a55c..7e98d0e7d7571 100644 --- a/workspaces/arborist/package.json +++ b/workspaces/arborist/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/arborist", - "version": "9.1.3", + "version": "9.1.4", "description": "Manage node_modules trees", "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", diff --git a/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs b/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs index 855539521b9df..f76c505e89ff2 100644 --- a/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs @@ -74672,6 +74672,315 @@ exports[`test/arborist/build-ideal-tree.js TAP more peer dep conflicts metadeps Array [] ` +exports[`test/arborist/build-ideal-tree.js TAP more peer dep conflicts peerDep replacement of top level dep with different version resulting detached top level dep > default result 1`] = ` +ArboristNode { + "children": Map { + "@test/a" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/a", + "spec": "^1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/b", + "name": "@test/a", + "spec": "1.1.0", + "type": "peer", + }, + }, + "edgesOut": Map { + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "peerOptional", + }, + "@test/c" => EdgeOut { + "name": "@test/c", + "spec": "1.1.0", + "to": null, + "type": "peerOptional", + }, + "lodash" => EdgeOut { + "name": "lodash", + "spec": "^4.17.0", + "to": null, + "type": "peerOptional", + }, + "uniq" => EdgeOut { + "name": "uniq", + "spec": "^1.0.0", + "to": null, + "type": "peerOptional", + }, + }, + "location": "node_modules/@test/a", + "name": "@test/a", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/a", + "resolved": "http://localhost:4873/@test/a/-/a-1.1.0.tgz", + "version": "1.1.0", + }, + "@test/b" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/b", + "spec": "1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/a", + "name": "@test/b", + "spec": "1.1.0", + "type": "peerOptional", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "1.1.0", + "to": "node_modules/@test/a", + "type": "peer", + }, + }, + "location": "node_modules/@test/b", + "name": "@test/b", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/b", + "resolved": "http://localhost:4873/@test/b/-/b-1.1.0.tgz", + "version": "1.1.0", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "^1.1.0", + "to": "node_modules/@test/a", + "type": "dev", + }, + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "dev", + }, + }, + "isProjectRoot": true, + "location": "", + "name": "tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", +} +` + +exports[`test/arborist/build-ideal-tree.js TAP more peer dep conflicts peerDep replacement of top level dep with different version resulting detached top level dep > force result 1`] = ` +ArboristNode { + "children": Map { + "@test/a" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/a", + "spec": "^1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/b", + "name": "@test/a", + "spec": "1.1.0", + "type": "peer", + }, + }, + "edgesOut": Map { + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "peerOptional", + }, + "@test/c" => EdgeOut { + "name": "@test/c", + "spec": "1.1.0", + "to": null, + "type": "peerOptional", + }, + "lodash" => EdgeOut { + "name": "lodash", + "spec": "^4.17.0", + "to": null, + "type": "peerOptional", + }, + "uniq" => EdgeOut { + "name": "uniq", + "spec": "^1.0.0", + "to": null, + "type": "peerOptional", + }, + }, + "location": "node_modules/@test/a", + "name": "@test/a", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/a", + "resolved": "http://localhost:4873/@test/a/-/a-1.1.0.tgz", + "version": "1.1.0", + }, + "@test/b" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/b", + "spec": "1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/a", + "name": "@test/b", + "spec": "1.1.0", + "type": "peerOptional", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "1.1.0", + "to": "node_modules/@test/a", + "type": "peer", + }, + }, + "location": "node_modules/@test/b", + "name": "@test/b", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/b", + "resolved": "http://localhost:4873/@test/b/-/b-1.1.0.tgz", + "version": "1.1.0", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "^1.1.0", + "to": "node_modules/@test/a", + "type": "dev", + }, + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "dev", + }, + }, + "isProjectRoot": true, + "location": "", + "name": "tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", +} +` + +exports[`test/arborist/build-ideal-tree.js TAP more peer dep conflicts peerDep replacement of top level dep with different version resulting detached top level dep > strict result 1`] = ` +ArboristNode { + "children": Map { + "@test/a" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/a", + "spec": "^1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/b", + "name": "@test/a", + "spec": "1.1.0", + "type": "peer", + }, + }, + "edgesOut": Map { + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "peerOptional", + }, + "@test/c" => EdgeOut { + "name": "@test/c", + "spec": "1.1.0", + "to": null, + "type": "peerOptional", + }, + "lodash" => EdgeOut { + "name": "lodash", + "spec": "^4.17.0", + "to": null, + "type": "peerOptional", + }, + "uniq" => EdgeOut { + "name": "uniq", + "spec": "^1.0.0", + "to": null, + "type": "peerOptional", + }, + }, + "location": "node_modules/@test/a", + "name": "@test/a", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/a", + "resolved": "http://localhost:4873/@test/a/-/a-1.1.0.tgz", + "version": "1.1.0", + }, + "@test/b" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/b", + "spec": "1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/a", + "name": "@test/b", + "spec": "1.1.0", + "type": "peerOptional", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "1.1.0", + "to": "node_modules/@test/a", + "type": "peer", + }, + }, + "location": "node_modules/@test/b", + "name": "@test/b", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/b", + "resolved": "http://localhost:4873/@test/b/-/b-1.1.0.tgz", + "version": "1.1.0", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "^1.1.0", + "to": "node_modules/@test/a", + "type": "dev", + }, + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "dev", + }, + }, + "isProjectRoot": true, + "location": "", + "name": "tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", +} +` + exports[`test/arborist/build-ideal-tree.js TAP more peer dep conflicts prod dep directly on conflicted peer, full peer set, newer > force result 1`] = ` ArboristNode { "children": Map { diff --git a/workspaces/arborist/tap-snapshots/test/arborist/load-actual.js.test.cjs b/workspaces/arborist/tap-snapshots/test/arborist/load-actual.js.test.cjs index 9eaf17e86887c..35ba9f7cafa84 100644 --- a/workspaces/arborist/tap-snapshots/test/arborist/load-actual.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/arborist/load-actual.js.test.cjs @@ -7784,4 +7784,4 @@ ArboristNode { "name": "yarn-lock-mkdirp-file-dep", "path": "yarn-lock-mkdirp-file-dep", } -` \ No newline at end of file +` diff --git a/workspaces/arborist/tap-snapshots/test/arborist/pruner.js.test.cjs b/workspaces/arborist/tap-snapshots/test/arborist/pruner.js.test.cjs index 2c5323fc59d3c..16c732a8b5600 100644 --- a/workspaces/arborist/tap-snapshots/test/arborist/pruner.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/arborist/pruner.js.test.cjs @@ -124,82 +124,6 @@ ArboristNode { } ` -exports[`test/arborist/pruner.js TAP prune with lockfile with implicit optional peer dependencies > should remove all deps from reified tree 1`] = ` -ArboristNode { - "children": Map { - "dedent" => ArboristNode { - "edgesIn": Set { - EdgeIn { - "from": "", - "name": "dedent", - "spec": "^1.6.0", - "type": "prod", - }, - }, - "edgesOut": Map { - "babel-plugin-macros" => EdgeOut { - "name": "babel-plugin-macros", - "spec": "^3.1.0", - "to": null, - "type": "peerOptional", - }, - }, - "location": "node_modules/dedent", - "name": "dedent", - "path": "{CWD}/test/arborist/tap-testdir-pruner-prune-with-lockfile-with-implicit-optional-peer-dependencies/node_modules/dedent", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "version": "1.6.0", - }, - }, - "edgesOut": Map { - "dedent" => EdgeOut { - "name": "dedent", - "spec": "^1.6.0", - "to": "node_modules/dedent", - "type": "prod", - }, - }, - "isProjectRoot": true, - "location": "", - "name": "tap-testdir-pruner-prune-with-lockfile-with-implicit-optional-peer-dependencies", - "packageName": "prune-lockfile-optional-peer", - "path": "{CWD}/test/arborist/tap-testdir-pruner-prune-with-lockfile-with-implicit-optional-peer-dependencies", - "version": "1.0.0", -} -` - -exports[`test/arborist/pruner.js TAP prune with lockfile with implicit optional peer dependencies > should remove optional peer dependencies in package-lock.json 1`] = ` -Object { - "lockfileVersion": 3, - "name": "prune-lockfile-optional-peer", - "packages": Object { - "": Object { - "dependencies": Object { - "dedent": "^1.6.0", - }, - "name": "prune-lockfile-optional-peer", - "version": "1.0.0", - }, - "node_modules/dedent": Object { - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "license": "MIT", - "peerDependencies": Object { - "babel-plugin-macros": "^3.1.0", - }, - "peerDependenciesMeta": Object { - "babel-plugin-macros": Object { - "optional": true, - }, - }, - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "version": "1.6.0", - }, - }, - "requires": true, - "version": "1.0.0", -} -` - exports[`test/arborist/pruner.js TAP prune workspaces > must match snapshot 1`] = ` ArboristNode { "children": Map { diff --git a/workspaces/arborist/test/arborist/build-ideal-tree.js b/workspaces/arborist/test/arborist/build-ideal-tree.js index 32bc6b25ed39c..db1a9f7ac539a 100644 --- a/workspaces/arborist/test/arborist/build-ideal-tree.js +++ b/workspaces/arborist/test/arborist/build-ideal-tree.js @@ -1655,6 +1655,16 @@ t.test('more peer dep conflicts', async t => { error: false, resolvable: true, }, + 'peerDep replacement of top level dep with different version resulting detached top level dep': { + pkg: { + description: 'a@ -> (PeerOptional(b, c, dep, dep)) b -> ( Peer(a) ) c -> ( Peer(a) )', + devDependencies: { + '@test/a': '^1.1.0', + '@test/b': '1.1.0', + }, + }, + error: false, + resolvable: true }, }) createRegistry(t, true) @@ -4389,4 +4399,64 @@ t.test('installLinks behavior with project-internal file dependencies', async t t.ok(nestedDep, 'nested-dep should be found') t.ok(nestedDep.isLink, 'nested-dep should be a link (project-internal)') }) + + t.test('installLinks=true with transitive external file dependencies', async t => { + // mainpkg installs b (external file dep) with --install-links + // b depends on a (another external file dep via file:../a) + // Both should be installed (not linked) and dependencies should resolve correctly + const testRoot = t.testdir({ + a: { + 'package.json': JSON.stringify({ + name: 'a', + main: 'index.js', + }), + 'index.js': 'export const A = "A";', + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + main: 'index.js', + dependencies: { + a: 'file:../a', + }, + }), + 'index.js': 'import {A} from "a";export const fn = () => console.log(A);', + }, + mainpkg: { + 'package.json': JSON.stringify({}), + }, + }) + + const mainpkgPath = join(testRoot, 'mainpkg') + const bPath = join(testRoot, 'b') + createRegistry(t, false) + + const arb = newArb(mainpkgPath, { installLinks: true }) + + // Add the external file dependency using the full path + await arb.buildIdealTree({ add: [`file:${bPath}`] }) + + const tree = arb.idealTree + + // Both packages should be present in the tree + const packageB = tree.children.get('b') + const packageA = tree.children.get('a') + + t.ok(packageB, 'package b should be found in tree') + t.ok(packageA, 'package a should be found in tree (transitive dependency)') + + // Both should be installed (not linked) due to installLinks=true + t.notOk(packageB.isLink, 'package b should not be a link (installLinks=true)') + t.notOk(packageA.isLink, 'package a should not be a link (transitive with installLinks=true)') + + // Verify that the resolved paths are correct + t.match(packageB.resolved, /file:.*[/\\]b$/, 'package b should have correct resolved path') + t.match(packageA.resolved, /file:.*[/\\]a$/, 'package a should have correct resolved path') + + // Verify the dependency relationship + const edgeToA = packageB.edgesOut.get('a') + t.ok(edgeToA, 'package b should have an edge to a') + t.ok(edgeToA.valid, 'the edge from b to a should be valid') + t.equal(edgeToA.to, packageA, 'the edge from b should point to package a') + }) }) diff --git a/workspaces/arborist/test/arborist/pruner.js b/workspaces/arborist/test/arborist/pruner.js index c805123b5a4cf..208acc1d2a05e 100644 --- a/workspaces/arborist/test/arborist/pruner.js +++ b/workspaces/arborist/test/arborist/pruner.js @@ -39,32 +39,19 @@ t.test('prune with lockfile', async t => { }) t.test('prune with lockfile with implicit optional peer dependencies', async t => { - registry.audit({}) - const opts = {} - - // todo: for some reason on Windows when doing this test NPM looks for - // the cache in the home directory, resulting in an unexpected real - // call being made to the registry - if (process.platform === 'win32') { - opts.cache = 'C:\\npm\\cache\\_cacache' - } - const path = fixture(t, 'prune-lockfile-optional-peer') - const tree = await pruneTree(path, opts) + const tree = await pruneTree(path, { audit: false }) const dep = tree.children.get('dedent') - t.ok(dep, 'required prod dep was pruned from tree') + t.ok(dep, 'required prod dep was not pruned from tree') const optionalPeerDep = tree.children.get('babel-plugin-macros') - t.notOk(optionalPeerDep, 'all listed optional peer deps pruned from tree') + t.notOk(optionalPeerDep, 'optional peer dep was pruned from tree') - t.matchSnapshot( - require(path + '/package-lock.json'), - 'should remove optional peer dependencies in package-lock.json' - ) - t.matchSnapshot( - printTree(tree), - 'should remove all deps from reified tree' + t.notMatch( + fs.readFileSync(path + '/package-lock.json'), + 'node_modules/babel-plugin-macros', + 'should remove optional peer dep from package-lock.json' ) }) diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index 566a62273e710..a2944ceff4e62 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -2444,28 +2444,20 @@ t.test('move aside symlink clutter', async t => { file: 'do not delete me please', 'package.json': JSON.stringify({ name: 'ABBREV', version: '1.0.0' }), }, - 'sensitivity-test': t.fixture('symlink', './target'), + node_modules: { + ABBREV: t.fixture('symlink', '../target'), + }, }) // check to see if we're on a case-insensitive fs try { - const st = fs.lstatSync(path + '/SENSITIVITY-TEST') + const st = fs.lstatSync(path + '/node_modules/abbrev') t.equal(st.isSymbolicLink(), true, 'fs is case insensitive') } catch (er) { t.plan(0, 'case sensitive file system, test not relevant') return } - const kReifyPackages = Symbol.for('reifyPackages') - const reifyPackages = Arborist.prototype[kReifyPackages] - t.teardown(() => Arborist.prototype[kReifyPackages] = reifyPackages) - Arborist.prototype[kReifyPackages] = async function () { - fs.mkdirSync(path + '/node_modules') - fs.symlinkSync('../target', path + '/node_modules/ABBREV') - Arborist.prototype[kReifyPackages] = reifyPackages - return this[kReifyPackages]() - } - createRegistry(t, true) const tree = await printReified(path) const st = fs.lstatSync(path + '/node_modules/abbrev') @@ -3627,6 +3619,59 @@ t.test('should preserve exact ranges, missing actual tree', async (t) => { await t.resolves(arb.reify(), 'reify should complete successfully') }) + + t.test('registry with different protocol should swap protocol', async (t) => { + const abbrevPackument4 = JSON.stringify({ + _id: 'abbrev', + _rev: 'lkjadflkjasdf', + name: 'abbrev', + 'dist-tags': { latest: '1.1.1' }, + versions: { + '1.1.1': { + name: 'abbrev', + version: '1.1.1', + dist: { + // Note: This URL has no path component that matches our registry path + tarball: 'https://external-registry.example.com/abbrev-1.1.1.tgz', + }, + }, + }, + }) + + const testdir = t.testdir({ + project: { + 'package.json': JSON.stringify({ + name: 'myproject', + version: '1.0.0', + dependencies: { + abbrev: '1.1.1', + }, + }), + }, + }) + + // Set up the registrywith an http protocol + const registryHost = 'http://registry.example.com' + const registryPath = '/custom/deep/path/registry' + const registry = `${registryHost}${registryPath}` + + tnock(t, registryHost) + .get(`${registryPath}/abbrev`) + .reply(200, abbrevPackument4) + + tnock(t, registryHost) + .get(`${registryPath}/abbrev-1.1.1.tgz`) + .reply(200, abbrevTGZ) + + const arb = new Arborist({ + path: resolve(testdir, 'project'), + registry, + cache: resolve(testdir, 'cache'), + replaceRegistryHost: 'always', + }) + + await t.resolves(arb.reify(), 'reify should complete successfully when protocol changes from https to http') + }) }) t.test('install stategy linked', async (t) => { diff --git a/workspaces/arborist/test/fixtures/create-reify-case.js b/workspaces/arborist/test/fixtures/create-reify-case.js index 5d2349dd33076..33bd44c185826 100644 --- a/workspaces/arborist/test/fixtures/create-reify-case.js +++ b/workspaces/arborist/test/fixtures/create-reify-case.js @@ -129,7 +129,7 @@ if (hiddenLocks.length) { } } -writeFileSync(outFile, `// generated from ${rel} +writeFileSync(outFile, `// generated from ${rel.replaceAll('\\', '/')} module.exports = t => { const path = ${output} return path diff --git a/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/node_modules/dedent/package.json b/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/node_modules/dedent/package.json new file mode 100644 index 0000000000000..50a7c71cc90d2 --- /dev/null +++ b/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/node_modules/dedent/package.json @@ -0,0 +1,12 @@ +{ + "name": "dedent", + "version": "1.6.0", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } +} \ No newline at end of file diff --git a/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/package-lock.json b/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/package-lock.json index 859d9f5f7770c..80b2ec4d213d9 100644 --- a/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/package-lock.json +++ b/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/package-lock.json @@ -11,103 +11,13 @@ "dedent": "^1.6.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/babel-plugin-macros": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } + "peer": true }, "node_modules/dedent": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -116,228 +26,6 @@ "optional": true } } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC", - "optional": true, - "peer": true - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">= 6" - } } } } diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/a.json b/workspaces/arborist/test/fixtures/registry-mocks/content/test/a.json new file mode 100644 index 0000000000000..94cd8577573fb --- /dev/null +++ b/workspaces/arborist/test/fixtures/registry-mocks/content/test/a.json @@ -0,0 +1,146 @@ +{ + "name": "@test/a", + "versions": { + "1.2.0": { + "name": "@test/a", + "version": "1.2.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "lodash": "^4.17.21", + "underscore": "^1.13.1", + "@test/b": "1.2.0", + "@test/c": "1.2.0" + }, + "peerDependenciesMeta": { + "@test/b": { + "optional": true + }, + "@test/c": { + "optional": true + }, + "lodash": { + "optional": true + }, + "underscore": { + "optional": true + } + }, + "_id": "@test/a@1.2.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-k7WYu8tdQY1aq8QV+7YEGcoSYXrdCACqnabuvNC8Tpwvpk/MF25CeX4nei6eliVaHqHxzNzAr60ne2TgsEoz2Q==", + "shasum": "b609076c847a018b144ab68c953817847195535a", + "tarball": "http://localhost:4873/@test/a/-/a-1.2.0.tgz" + }, + "contributors": [] + }, + "1.1.0": { + "name": "@test/a", + "version": "1.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "lodash": "^4.17.0", + "uniq": "^1.0.0", + "@test/b": "1.1.0", + "@test/c": "1.1.0" + }, + "peerDependenciesMeta": { + "@test/b": { + "optional": true + }, + "@test/c": { + "optional": true + }, + "lodash": { + "optional": true + }, + "uniq": { + "optional": true + } + }, + "_id": "@test/a@1.1.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-qlfAcmAKeohHKBVVAnwsiDs+URz5jCPYlXe+srdxX6Nzhl9W6FX9kV5Lm6XahBhB+H/c+eRi+ghAE8YcdzmFIA==", + "shasum": "0d9b53f67e05d388195ad096f61fe2c1c6f0ff8d", + "tarball": "http://localhost:4873/@test/a/-/a-1.1.0.tgz" + }, + "contributors": [] + }, + "1.0.0": { + "name": "@test/a", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "lodash": "^4.17.0", + "uniq": "^1.0.0", + "@test/b": "1.0.0", + "@test/c": "1.0.0" + }, + "peerDependenciesMeta": { + "@test/b": { + "optional": true + }, + "@test/c": { + "optional": true + }, + "lodash": { + "optional": true + }, + "uniq": { + "optional": true + } + }, + "_id": "@test/a@1.0.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-BRD01XQTy4WW2PrMdV0ZvHdqlY6v0FY3kyvEIv4v0n7apOHQwuQqjdL4iWnApfEwD0o0mVSbQs5s6DibNmDnMg==", + "shasum": "a1ec39760cf04261fff44b23582f1bafba0b14ff", + "tarball": "http://localhost:4873/@test/a/-/a-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-07-31T16:24:31.780Z", + "created": "2025-07-29T12:59:32.758Z", + "1.2.0": "2025-07-29T13:15:20.477Z", + "1.1.0": "2025-07-31T16:24:09.634Z", + "1.0.0": "2025-07-31T16:24:31.780Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_rev": "44-1c1667b80cb416cc", + "_id": "@test/a", + "readme": "ERROR: No README data found!", + "_attachments": {} +} \ No newline at end of file diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/a/a-1.0.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/test/a/a-1.0.0.tgz new file mode 100644 index 0000000000000..00df7811e9df7 Binary files /dev/null and b/workspaces/arborist/test/fixtures/registry-mocks/content/test/a/a-1.0.0.tgz differ diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/a/a-1.1.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/test/a/a-1.1.0.tgz new file mode 100644 index 0000000000000..c1a2ba7b9b186 Binary files /dev/null and b/workspaces/arborist/test/fixtures/registry-mocks/content/test/a/a-1.1.0.tgz differ diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/b.json b/workspaces/arborist/test/fixtures/registry-mocks/content/test/b.json new file mode 100644 index 0000000000000..b32a350389ca7 --- /dev/null +++ b/workspaces/arborist/test/fixtures/registry-mocks/content/test/b.json @@ -0,0 +1,95 @@ +{ + "name": "@test/b", + "versions": { + "1.0.0": { + "name": "@test/b", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "@test/a": "1.0.0" + }, + "_id": "@test/b@1.0.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-q2p6qVG/lIpauYmngTeuWBAhqMYOR/dAzIk/nhpIuuqueji1cuhXFkuxykRn1N/imlLKWEzXxdS72krNMAohYg==", + "shasum": "049ecb3edfce0c78d1e94718bda1e2c24d004f5c", + "tarball": "http://localhost:4873/@test/b/-/b-1.0.0.tgz" + }, + "contributors": [] + }, + "1.1.0": { + "name": "@test/b", + "version": "1.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "@test/a": "1.1.0" + }, + "_id": "@test/b@1.1.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-WrPD0/5vcNlm12B6XDjTiIBN0U5SfGAuPBYJi3QeV2jEaBAnXnjWnffv7Dov0KPON3zsPg11t/EB4BDVgWIJEg==", + "shasum": "33107fbfdc56efed9ae21749aae5ea84bc4a5b80", + "tarball": "http://localhost:4873/@test/b/-/b-1.1.0.tgz" + }, + "contributors": [] + }, + "1.2.0": { + "name": "@test/b", + "version": "1.2.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "@test/a": "1.2.0" + }, + "_id": "@test/b@1.2.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-X90tU1P+EZ/IyiG4ICrQgKEBcQBQugZ/0OKo9xGN8a2dWPoJ7zefIQwpoQTy2NE5w7SgGyi6v9PQSw191v524Q==", + "shasum": "b735bde9da04cbd5f1cbb31817bf64302f0db265", + "tarball": "http://localhost:4873/@test/b/-/b-1.2.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-07-29T13:02:09.586Z", + "created": "2025-07-29T13:01:53.128Z", + "1.0.0": "2025-07-29T13:01:53.128Z", + "1.1.0": "2025-07-29T13:02:01.228Z", + "1.2.0": "2025-07-29T13:02:09.586Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.2.0" + }, + "_rev": "9-e8970996bfeb2c4f", + "_id": "@test/b", + "readme": "ERROR: No README data found!", + "_attachments": {} +} \ No newline at end of file diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/b/b-1.0.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/test/b/b-1.0.0.tgz new file mode 100644 index 0000000000000..0a10edf65b2e3 Binary files /dev/null and b/workspaces/arborist/test/fixtures/registry-mocks/content/test/b/b-1.0.0.tgz differ diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/b/b-1.1.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/test/b/b-1.1.0.tgz new file mode 100644 index 0000000000000..96f29aea98300 Binary files /dev/null and b/workspaces/arborist/test/fixtures/registry-mocks/content/test/b/b-1.1.0.tgz differ diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/c.json b/workspaces/arborist/test/fixtures/registry-mocks/content/test/c.json new file mode 100644 index 0000000000000..e26765f61e416 --- /dev/null +++ b/workspaces/arborist/test/fixtures/registry-mocks/content/test/c.json @@ -0,0 +1,95 @@ +{ + "name": "@test/c", + "versions": { + "1.0.0": { + "name": "@test/c", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "@test/a": "1.0.0" + }, + "_id": "@test/c@1.0.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-ikGDvMXxzqHgCkIycVNWmpfDs6G/aA7i3AY1Or+T+hO+2G/t+rfIxnLgIc4p10K7GM2ZxcipK9Z7U6LAtTO0iw==", + "shasum": "96b5a6fa92f8713c240686e9f3dfd5c00df7497e", + "tarball": "http://localhost:4873/@test/c/-/c-1.0.0.tgz" + }, + "contributors": [] + }, + "1.1.0": { + "name": "@test/c", + "version": "1.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "@test/a": "1.1.0" + }, + "_id": "@test/c@1.1.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-BNxNmwGwAhVxA8RQpog/wy/NNZfa5ruskwZePlKfu1zpLVtsrjO8zGau6C/c8iIw9mwrVqBAeBuFpUwJhLTAZA==", + "shasum": "01db72391f551fd7944adbf0f54eaebc389b90c4", + "tarball": "http://localhost:4873/@test/c/-/c-1.1.0.tgz" + }, + "contributors": [] + }, + "1.2.0": { + "name": "@test/c", + "version": "1.2.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "@test/a": "1.2.0" + }, + "_id": "@test/c@1.2.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-pAHdEr8mb8mXuWPQL0mbkyHVulhzVWQ3HvpO9OBZ0azF56p9cr1+hVy/CajxPdEr/Crx6iBfjpoaNYscVPbvMg==", + "shasum": "e915f26882f8bbde7e238c02908bbc5625cf3c4e", + "tarball": "http://localhost:4873/@test/c/-/c-1.2.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-07-29T13:03:42.407Z", + "created": "2025-07-29T13:03:29.009Z", + "1.0.0": "2025-07-29T13:03:29.009Z", + "1.1.0": "2025-07-29T13:03:36.559Z", + "1.2.0": "2025-07-29T13:03:42.407Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.2.0" + }, + "_rev": "9-19d0ff85a20ff576", + "_id": "@test/c", + "readme": "ERROR: No README data found!", + "_attachments": {} +} \ No newline at end of file diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.0.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.0.0.tgz new file mode 100644 index 0000000000000..65661f1328219 Binary files /dev/null and b/workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.0.0.tgz differ diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.1.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.1.0.tgz new file mode 100644 index 0000000000000..cdbee76034001 Binary files /dev/null and b/workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.1.0.tgz differ diff --git a/workspaces/arborist/test/fixtures/reify-cases/prune-lockfile-optional-peer.js b/workspaces/arborist/test/fixtures/reify-cases/prune-lockfile-optional-peer.js index b98dc57d3ae0e..b420709f5aa68 100644 --- a/workspaces/arborist/test/fixtures/reify-cases/prune-lockfile-optional-peer.js +++ b/workspaces/arborist/test/fixtures/reify-cases/prune-lockfile-optional-peer.js @@ -1,6 +1,22 @@ // generated from test/fixtures/prune-lockfile-optional-peer module.exports = t => { const path = t.testdir({ + "node_modules": { + "dedent": { + "package.json": JSON.stringify({ + "name": "dedent", + "version": "1.6.0", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }) + } + }, "package-lock.json": JSON.stringify({ "name": "prune-lockfile-optional-peer", "version": "1.0.0", @@ -14,103 +30,13 @@ module.exports = t => { "dedent": "^1.6.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/babel-plugin-macros": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } + "peer": true }, "node_modules/dedent": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -119,228 +45,6 @@ module.exports = t => { "optional": true } } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC", - "optional": true, - "peer": true - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">= 6" - } } } }), diff --git a/workspaces/config/CHANGELOG.md b/workspaces/config/CHANGELOG.md index 3d62d65f24dbc..5ec7910f2963f 100644 --- a/workspaces/config/CHANGELOG.md +++ b/workspaces/config/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [10.4.0](https://github.com/npm/cli/compare/config-v10.3.1...config-v10.4.0) (2025-09-03) +### Features +* [`bdcc10d`](https://github.com/npm/cli/commit/bdcc10d9f848940987b3d326ccd4673fab2bcfef) [#8359](https://github.com/npm/cli/pull/8359) add support for optional env var replacements in .npmrc (#8359) (@aczekajski, @owlstronaut) + ## [10.3.1](https://github.com/npm/cli/compare/config-v10.3.0...config-v10.3.1) (2025-07-24) ### Bug Fixes * [`7f66f0a`](https://github.com/npm/cli/commit/7f66f0ae8fb84f567fe83a9a5738d06c7fe8fb54) [#8447](https://github.com/npm/cli/pull/8447) add better hint for `before` and clean up description (@wraithgar) diff --git a/workspaces/config/lib/env-replace.js b/workspaces/config/lib/env-replace.js index c851f6e4d1501..c347be480ed68 100644 --- a/workspaces/config/lib/env-replace.js +++ b/workspaces/config/lib/env-replace.js @@ -1,9 +1,11 @@ // replace any ${ENV} values with the appropriate environ. +// optional "?" modifier can be used like this: ${ENV?} so in case of the variable being not defined, it evaluates into empty string. -const envExpr = /(? f.replace(envExpr, (orig, esc, name) => { - const val = env[name] !== undefined ? env[name] : `$\{${name}}` +module.exports = (f, env) => f.replace(envExpr, (orig, esc, name, modifier) => { + const fallback = modifier === '?' ? '' : `$\{${name}}` + const val = env[name] !== undefined ? env[name] : fallback // consume the escape chars that are relevant. if (esc.length % 2) { diff --git a/workspaces/config/package.json b/workspaces/config/package.json index fc6c9fd10ee7f..5cb8925d4cf4b 100644 --- a/workspaces/config/package.json +++ b/workspaces/config/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/config", - "version": "10.3.1", + "version": "10.4.0", "files": [ "bin/", "lib/" diff --git a/workspaces/config/test/env-replace.js b/workspaces/config/test/env-replace.js index c2b570364de87..c6b40266d30e5 100644 --- a/workspaces/config/test/env-replace.js +++ b/workspaces/config/test/env-replace.js @@ -6,8 +6,18 @@ const env = { bar: 'baz', } -t.equal(envReplace('\\${foo}', env), '${foo}') -t.equal(envReplace('\\\\${foo}', env), '\\bar') -t.equal(envReplace('${baz}', env), '${baz}') -t.equal(envReplace('\\${baz}', env), '${baz}') -t.equal(envReplace('\\\\${baz}', env), '\\${baz}') +t.equal(envReplace('${foo}', env), 'bar', 'replaces defined variable') +t.equal(envReplace('${foo?}', env), 'bar', 'replaces defined variable with ? modifier') +t.equal(envReplace('${foo}${bar}', env), 'barbaz', 'replaces multiple defined variables') +t.equal(envReplace('${foo?}${baz?}', env), 'bar', 'replaces mixed defined/undefined variables with ? modifier') +t.equal(envReplace('\\${foo}', env), '${foo}', 'escapes normal variable') +t.equal(envReplace('\\\\${foo}', env), '\\bar', 'double escape allows replacement') +t.equal(envReplace('\\\\\\${foo}', env), '\\${foo}', 'triple escape prevents replacement') +t.equal(envReplace('${baz}', env), '${baz}', 'leaves undefined variable unreplaced') +t.equal(envReplace('\\${baz}', env), '${baz}', 'escapes undefined variable') +t.equal(envReplace('\\\\${baz}', env), '\\${baz}', 'double escape with undefined variable') +t.equal(envReplace('\\${foo?}', env), '${foo?}', 'escapes optional variable') +t.equal(envReplace('\\\\${foo?}', env), '\\bar', 'double escape allows optional replacement') +t.equal(envReplace('${baz?}', env), '', 'replaces undefined variable with empty string when using ? modifier') +t.equal(envReplace('\\${baz?}', env), '${baz?}', 'escapes undefined optional variable') +t.equal(envReplace('\\\\${baz?}', env), '\\', 'double escape with undefined optional variable results in empty replacement') diff --git a/workspaces/libnpmdiff/CHANGELOG.md b/workspaces/libnpmdiff/CHANGELOG.md index 7e70e19ad250e..5a3073bdd24ea 100644 --- a/workspaces/libnpmdiff/CHANGELOG.md +++ b/workspaces/libnpmdiff/CHANGELOG.md @@ -28,6 +28,10 @@ * [workspace](https://github.com/npm/cli/releases/tag/arborist-v9.1.3): `@npmcli/arborist@9.1.3` +### Dependencies + +* [workspace](https://github.com/npm/cli/releases/tag/arborist-v9.1.4): `@npmcli/arborist@9.1.4` + ## [8.0.0](https://github.com/npm/cli/compare/libnpmdiff-v8.0.0-pre.1...libnpmdiff-v8.0.0) (2024-12-16) ### Features * [`a7bfc6d`](https://github.com/npm/cli/commit/a7bfc6df76882996ebb834dbca785fdf33b8c50d) [#7972](https://github.com/npm/cli/pull/7972) trigger release process (#7972) (@wraithgar) diff --git a/workspaces/libnpmdiff/package.json b/workspaces/libnpmdiff/package.json index c89c809e456da..87c467b5a9783 100644 --- a/workspaces/libnpmdiff/package.json +++ b/workspaces/libnpmdiff/package.json @@ -1,6 +1,6 @@ { "name": "libnpmdiff", - "version": "8.0.6", + "version": "8.0.7", "description": "The registry diff", "repository": { "type": "git", @@ -47,7 +47,7 @@ "tap": "^16.3.8" }, "dependencies": { - "@npmcli/arborist": "^9.1.3", + "@npmcli/arborist": "^9.1.4", "@npmcli/installed-package-contents": "^3.0.0", "binary-extensions": "^3.0.0", "diff": "^7.0.0", diff --git a/workspaces/libnpmexec/CHANGELOG.md b/workspaces/libnpmexec/CHANGELOG.md index 477e6dd274fab..c728d557eb77d 100644 --- a/workspaces/libnpmexec/CHANGELOG.md +++ b/workspaces/libnpmexec/CHANGELOG.md @@ -12,6 +12,10 @@ * [workspace](https://github.com/npm/cli/releases/tag/arborist-v9.1.3): `@npmcli/arborist@9.1.3` +### Dependencies + +* [workspace](https://github.com/npm/cli/releases/tag/arborist-v9.1.4): `@npmcli/arborist@9.1.4` + ## [10.1.2](https://github.com/npm/cli/compare/libnpmexec-v10.1.1...libnpmexec-v10.1.2) (2025-05-15) ### Bug Fixes * [`fdc3413`](https://github.com/npm/cli/commit/fdc3413019c2f34f1fde35449e5f3a6b0fb51ba2) [#8221](https://github.com/npm/cli/pull/8221) exec: Fails to Execute Binaries Named After Shell Keywords (#8221) (@13sfaith) diff --git a/workspaces/libnpmexec/package.json b/workspaces/libnpmexec/package.json index 49b188d919912..91fb9eb8e9e3a 100644 --- a/workspaces/libnpmexec/package.json +++ b/workspaces/libnpmexec/package.json @@ -1,6 +1,6 @@ { "name": "libnpmexec", - "version": "10.1.5", + "version": "10.1.6", "files": [ "bin/", "lib/" @@ -60,7 +60,7 @@ "tap": "^16.3.8" }, "dependencies": { - "@npmcli/arborist": "^9.1.3", + "@npmcli/arborist": "^9.1.4", "@npmcli/package-json": "^6.1.1", "@npmcli/run-script": "^9.0.1", "ci-info": "^4.0.0", diff --git a/workspaces/libnpmfund/CHANGELOG.md b/workspaces/libnpmfund/CHANGELOG.md index d9e726cfd8815..1152475ab3f08 100644 --- a/workspaces/libnpmfund/CHANGELOG.md +++ b/workspaces/libnpmfund/CHANGELOG.md @@ -36,6 +36,10 @@ * [workspace](https://github.com/npm/cli/releases/tag/arborist-v9.1.3): `@npmcli/arborist@9.1.3` +### Dependencies + +* [workspace](https://github.com/npm/cli/releases/tag/arborist-v9.1.4): `@npmcli/arborist@9.1.4` + ## [7.0.0](https://github.com/npm/cli/compare/libnpmfund-v7.0.0-pre.1...libnpmfund-v7.0.0) (2024-12-16) ### Features * [`a7bfc6d`](https://github.com/npm/cli/commit/a7bfc6df76882996ebb834dbca785fdf33b8c50d) [#7972](https://github.com/npm/cli/pull/7972) trigger release process (#7972) (@wraithgar) diff --git a/workspaces/libnpmfund/package.json b/workspaces/libnpmfund/package.json index d888665298a9a..10c769275c499 100644 --- a/workspaces/libnpmfund/package.json +++ b/workspaces/libnpmfund/package.json @@ -1,6 +1,6 @@ { "name": "libnpmfund", - "version": "7.0.6", + "version": "7.0.7", "main": "lib/index.js", "files": [ "bin/", @@ -46,7 +46,7 @@ "tap": "^16.3.8" }, "dependencies": { - "@npmcli/arborist": "^9.1.3" + "@npmcli/arborist": "^9.1.4" }, "engines": { "node": "^20.17.0 || >=22.9.0" diff --git a/workspaces/libnpmpack/CHANGELOG.md b/workspaces/libnpmpack/CHANGELOG.md index f072f9a670a09..77b03a441e846 100644 --- a/workspaces/libnpmpack/CHANGELOG.md +++ b/workspaces/libnpmpack/CHANGELOG.md @@ -28,6 +28,10 @@ * [workspace](https://github.com/npm/cli/releases/tag/arborist-v9.1.3): `@npmcli/arborist@9.1.3` +### Dependencies + +* [workspace](https://github.com/npm/cli/releases/tag/arborist-v9.1.4): `@npmcli/arborist@9.1.4` + ## [9.0.0](https://github.com/npm/cli/compare/libnpmpack-v9.0.0-pre.1...libnpmpack-v9.0.0) (2024-12-16) ### Features * [`a7bfc6d`](https://github.com/npm/cli/commit/a7bfc6df76882996ebb834dbca785fdf33b8c50d) [#7972](https://github.com/npm/cli/pull/7972) trigger release process (#7972) (@wraithgar) diff --git a/workspaces/libnpmpack/package.json b/workspaces/libnpmpack/package.json index 1aa091fbb5d6b..a48d3d983707e 100644 --- a/workspaces/libnpmpack/package.json +++ b/workspaces/libnpmpack/package.json @@ -1,6 +1,6 @@ { "name": "libnpmpack", - "version": "9.0.6", + "version": "9.0.7", "description": "Programmatic API for the bits behind npm pack", "author": "GitHub Inc.", "main": "lib/index.js", @@ -37,7 +37,7 @@ "bugs": "https://github.com/npm/libnpmpack/issues", "homepage": "https://npmjs.com/package/libnpmpack", "dependencies": { - "@npmcli/arborist": "^9.1.3", + "@npmcli/arborist": "^9.1.4", "@npmcli/run-script": "^9.0.1", "npm-package-arg": "^12.0.0", "pacote": "^21.0.0"