Skip to content
36 changes: 36 additions & 0 deletions src/install/lockfile/Package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2165,6 +2165,42 @@
out
};

// `Features::LINK` skips dependencies and peerDependencies: the linked
// package is a symlink into its real source tree, so its own install
// owns that node_modules. Runtime resolution realpath's the symlink
// before walking node_modules, which means peers installed in *this*
// project are invisible to the linked package. pnpm prints a warning
// in this situation (https://github.com/pnpm/pnpm/pull/5876); do the
// same so `bun link` users aren't surprised.
Comment thread
robobun marked this conversation as resolved.
Outdated
if FEATURES == Features::LINK
&& pm.options.log_level != crate::package_manager::LogLevel::Silent
{
if let Some(peer_deps) = json.as_property(b"peerDependencies") {
if peer_deps.expr.property_count() > 0 {
let name = json
.as_property(b"name")
.and_then(|q| q.expr.as_utf8(&bump))
.unwrap_or(b"");
bun_core::warn!(
"Linked package <b>\"{}\"<r> declares peerDependencies that will not resolve from this project:",
bstr::BStr::new(name),
);
peer_deps.expr.for_each_property(|key, _loc, value| {
let ver = value.as_utf8(&bump).unwrap_or(b"");
bun_core::pretty_errorln!(
" <d>-<r> {}<d>@{}<r>",
bstr::BStr::new(key),
bstr::BStr::new(ver),
);
});

Check warning on line 2195 in src/install/lockfile/Package.rs

View check run for this annotation

Claude / Claude Code Review

Warning does not filter optional peer dependencies

This warning enumerates every key in `peerDependencies` without consulting `peerDependenciesMeta` for `optional: true` entries. A linked package that declares only optional peers (a common plugin-host pattern) will get the full warning + `--preserve-symlinks` remedy even though absence of those peers is contractually fine. Consider reading `peerDependenciesMeta` here (the block just below at line ~2219 already shows how) and either annotating optional peers or suppressing the warning entirely wh
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
bun_core::pretty_errorln!(
" Linked packages resolve modules from their real location on disk.\n Run bun with <cyan>--preserve-symlinks<r> to resolve peers from this project's node_modules.",
);
Output::flush();
}
}
}

let mut workspace_names = workspace_map::WorkspaceMap::init();
// defer workspace_names.deinit(); — Drop handles it

Expand Down
93 changes: 93 additions & 0 deletions test/cli/install/bun-link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,3 +472,96 @@ it("should link dependency without crashing", async () => {
// This should fail with a non-zero exit code.
expect(await exited4).toBe(1);
});

// https://github.com/oven-sh/bun/issues/13676
it("should warn when linked package has peerDependencies", async () => {
const link_name = basename(link_dir).slice("bun-link.".length);
await writeFile(
join(link_dir, "package.json"),
JSON.stringify({
name: link_name,
version: "0.0.1",
peerDependencies: {
"peer-one": "^1.0.0",
"peer-two": "*",
},
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "consumer",
version: "0.0.2",
}),
);

// Registering the link (no args) should not warn: nothing is being resolved.
{
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "link"],
cwd: link_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = stderrForInstall(await stderr.text());
expect(err).not.toContain("peerDependencies");
expect(await stdout.text()).toContain(`Success! Registered "${link_name}"`);
expect(await exited).toBe(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

// `bun link <name>` should warn, listing every peer and the workaround.
{
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "link", link_name],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = stderrForInstall(await stderr.text());
expect(err).toContain(`Linked package "${link_name}" declares peerDependencies`);
expect(err).toContain("peer-one@^1.0.0");
expect(err).toContain("peer-two@*");
expect(err).toContain("--preserve-symlinks");
expect(await stdout.text()).toContain(`installed ${link_name}@link:${link_name}`);
expect(await exited).toBe(0);
}

// `bun install` with a `link:` dependency in package.json should warn too.
{
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "consumer",
version: "0.0.2",
dependencies: {
[link_name]: `link:${link_name}`,
},
}),
);
const { err } = await runBunInstall(env, package_dir, { allowWarnings: true });
expect(err).toContain(`Linked package "${link_name}" declares peerDependencies`);
expect(err).toContain("peer-one@^1.0.0");
expect(err).toContain("--preserve-symlinks");
}

// --silent suppresses the warning.
{
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "link", link_name, "--silent"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = stderrForInstall(await stderr.text());
expect(err).not.toContain("peerDependencies");
expect(err).not.toContain("--preserve-symlinks");
await stdout.text();
expect(await exited).toBe(0);
}
});
Loading