Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
4546a71fe4 fix(install): don't apply minimum-release-age to transitive dependencies
When `minimum-release-age` is configured, exact version pins from
transitive dependencies (e.g. `@types/bun` depending on `bun-types@1.3.9`)
were incorrectly filtered, causing install failures even though the user
only declared the parent package as a direct dependency.

The fix checks whether the dependency being resolved is a workspace
dependency (direct dep of any workspace package.json) before applying
the age filter. Transitive dependencies are no longer filtered since
their versions are determined by their parent package authors.

Fixes #27004

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-13 19:09:38 +00:00
2 changed files with 116 additions and 3 deletions

View File

@@ -725,7 +725,15 @@ pub fn enqueueDependencyWithMainAndSuccessFn(
if (version.tag == .npm and version.value.npm.version.isExact()) {
if (loaded_manifest.?.findByVersion(version.value.npm.version.head.head.range.left.version)) |find_result| {
if (this.options.minimum_release_age_ms) |min_age_ms| {
if (!loaded_manifest.?.shouldExcludeFromAgeFilter(this.options.minimum_release_age_excludes) and Npm.PackageManifest.isPackageVersionTooRecent(find_result.package, min_age_ms)) {
// Only apply minimum-release-age to workspace dependencies
// (direct deps of any workspace package.json).
// Transitive dependencies should not be filtered, as they are
// pinned by their parent package and filtering them would break
// the dependency tree (see #27004).
if (this.lockfile.isWorkspaceDependency(id) and
!loaded_manifest.?.shouldExcludeFromAgeFilter(this.options.minimum_release_age_excludes) and
Npm.PackageManifest.isPackageVersionTooRecent(find_result.package, min_age_ms))
{
const package_name = this.lockfile.str(&name);
const min_age_seconds = min_age_ms / std.time.ms_per_s;
this.log.addErrorFmt(null, logger.Loc.Empty, this.allocator, "Version \"{s}@{f}\" was published within minimum release age of {d} seconds", .{ package_name, find_result.version.fmt(this.lockfile.buffers.string_bytes.items), min_age_seconds }) catch {};
@@ -1625,9 +1633,19 @@ fn getOrPutResolvedPackage(
this.options.minimum_release_age_ms != null,
) orelse return null; // manifest might still be downloading. This feels unreliable.
// Only apply minimum-release-age to workspace dependencies
// (direct deps of any workspace package.json).
// Transitive dependencies should not be filtered, as they are pinned
// by their parent package and filtering them would break the
// dependency tree (see #27004).
const effective_min_age_ms: ?f64 = if (this.lockfile.isWorkspaceDependency(dependency_id))
this.options.minimum_release_age_ms
else
null;
const version_result: Npm.PackageManifest.FindVersionResult = switch (version.tag) {
.dist_tag => manifest.findByDistTagWithFilter(this.lockfile.str(&version.value.dist_tag.tag), this.options.minimum_release_age_ms, this.options.minimum_release_age_excludes),
.npm => manifest.findBestVersionWithFilter(version.value.npm.version, this.lockfile.buffers.string_bytes.items, this.options.minimum_release_age_ms, this.options.minimum_release_age_excludes),
.dist_tag => manifest.findByDistTagWithFilter(this.lockfile.str(&version.value.dist_tag.tag), effective_min_age_ms, this.options.minimum_release_age_excludes),
.npm => manifest.findBestVersionWithFilter(version.value.npm.version, this.lockfile.buffers.string_bytes.items, effective_min_age_ms, this.options.minimum_release_age_excludes),
else => unreachable,
};

View File

@@ -786,6 +786,69 @@ describe("minimum-release-age", () => {
return Response.json(packageData);
}
// TEST PACKAGE: parent-package (has exact transitive dep on recent-child-package)
if (url.pathname === "/parent-package") {
const packageData = {
name: "parent-package",
"dist-tags": { latest: "1.0.0" },
versions: {
"1.0.0": {
name: "parent-package",
version: "1.0.0",
dependencies: {
"recent-child-package": "1.0.0", // exact pin on a recent version
},
dist: {
tarball: `${mockRegistryUrl}/parent-package/-/parent-package-1.0.0.tgz`,
integrity: "sha512-parent1==",
},
},
},
time: {
"1.0.0": daysAgo(10), // old enough to pass filter
},
};
if (req.headers.get("accept")?.includes("application/vnd.npm.install-v1+json")) {
return Response.json({
name: packageData.name,
"dist-tags": packageData["dist-tags"],
versions: packageData.versions,
});
}
return Response.json(packageData);
}
// TEST PACKAGE: recent-child-package (transitive dep that is too recent)
if (url.pathname === "/recent-child-package") {
const packageData = {
name: "recent-child-package",
"dist-tags": { latest: "1.0.0" },
versions: {
"1.0.0": {
name: "recent-child-package",
version: "1.0.0",
dist: {
tarball: `${mockRegistryUrl}/recent-child-package/-/recent-child-package-1.0.0.tgz`,
integrity: "sha512-child1==",
},
},
},
time: {
"1.0.0": daysAgo(1), // too recent - would fail if filtered
},
};
if (req.headers.get("accept")?.includes("application/vnd.npm.install-v1+json")) {
return Response.json({
name: packageData.name,
"dist-tags": packageData["dist-tags"],
versions: packageData.versions,
});
}
return Response.json(packageData);
}
// Serve tarballs
if (url.pathname.includes(".tgz")) {
// Match both regular and scoped package tarballs
@@ -1693,6 +1756,38 @@ registry = "${mockRegistryUrl}"`,
// Direct dependency should be filtered
expect(lockfile).toContain("regular-package@2.1.0");
});
test("exact-pinned transitive dependencies that are too recent should not be filtered (#27004)", async () => {
// parent-package@1.0.0 (10 days old, passes filter) depends on
// recent-child-package@1.0.0 (1 day old, would fail if filtered).
// The child should NOT be filtered because it's a transitive dependency.
using dir = tempDir("transitive-exact-pin", {
"package.json": JSON.stringify({
dependencies: {
"parent-package": "*",
},
}),
".npmrc": `registry=${mockRegistryUrl}`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("minimum release age");
expect(exitCode).toBe(0);
const lockfile = await Bun.file(`${dir}/bun.lock`).text();
// Both packages should be installed
expect(lockfile).toContain("parent-package");
expect(lockfile).toContain("recent-child-package");
});
});
describe("special dependencies", () => {