Skip to content

Commit 834408e

Browse files
martinrrmCopilot
andauthored
fix(pack): honor min-release-age-exclude (#9760)
Make `npm pack` honor `min-release-age-exclude` when resolving packages from a registry. Given: ```ini min-release-age=7 min-release-age-exclude=@myscope/* ```  `npm pack @myscope/some-package@1.2.3`  incorrectly failed with  ETARGET  when the package was newer than seven days, despite matching the exclusion. Root cause `min-release-age`  is flattened into the  `before`  option consumed by  `pacote` . However,  `pacote`  does not interpret  `min-release-age-exclude` ; callers must remove  before  for matching packages.  `npm pack`  performs two manifest resolutions: 1. Directly through `pacote.manifest` 2. Internally through  `libnpmpack`  Both resolutions received the unmodified `before` option, so the exclusion was never applied. Fix Derive effective options for each package spec using the existing Arborist release-age helpers: • Clear `before` when the package matches `min-release-age-exclude`  • Preserve the cutoff for nonmatching packages • Pass the same effective options to both manifest resolutions Using the alias target prevents an excluded alias name from disabling the release-age policy for an unrelated package. Test coverage Added regression coverage confirming that: • A recently published scoped package matching an exclusion glob can be packed • An excluded alias name does not exempt its non-excluded registry target The original scenario was also reproduced against a local registry: it failed with `ETARGET` before this change and successfully produced the tarball afterward. References Fixes #9759 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dec204b6-ad66-45a5-8228-831e306f6ba6
1 parent ef6cfea commit 834408e

2 files changed

Lines changed: 68 additions & 4 deletions

File tree

lib/commands/pack.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ const pacote = require('pacote')
22
const libpack = require('libnpmpack')
33
const npa = require('npm-package-arg')
44
const { log, output } = require('proc-log')
5+
const {
6+
isReleaseAgeExcluded,
7+
trustedSpecName,
8+
} = require('@npmcli/arborist/lib/release-age-exclude.js')
59
const { getContents, logTar } = require('../utils/tar.js')
610
const BaseCommand = require('../base-cmd.js')
711

@@ -35,23 +39,27 @@ class Pack extends BaseCommand {
3539
const manifests = []
3640
for (const arg of args) {
3741
const spec = npa(arg)
42+
const options = isReleaseAgeExcluded(
43+
trustedSpecName(spec),
44+
this.npm.flatOptions.minReleaseAgeExclude
45+
) ? { ...this.npm.flatOptions, before: null } : this.npm.flatOptions
3846
const manifest = await pacote.manifest(spec, {
39-
...this.npm.flatOptions,
47+
...options,
4048
Arborist,
4149
preferOnline: true,
4250
_isRoot: true,
4351
})
4452
if (!manifest._id) {
4553
throw new Error('Invalid package, must have name and version')
4654
}
47-
manifests.push({ arg, manifest })
55+
manifests.push({ arg, manifest, options })
4856
}
4957

5058
// Load tarball names up for printing afterward to isolate from the noise generated during packing
5159
const tarballs = []
52-
for (const { arg, manifest } of manifests) {
60+
for (const { arg, manifest, options } of manifests) {
5361
const tarballData = await libpack(arg, {
54-
...this.npm.flatOptions,
62+
...options,
5563
foregroundScripts: this.npm.config.isDefault('foreground-scripts')
5664
? true
5765
: this.npm.config.get('foreground-scripts'),

test/lib/commands/pack.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const t = require('tap')
22
const { load: loadMockNpm } = require('../../fixtures/mock-npm')
33
const { cleanZlib } = require('../../fixtures/clean-snapshot')
4+
const MockRegistry = require('@npmcli/mock-registry')
45
const path = require('node:path')
56
const fs = require('node:fs')
67

@@ -162,6 +163,61 @@ t.test('foreground-scripts can still be set to false', async t => {
162163
t.throws(() => fs.statSync(path.resolve(npm.prefix, filename)))
163164
})
164165

166+
t.test('min-release-age-exclude applies to registry packages', async t => {
167+
const name = '@myscope/some-package'
168+
const version = '1.2.3'
169+
const { npm, outputs } = await loadMockNpm(t, {
170+
prefixDir: {
171+
package: {
172+
'package.json': JSON.stringify({ name, version }),
173+
},
174+
},
175+
config: {
176+
'min-release-age': 7,
177+
'min-release-age-exclude': ['@myscope/*'],
178+
},
179+
})
180+
const registry = new MockRegistry({
181+
tap: t,
182+
registry: npm.config.get('registry'),
183+
})
184+
const manifest = registry.manifest({ name, versions: [version] })
185+
await registry.package({
186+
manifest,
187+
times: 2,
188+
tarballs: { [version]: path.join(npm.prefix, 'package') },
189+
})
190+
191+
await npm.exec('pack', [`${name}@${version}`])
192+
193+
const filename = 'myscope-some-package-1.2.3.tgz'
194+
t.strictSame(outputs, [filename])
195+
t.ok(fs.statSync(path.resolve(npm.prefix, filename)))
196+
})
197+
198+
t.test('excluded alias name does not bypass min-release-age for its target', async t => {
199+
const target = 'other-package'
200+
const version = '1.2.3'
201+
const { npm } = await loadMockNpm(t, {
202+
config: {
203+
'min-release-age': 7,
204+
'min-release-age-exclude': ['@myscope/*'],
205+
},
206+
})
207+
const registry = new MockRegistry({
208+
tap: t,
209+
registry: npm.config.get('registry'),
210+
})
211+
await registry.package({
212+
manifest: registry.manifest({ name: target, versions: [version] }),
213+
})
214+
215+
await t.rejects(
216+
npm.exec('pack', [`@myscope/alias@npm:${target}@${version}`]),
217+
{ code: 'ETARGET' }
218+
)
219+
})
220+
165221
t.test('invalid packument', async t => {
166222
const { npm, outputs } = await loadMockNpm(t, {
167223
prefixDir: {

0 commit comments

Comments
 (0)