Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/commands/outdated.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Outdated extends ArboristWorkspaceCmd {
'global',
'workspace',
'before',
'min-release-age',
]

#tree
Expand Down
1 change: 1 addition & 0 deletions lib/commands/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Update extends ArboristWorkspaceCmd {
'ignore-scripts',
'audit',
'before',
'min-release-age',
'bin-links',
'fund',
'dry-run',
Expand Down
38 changes: 26 additions & 12 deletions tap-snapshots/test/lib/docs.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,13 @@ If the requested version is a \`dist-tag\` and the given tag does not pass the
will be used. For example, \`foo@latest\` might install \`foo@1.2\` even though
\`latest\` is \`2.0\`.

This config cannot be used with: \`min-release-age\`
If \`before\` and \`min-release-age\` are both set in the same source, \`before\`
wins (an explicit absolute date overrides a relative window). Across
sources, the standard precedence applies (cli > env > project > user >
global), so a higher-priority source can always relax or override a
lower-priority one.



#### \`bin-links\`

Expand Down Expand Up @@ -1194,9 +1200,11 @@ are no versions available for the current set of dependencies, the command
will error.

This flag is a complement to \`before\`, which accepts an exact date instead
of a relative number of days.

This config cannot be used with: \`before\`
of a relative number of days. The two may coexist (e.g. \`min-release-age\` in
your \`.npmrc\` is preserved when npm internally spawns a sub-process with
\`--before\` while preparing a \`git:\` or \`github:\` dependency); when both
apply, \`before\` wins within a single source and across sources the standard
precedence rules apply.

This value is not exported to the environment for child processes.

Expand Down Expand Up @@ -3985,9 +3993,9 @@ Options:
[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only]
[--foreground-scripts] [--ignore-scripts] [--allow-directory <all|none|root>]
[--allow-file <all|none|root>] [--allow-git <all|none|root>]
[--allow-remote <all|none|root>] [--no-audit]
[--before <date>|--min-release-age <days>] [--no-bin-links] [--no-fund]
[--dry-run] [--cpu <cpu>] [--os <os>] [--libc <libc>]
[--allow-remote <all|none|root>] [--no-audit] [--before <date>]
[--min-release-age <days>] [--no-bin-links] [--no-fund] [--dry-run] [--cpu <cpu>]
[--os <os>] [--libc <libc>]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[--workspaces] [--include-workspace-root] [--install-links]

Expand Down Expand Up @@ -4253,9 +4261,9 @@ Options:
[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only]
[--foreground-scripts] [--ignore-scripts] [--allow-directory <all|none|root>]
[--allow-file <all|none|root>] [--allow-git <all|none|root>]
[--allow-remote <all|none|root>] [--no-audit]
[--before <date>|--min-release-age <days>] [--no-bin-links] [--no-fund]
[--dry-run] [--cpu <cpu>] [--os <os>] [--libc <libc>]
[--allow-remote <all|none|root>] [--no-audit] [--before <date>]
[--min-release-age <days>] [--no-bin-links] [--no-fund] [--dry-run] [--cpu <cpu>]
[--os <os>] [--libc <libc>]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[--workspaces] [--include-workspace-root] [--install-links]

Expand Down Expand Up @@ -4829,7 +4837,7 @@ npm outdated [<package-spec> ...]
Options:
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[--before <date>|--min-release-age <days>]
[--before <date>] [--min-release-age <days>]

-a|--all
When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show
Expand All @@ -4852,6 +4860,9 @@ Options:
--before
If passed to \`npm install\`, will rebuild the npm tree such that only

--min-release-age
If set, npm will build the npm tree such that only versions that were


Run "npm help outdated" for more info

Expand Down Expand Up @@ -5991,7 +6002,7 @@ Options:
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
[--include <prod|dev|optional|peer> [--include <prod|dev|optional|peer> ...]]
[--strict-peer-deps] [--no-package-lock] [--foreground-scripts]
[--ignore-scripts] [--no-audit] [--before <date>|--min-release-age <days>]
[--ignore-scripts] [--no-audit] [--before <date>] [--min-release-age <days>]
[--no-bin-links] [--no-fund] [--dry-run]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[--workspaces] [--include-workspace-root] [--install-links]
Expand Down Expand Up @@ -6035,6 +6046,9 @@ Options:
--before
If passed to \`npm install\`, will rebuild the npm tree such that only

--min-release-age
If set, npm will build the npm tree such that only versions that were

--bin-links
Tells npm to create symlinks (or \`.cmd\` shims on Windows) for package

Expand Down
21 changes: 16 additions & 5 deletions workspaces/config/lib/definitions/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,6 @@ const definitions = {
default: null,
hint: '<date>',
type: [null, Date],
exclusive: ['min-release-age'],
description: `
If passed to \`npm install\`, will rebuild the npm tree such that only
versions that were available **on or before** the given date are
Expand All @@ -303,6 +302,12 @@ const definitions = {
pass the \`--before\` filter, the most recent version less than or equal
to that tag will be used. For example, \`foo@latest\` might install
\`foo@1.2\` even though \`latest\` is \`2.0\`.

If \`before\` and \`min-release-age\` are both set in the same source,
\`before\` wins (an explicit absolute date overrides a relative window).
Across sources, the standard precedence applies (cli > env > project >
user > global), so a higher-priority source can always relax or
override a lower-priority one.
`,
flatten,
}),
Expand Down Expand Up @@ -1409,7 +1414,6 @@ const definitions = {
default: null,
hint: '<days>',
type: [null, Number],
exclusive: ['before'],
envExport: false,
description: `
If set, npm will build the npm tree such that only versions that were
Expand All @@ -1418,12 +1422,19 @@ const definitions = {
command will error.

This flag is a complement to \`before\`, which accepts an exact date
instead of a relative number of days.
instead of a relative number of days. The two may coexist (e.g.
\`min-release-age\` in your \`.npmrc\` is preserved when npm internally
spawns a sub-process with \`--before\` while preparing a \`git:\` or
\`github:\` dependency); when both apply, \`before\` wins within a
single source and across sources the standard precedence rules apply.
`,
flatten: (key, obj, flatOptions) => {
if (obj['min-release-age'] !== null) {
// If `before` is set in the same source, defer to it: an explicit
// absolute date overrides a relative window. Across sources, normal
// priority ordering means a higher-priority `before` will overwrite
// this `flatOptions.before` later in the flatten loop.
if (obj['min-release-age'] != null && obj.before == null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (obj['min-release-age'] != null && obj.before == null) {
if (obj['min-release-age'] != null && !Object.hasOwn(obj, 'before')) {

this will scope it to the current source so the CLI/env can still override an npmrc-set before

flatOptions.before = new Date(Date.now() - (86400000 * obj['min-release-age']))
obj.before = flatOptions.before
}
},
}),
Expand Down
82 changes: 82 additions & 0 deletions workspaces/config/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1869,3 +1869,85 @@ t.test('before and min-release-age', async t => {
t.ok(config.flat.before < Date.now(), 'before date is in the past not the future')
t.equal(config.get('min-release-age'), 30, 'min-release-age config remains readable after flattening')
})

// Regression test for https://github.com/npm/cli/issues/9291
// pacote spawns child npm processes with `--before=<date>` whenever it has a
// `before` option (which includes the case where the parent derived `before`
// from `min-release-age`). The child process then loads the user's npmrc, which
// still contains `min-release-age=N`. Previously this combination crashed
// because the two options were declared mutually exclusive.
t.test('min-release-age in npmrc coexists with --before from CLI (pacote spawn)', async t => {
const dir = t.testdir({
'.npmrc': 'min-release-age=7',
})
const cliBefore = new Date('2024-01-15T00:00:00.000Z')
const config = new Config({
npmPath: __dirname,
env: { HOME: dir },
argv: [process.execPath, __filename, `--before=${cliBefore.toISOString()}`],
cwd: dir,
definitions,
shorthands,
flatten,
})
await t.resolves(config.load(), 'loads without crashing on previously exclusive options')
// CLI is the highest-priority source, so its `before` overrides whatever
// `min-release-age` in the npmrc would have produced.
t.equal(
config.flat.before.toISOString(),
cliBefore.toISOString(),
'CLI --before overrides npmrc min-release-age'
)
})

// A higher-priority source must be able to relax (or override) a stricter
// lower-priority `min-release-age`. Previously this would have thrown via
// the `exclusive` check; now it follows normal cli > npmrc precedence.
t.test('CLI --min-release-age=0 relaxes a stricter npmrc min-release-age', async t => {
const dir = t.testdir({
'.npmrc': 'min-release-age=30',
})
const config = new Config({
npmPath: __dirname,
env: { HOME: dir },
argv: [process.execPath, __filename, '--min-release-age=0'],
cwd: dir,
definitions,
shorthands,
flatten,
})
await config.load()
// min-release-age=0 means "now" — the CLI must win, not the npmrc's 30 days.
const now = Date.now()
t.ok(
Math.abs(config.flat.before.getTime() - now) < 60_000,
'flat.before resolves to ~now (CLI overrode the stricter npmrc)'
)
})

// Within a single source, an explicit `before` wins over a relative
// `min-release-age` so the resolution is deterministic regardless of the
// argv parser's key-iteration order.
t.test('within a single source, before wins over min-release-age', async t => {
const path = t.testdir()
const config = new Config({
npmPath: `${path}/npm`,
env: {},
argv: [
process.execPath,
__filename,
'--min-release-age=1',
'--before=2020-01-01T00:00:00.000Z',
],
cwd: path,
definitions,
shorthands,
flatten,
})
await config.load()
t.equal(
config.flat.before.toISOString(),
'2020-01-01T00:00:00.000Z',
'explicit --before wins over --min-release-age in the same source'
)
})
Loading