Skip to content

Commit 6aa332d

Browse files
raazkhnlMichael Smith
authored andcommitted
fix: allow min-release-age in npmrc to coexist with --before
When the user has `min-release-age=N` in their `.npmrc`, the config flatten function derives a `before` date used by pacote. Whenever pacote spawns a child npm process (e.g. preparing a `git:` or `github:` dep), it forwards `--before=<date>` to the child. The child then loads the same `.npmrc` and the previously declared mutual-exclusivity between `before` and `min-release-age` caused a hard configuration error. This makes the two options coexist: the `exclusive` constraints are removed and both flatten functions resolve to the earlier of the two effective dates, never widening the user's most conservative bound. The `min-release-age` flatten no longer mutates the per-source config object (the prior `obj.before = ...` / `delete obj['min-release-age']` mutations were vestigial and only masked the conflict at the parent level, not in spawned children). `min-release-age` is also added to the `params` arrays for `outdated` and `update` so it remains visible in their command help; it was previously displayed implicitly via the `before` exclusive grouping. Fixes: #9291 (cherry picked from commit ca585c8)
1 parent d7e195a commit 6aa332d

5 files changed

Lines changed: 126 additions & 17 deletions

File tree

lib/commands/outdated.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class Outdated extends ArboristWorkspaceCmd {
3131
'global',
3232
'workspace',
3333
'before',
34+
'min-release-age',
3435
]
3536

3637
#tree

