Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
aaae4d9481 fix(install): improve peer dependency warning messages
Previously, peer dependency warnings only showed the resolved package
name and version:

  warn: incorrect peer dependency "no-deps@2.0.0"

This was unhelpful as users couldn't tell:
- Which package required the peer dependency
- What version was expected

Now the warning includes all the helpful information:

  warn: "peer-deps-fixed@1.0.0" has incorrect peer dependency "no-deps@2.0.0" (expected "^1.0.0")

Changes:
- Add findDependencyOwner() to find the package that owns a dependency
- Update both peer dependency warning sites to show:
  - The requiring package name and version
  - The resolved peer dependency name and version
  - The expected version range from peerDependencies

Closes #26076

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 19:27:32 +00:00
3 changed files with 176 additions and 20 deletions

View File

@@ -1519,16 +1519,35 @@ fn getOrPutResolvedPackage(
const ver_tag = version.tag;
if ((res_tag == .npm and ver_tag == .npm) or (res_tag == .git and ver_tag == .git) or (res_tag == .github and ver_tag == .github)) {
const existing_package = this.lockfile.packages.get(existing_id);
this.log.addWarningFmt(
null,
logger.Loc.Empty,
this.allocator,
"incorrect peer dependency \"{f}@{f}\"",
.{
existing_package.name.fmt(this.lockfile.buffers.string_bytes.items),
existing_package.resolution.fmt(this.lockfile.buffers.string_bytes.items, .auto),
},
) catch unreachable;
const buf = this.lockfile.buffers.string_bytes.items;
if (findDependencyOwner(this, dependency_id)) |owner_id| {
const owner_package = this.lockfile.packages.get(owner_id);
this.log.addWarningFmt(
null,
logger.Loc.Empty,
this.allocator,
"\"{f}@{f}\" has incorrect peer dependency \"{f}@{f}\" (expected \"{f}\")",
.{
owner_package.name.fmt(buf),
owner_package.resolution.fmt(buf, .auto),
existing_package.name.fmt(buf),
existing_package.resolution.fmt(buf, .auto),
version.literal.fmt(buf),
},
) catch unreachable;
} else {
this.log.addWarningFmt(
null,
logger.Loc.Empty,
this.allocator,
"incorrect peer dependency \"{f}@{f}\" (expected \"{f}\")",
.{
existing_package.name.fmt(buf),
existing_package.resolution.fmt(buf, .auto),
version.literal.fmt(buf),
},
) catch unreachable;
}
successFn(this, dependency_id, existing_id);
return .{
// we must fetch it from the packages array again, incase the package array mutates the value in the `successFn`
@@ -1556,16 +1575,35 @@ fn getOrPutResolvedPackage(
if ((res_tag == .npm and ver_tag == .npm) or (res_tag == .git and ver_tag == .git) or (res_tag == .github and ver_tag == .github)) {
const existing_package_id = list.items[0];
const existing_package = this.lockfile.packages.get(existing_package_id);
this.log.addWarningFmt(
null,
logger.Loc.Empty,
this.allocator,
"incorrect peer dependency \"{f}@{f}\"",
.{
existing_package.name.fmt(this.lockfile.buffers.string_bytes.items),
existing_package.resolution.fmt(this.lockfile.buffers.string_bytes.items, .auto),
},
) catch unreachable;
const buf = this.lockfile.buffers.string_bytes.items;
if (findDependencyOwner(this, dependency_id)) |owner_id| {
const owner_package = this.lockfile.packages.get(owner_id);
this.log.addWarningFmt(
null,
logger.Loc.Empty,
this.allocator,
"\"{f}@{f}\" has incorrect peer dependency \"{f}@{f}\" (expected \"{f}\")",
.{
owner_package.name.fmt(buf),
owner_package.resolution.fmt(buf, .auto),
existing_package.name.fmt(buf),
existing_package.resolution.fmt(buf, .auto),
version.literal.fmt(buf),
},
) catch unreachable;
} else {
this.log.addWarningFmt(
null,
logger.Loc.Empty,
this.allocator,
"incorrect peer dependency \"{f}@{f}\" (expected \"{f}\")",
.{
existing_package.name.fmt(buf),
existing_package.resolution.fmt(buf, .auto),
version.literal.fmt(buf),
},
) catch unreachable;
}
successFn(this, dependency_id, list.items[0]);
return .{
// we must fetch it from the packages array again, incase the package array mutates the value in the `successFn`
@@ -1862,6 +1900,18 @@ fn resolutionSatisfiesDependency(this: *PackageManager, resolution: Resolution,
return false;
}
/// Find the package that owns a given dependency ID by searching through
/// all packages' dependency slices.
fn findDependencyOwner(this: *PackageManager, dependency_id: DependencyID) ?PackageID {
const pkg_deps = this.lockfile.packages.items(.dependencies);
for (pkg_deps, 0..) |deps, pkg_id| {
if (deps.contains(dependency_id)) {
return @intCast(pkg_id);
}
}
return null;
}
const string = []const u8;
const std = @import("std");

View File

@@ -7673,7 +7673,9 @@ describe("yarn tests", () => {
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("error:");
expect(err).not.toContain("not found");
// Verify the improved peer dependency warning message format
expect(err).toContain("incorrect peer dependency");
expect(err).toContain('"peer-deps-fixed@1.0.0" has incorrect peer dependency "no-deps@2.0.0" (expected "^1.0.0")');
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",

View File

@@ -0,0 +1,104 @@
import { spawn, write } from "bun";
import { afterAll, beforeAll, beforeEach, expect, setDefaultTimeout, test } from "bun:test";
import { bunExe, bunEnv as env, VerdaccioRegistry } from "harness";
import { join } from "path";
// Test for https://github.com/oven-sh/bun/issues/26076
// Peer dependency warning messages should include:
// 1. The requiring package name and version
// 2. The actual resolved version
// 3. The expected version range
let registry: VerdaccioRegistry;
let packageDir: string;
let packageJson: string;
setDefaultTimeout(1000 * 60 * 5);
// Helper to get IPv6-compatible registry URL
function registryUrl() {
// Verdaccio binds to IPv6 by default when running under Bun, use [::1] instead of localhost
return `http://[::1]:${registry.port}/`;
}
beforeAll(async () => {
registry = new VerdaccioRegistry();
await registry.start();
// Wait for the registry to be fully ready (verdaccio binds to IPv6)
let retries = 10;
while (retries > 0) {
try {
const resp = await fetch(registryUrl());
if (resp.ok) break;
} catch {
await Bun.sleep(500);
retries--;
}
}
});
afterAll(() => {
registry.stop();
});
beforeEach(async () => {
({ packageDir, packageJson } = await registry.createTestDir({
bunfigOpts: { saveTextLockfile: false, linker: "hoisted" },
}));
env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache");
env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp");
// Override the bunfig.toml to use IPv6 address
const bunfigPath = join(packageDir, "bunfig.toml");
await write(
bunfigPath,
`[install]
cache = "${join(packageDir, ".bun-cache")}"
saveTextLockfile = false
registry = "${registryUrl()}"
linker = "hoisted"
`,
);
});
test("peer dependency warnings include helpful version information", async () => {
// peer-deps-fixed has peerDependencies: { "no-deps": "^1.0.0" }
// Installing no-deps@2.0.0 should trigger a peer dependency warning
await write(
packageJson,
JSON.stringify({
name: "test-peer-warning",
version: "1.0.0",
dependencies: {
"peer-deps-fixed": "1.0.0",
"no-deps": "2.0.0",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env,
});
const out = await stdout.text();
const err = await stderr.text();
const exitCode = await exited;
// Should complete successfully
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("error:");
// Verify the improved warning message format:
// "peer-deps-fixed@1.0.0" has incorrect peer dependency "no-deps@2.0.0" (expected "^1.0.0")
expect(err).toContain("incorrect peer dependency");
expect(err).toContain("peer-deps-fixed@1.0.0");
expect(err).toContain("no-deps@2.0.0");
expect(err).toContain("^1.0.0");
expect(exitCode).toBe(0);
});