From c12fa51298f5a1e57fcc1c5b2ad09da72993c6f7 Mon Sep 17 00:00:00 2001 From: Martin Ruiz Date: Mon, 13 Jul 2026 11:42:12 -0700 Subject: [PATCH] fix(pack): honor min-release-age-exclude Apply release-age exclusions to both manifest resolutions used by npm pack. Resolve aliases by their registry target so an excluded alias cannot bypass the cutoff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dec204b6-ad66-45a5-8228-831e306f6ba6 --- lib/commands/pack.js | 16 ++++++++--- test/lib/commands/pack.js | 56 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/lib/commands/pack.js b/lib/commands/pack.js index 8a4d2f0f74382..97ff48a6b2e64 100644 --- a/lib/commands/pack.js +++ b/lib/commands/pack.js @@ -2,6 +2,10 @@ const pacote = require('pacote') const libpack = require('libnpmpack') const npa = require('npm-package-arg') const { log, output } = require('proc-log') +const { + isReleaseAgeExcluded, + trustedSpecName, +} = require('@npmcli/arborist/lib/release-age-exclude.js') const { getContents, logTar } = require('../utils/tar.js') const BaseCommand = require('../base-cmd.js') @@ -35,8 +39,12 @@ class Pack extends BaseCommand { const manifests = [] for (const arg of args) { const spec = npa(arg) + const options = isReleaseAgeExcluded( + trustedSpecName(spec), + this.npm.flatOptions.minReleaseAgeExclude + ) ? { ...this.npm.flatOptions, before: null } : this.npm.flatOptions const manifest = await pacote.manifest(spec, { - ...this.npm.flatOptions, + ...options, Arborist, preferOnline: true, _isRoot: true, @@ -44,14 +52,14 @@ class Pack extends BaseCommand { if (!manifest._id) { throw new Error('Invalid package, must have name and version') } - manifests.push({ arg, manifest }) + manifests.push({ arg, manifest, options }) } // Load tarball names up for printing afterward to isolate from the noise generated during packing const tarballs = [] - for (const { arg, manifest } of manifests) { + for (const { arg, manifest, options } of manifests) { const tarballData = await libpack(arg, { - ...this.npm.flatOptions, + ...options, foregroundScripts: this.npm.config.isDefault('foreground-scripts') ? true : this.npm.config.get('foreground-scripts'), diff --git a/test/lib/commands/pack.js b/test/lib/commands/pack.js index 121992bdfc887..d857401bc63fd 100644 --- a/test/lib/commands/pack.js +++ b/test/lib/commands/pack.js @@ -1,6 +1,7 @@ const t = require('tap') const { load: loadMockNpm } = require('../../fixtures/mock-npm') const { cleanZlib } = require('../../fixtures/clean-snapshot') +const MockRegistry = require('@npmcli/mock-registry') const path = require('node:path') const fs = require('node:fs') @@ -162,6 +163,61 @@ t.test('foreground-scripts can still be set to false', async t => { t.throws(() => fs.statSync(path.resolve(npm.prefix, filename))) }) +t.test('min-release-age-exclude applies to registry packages', async t => { + const name = '@myscope/some-package' + const version = '1.2.3' + const { npm, outputs } = await loadMockNpm(t, { + prefixDir: { + package: { + 'package.json': JSON.stringify({ name, version }), + }, + }, + config: { + 'min-release-age': 7, + 'min-release-age-exclude': ['@myscope/*'], + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + const manifest = registry.manifest({ name, versions: [version] }) + await registry.package({ + manifest, + times: 2, + tarballs: { [version]: path.join(npm.prefix, 'package') }, + }) + + await npm.exec('pack', [`${name}@${version}`]) + + const filename = 'myscope-some-package-1.2.3.tgz' + t.strictSame(outputs, [filename]) + t.ok(fs.statSync(path.resolve(npm.prefix, filename))) +}) + +t.test('excluded alias name does not bypass min-release-age for its target', async t => { + const target = 'other-package' + const version = '1.2.3' + const { npm } = await loadMockNpm(t, { + config: { + 'min-release-age': 7, + 'min-release-age-exclude': ['@myscope/*'], + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + await registry.package({ + manifest: registry.manifest({ name: target, versions: [version] }), + }) + + await t.rejects( + npm.exec('pack', [`@myscope/alias@npm:${target}@${version}`]), + { code: 'ETARGET' } + ) +}) + t.test('invalid packument', async t => { const { npm, outputs } = await loadMockNpm(t, { prefixDir: {