lib/commands/update.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Update extends ArboristWorkspaceCmd {
2121
'ignore-scripts',
2222
'audit',
2323
'before',
24+
'min-release-age',
2425
'bin-links',
2526
'fund',
2627
'dry-run',

tap-snapshots/test/lib/docs.js.test.cjs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,13 @@ If the requested version is a \`dist-tag\` and the given tag does not pass the
347347
will be used. For example, \`foo@latest\` might install \`foo@1.2\` even though
348348
\`latest\` is \`2.0\`.
349349
350-
This config cannot be used with: \`min-release-age\`
350+
If \`before\` and \`min-release-age\` are both set in the same source, \`before\`
351+
wins (an explicit absolute date overrides a relative window). Across
352+
sources, the standard precedence applies (cli > env > project > user >
353+
global), so a higher-priority source can always relax or override a
354+
lower-priority one.
355+
356+
351357
352358
#### \`bin-links\`
353359
@@ -1201,9 +1207,11 @@ are no versions available for the current set of dependencies, the command
12011207
will error.
12021208
12031209
This flag is a complement to \`before\`, which accepts an exact date instead
1204-
of a relative number of days.
1205-
1206-
This config cannot be used with: \`before\`
1210+
of a relative number of days. The two may coexist (e.g. \`min-release-age\` in
1211+
your \`.npmrc\` is preserved when npm internally spawns a sub-process with
1212+
\`--before\` while preparing a \`git:\` or \`github:\` dependency); when both
1213+
apply, \`before\` wins within a single source and across sources the standard
1214+
precedence rules apply.
12071215
12081216
This value is not exported to the environment for child processes.
12091217
@@ -4041,9 +4049,9 @@ Options:
40414049
[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only]
40424050
[--foreground-scripts] [--ignore-scripts] [--allow-directory <all|none|root>]
40434051
[--allow-file <all|none|root>] [--allow-git <all|none|root>]
4044-
[--allow-remote <all|none|root>] [--no-audit]
4045-
[--before <date>|--min-release-age <days>] [--no-bin-links] [--no-fund]
4046-
[--dry-run] [--cpu <cpu>] [--os <os>] [--libc <libc>]
4052+
[--allow-remote <all|none|root>] [--no-audit] [--before <date>]
4053+
[--min-release-age <days>] [--no-bin-links] [--no-fund] [--dry-run] [--cpu <cpu>]
4054+
[--os <os>] [--libc <libc>]
40474055
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
40484056
[--workspaces] [--include-workspace-root] [--install-links]
40494057
@@ -4309,9 +4317,9 @@ Options:
43094317
[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only]
43104318
[--foreground-scripts] [--ignore-scripts] [--allow-directory <all|none|root>]
43114319
[--allow-file <all|none|root>] [--allow-git <all|none|root>]
4312-
[--allow-remote <all|none|root>] [--no-audit]
4313-
[--before <date>|--min-release-age <days>] [--no-bin-links] [--no-fund]
4314-
[--dry-run] [--cpu <cpu>] [--os <os>] [--libc <libc>]
4320+
[--allow-remote <all|none|root>] [--no-audit] [--before <date>]
4321+
[--min-release-age <days>] [--no-bin-links] [--no-fund] [--dry-run] [--cpu <cpu>]
4322+
[--os <os>] [--libc <libc>]
43154323
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
43164324
[--workspaces] [--include-workspace-root] [--install-links]
43174325
@@ -4885,7 +4893,7 @@ npm outdated [<package-spec> ...]
48854893
Options:
48864894
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global]
48874895
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
4888-
[--before <date>|--min-release-age <days>]
4896+
[--before <date>] [--min-release-age <days>]
48894897
48904898
-a|--all
48914899
When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show
@@ -4908,6 +4916,9 @@ Options:
49084916
--before
49094917
If passed to \`npm install\`, will rebuild the npm tree such that only
49104918
4919+
--min-release-age
4920+
If set, npm will build the npm tree such that only versions that were
4921+
49114922
49124923
Run "npm help outdated" for more info
49134924
@@ -6152,7 +6163,7 @@ Options:
61526163
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
61536164
[--include <prod|dev|optional|peer> [--include <prod|dev|optional|peer> ...]]
61546165
[--strict-peer-deps] [--no-package-lock] [--foreground-scripts]
6155-
[--ignore-scripts] [--no-audit] [--before <date>|--min-release-age <days>]
6166+
[--ignore-scripts] [--no-audit] [--before <date>] [--min-release-age <days>]
61566167
[--no-bin-links] [--no-fund] [--dry-run]
61576168
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
61586169
[--workspaces] [--include-workspace-root] [--install-links]
@@ -6196,6 +6207,9 @@ Options:
61966207
--before
61976208
If passed to \`npm install\`, will rebuild the npm tree such that only
61986209
6210+
--min-release-age
6211+
If set, npm will build the npm tree such that only versions that were
6212+
61996213
--bin-links
62006214
Tells npm to create symlinks (or \`.cmd\` shims on Windows) for package
62016215

workspaces/config/lib/definitions/definitions.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,6 @@ const definitions = {
292292
default: null,
293293
hint: '<date>',
294294
type: [null, Date],
295-
exclusive: ['min-release-age'],
296295
description: `
297296
If passed to \`npm install\`, will rebuild the npm tree such that only
298297
versions that were available **on or before** the given date are
@@ -303,6 +302,12 @@ const definitions = {
303302
pass the \`--before\` filter, the most recent version less than or equal
304303
to that tag will be used. For example, \`foo@latest\` might install
305304
\`foo@1.2\` even though \`latest\` is \`2.0\`.
305+
306+
If \`before\` and \`min-release-age\` are both set in the same source,
307+
\`before\` wins (an explicit absolute date overrides a relative window).
308+
Across sources, the standard precedence applies (cli > env > project >
309+
user > global), so a higher-priority source can always relax or
310+
override a lower-priority one.
306311
`,
307312
flatten,
308313
}),
@@ -1409,7 +1414,6 @@ const definitions = {
14091414
default: null,
14101415
hint: '<days>',
14111416
type: [null, Number],
1412-
exclusive: ['before'],
14131417
envExport: false,
14141418
description: `
14151419
If set, npm will build the npm tree such that only versions that were
@@ -1418,12 +1422,19 @@ const definitions = {
14181422
command will error.
14191423
14201424
This flag is a complement to \`before\`, which accepts an exact date
1421-
instead of a relative number of days.
1425+
instead of a relative number of days. The two may coexist (e.g.
1426+
\`min-release-age\` in your \`.npmrc\` is preserved when npm internally
1427+
spawns a sub-process with \`--before\` while preparing a \`git:\` or
1428+
\`github:\` dependency); when both apply, \`before\` wins within a
1429+
single source and across sources the standard precedence rules apply.
14221430
`,
14231431
flatten: (key, obj, flatOptions) => {
1424-
if (obj['min-release-age'] !== null) {
1432+
// If `before` is set in the same source, defer to it: an explicit
1433+
// absolute date overrides a relative window. Across sources, normal
1434+
// priority ordering means a higher-priority `before` will overwrite
1435+
// this `flatOptions.before` later in the flatten loop.
1436+
if (obj['min-release-age'] != null && obj.before == null) {
14251437
flatOptions.before = new Date(Date.now() - (86400000 * obj['min-release-age']))
1426-
obj.before = flatOptions.before
14271438
}
14281439
},
14291440
}),

