Skip to content
39 changes: 39 additions & 0 deletions src/install/lockfile/Package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2165,6 +2165,45 @@ impl Package<u64> {
out
};

// Linked packages are symlinks; node_modules lookup realpath's them
// first, so peers installed here are invisible. Match pnpm and warn.
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"");
let peer_meta = json.as_property(b"peerDependenciesMeta");
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"");
let is_optional = peer_meta
.as_ref()
.and_then(|m| m.expr.as_property(key))
.and_then(|m| m.expr.as_property(b"optional"))
.map(|o| matches!(&o.expr.data, ExprData::EBoolean(b) if b.value))
.unwrap_or(false);
bun_core::pretty_errorln!(
" <d>-<r> {}<d>@{}{}<r>",
bstr::BStr::new(key),
bstr::BStr::new(ver),
if is_optional { " (optional)" } else { "" },
);
});
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
97 changes: 97 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,100 @@ 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": "*",
},
peerDependenciesMeta: {
"peer-two": { optional: true },
},
}),
);
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).not.toContain("peer-one@^1.0.0 (optional)");
expect(err).toContain("peer-two@* (optional)");
expect(err).toContain("--preserve-symlinks");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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