workspaces/config/test/index.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,3 +1869,85 @@ t.test('before and min-release-age', async t => {
18691869
t.ok(config.flat.before < Date.now(), 'before date is in the past not the future')
18701870
t.equal(config.get('min-release-age'), 30, 'min-release-age config remains readable after flattening')
18711871
})
1872+
1873+
// Regression test for https://github.com/npm/cli/issues/9291
1874+
// pacote spawns child npm processes with `--before=<date>` whenever it has a
1875+
// `before` option (which includes the case where the parent derived `before`
1876+
// from `min-release-age`). The child process then loads the user's npmrc, which
1877+
// still contains `min-release-age=N`. Previously this combination crashed
1878+
// because the two options were declared mutually exclusive.
1879+
t.test('min-release-age in npmrc coexists with --before from CLI (pacote spawn)', async t => {
1880+
const dir = t.testdir({
1881+
'.npmrc': 'min-release-age=7',
1882+
})
1883+
const cliBefore = new Date('2024-01-15T00:00:00.000Z')
1884+
const config = new Config({
1885+
npmPath: __dirname,
1886+
env: { HOME: dir },
1887+
argv: [process.execPath, __filename, `--before=${cliBefore.toISOString()}`],
1888+
cwd: dir,
1889+
definitions,
1890+
shorthands,
1891+
flatten,
1892+
})
1893+
await t.resolves(config.load(), 'loads without crashing on previously exclusive options')
1894+
// CLI is the highest-priority source, so its `before` overrides whatever
1895+
// `min-release-age` in the npmrc would have produced.
1896+
t.equal(
1897+
config.flat.before.toISOString(),
1898+
cliBefore.toISOString(),
1899+
'CLI --before overrides npmrc min-release-age'
1900+
)
1901+
})
1902+
1903+
// A higher-priority source must be able to relax (or override) a stricter
1904+
// lower-priority `min-release-age`. Previously this would have thrown via
1905+
// the `exclusive` check; now it follows normal cli > npmrc precedence.
1906+
t.test('CLI --min-release-age=0 relaxes a stricter npmrc min-release-age', async t => {
1907+
const dir = t.testdir({
1908+
'.npmrc': 'min-release-age=30',
1909+
})
1910+
const config = new Config({
1911+
npmPath: __dirname,
1912+
env: { HOME: dir },
1913+
argv: [process.execPath, __filename, '--min-release-age=0'],
1914+
cwd: dir,
1915+
definitions,
1916+
shorthands,
1917+
flatten,
1918+
})
1919+
await config.load()
1920+
// min-release-age=0 means "now" — the CLI must win, not the npmrc's 30 days.
1921+
const now = Date.now()
1922+
t.ok(
1923+
Math.abs(config.flat.before.getTime() - now) < 60_000,
1924+
'flat.before resolves to ~now (CLI overrode the stricter npmrc)'
1925+
)
1926+
})
1927+
1928+
// Within a single source, an explicit `before` wins over a relative
1929+
// `min-release-age` so the resolution is deterministic regardless of the
1930+
// argv parser's key-iteration order.
1931+
t.test('within a single source, before wins over min-release-age', async t => {
1932+
const path = t.testdir()
1933+
const config = new Config({
1934+
npmPath: `${path}/npm`,
1935+
env: {},
1936+
argv: [
1937+
process.execPath,
1938+
__filename,
1939+
'--min-release-age=1',
1940+
'--before=2020-01-01T00:00:00.000Z',
1941+
],
1942+
cwd: path,
1943+
definitions,
1944+
shorthands,
1945+
flatten,
1946+
})
1947+
await config.load()
1948+
t.equal(
1949+
config.flat.before.toISOString(),
1950+
'2020-01-01T00:00:00.000Z',
1951+
'explicit --before wins over --min-release-age in the same source'
1952+
)
1953+
})

0 commit comments

Comments
 (0)