Skip to content

install: support path-form link: dependencies - #35461

Open
robobun wants to merge 5 commits into
mainfrom
farm/2cb823b1/link-protocol-path
Open

install: support path-form link: dependencies#35461
robobun wants to merge 5 commits into
mainfrom
farm/2cb823b1/link-protocol-path

Conversation

@robobun

@robobun robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • "pkg": "link:./lib/pkg" in package.json fails bun install with error: Package "pkg" is not linked (the yarn/pnpm spelling for "symlink this directory"; Bun link works but installs says package is not linked #4719, Install package from local directory #5045).
  • Cause: every consumer of a Symlink resolution treats the value as a name registered by bun link and joins it onto the global link dir (PackageManagerEnqueue.rs Tag::Symlink arm, PackageInstaller.rs, PackageManagerDirectories.rs, isolated_install/Installer.rs::append_store_path).
  • link:../x is the one shape that got further on main: normalize_package_json_path resolves a .-prefixed value against the project root regardless of the global dir, so it resolves, is written to bun.lock as link:../x, and then fails at link time (FileNotFound: failed linking dependency). See lockfile note below.
  • The pnpm-lock.yaml migration (pnpm.rs) already had a notion of path-form link: (it stored pnpm's value verbatim) that disagreed with the rest of install on both what counts as a path and what the stored value is relative to.

Fix

  • dependency::is_link_path: a link: value is a path unless it is an npm package name. This is the rule pnpm.rs already used, and pnpm.rs now calls the shared helper, so the two writers of bun.lock agree (link:lib/foo is a path on both; link:foo and link:@s/foo stay global).
  • Stored form: Package::parse_dependency re-bases a path-form value from the declaring package.json onto the project root (as it already does for file:), and link_path_for_lockfile makes it /-separated and ./-prefixed, so what is written always reads back as a path. pnpm.rs produces the same form from pnpm's importer-relative value.
  • Resolution: a path-form value resolves via a new Relative(Symlink) arm in folder_resolver.rs (project-relative, Features::LINK as before); name-form is unchanged. An overlong target is reported as ENAMETOOLONG, as the other path-buffer overflows in the crate are.
  • Install: the three installer sites symlink a path-form value from the project root instead of the global dir; the isolated linker only pre-opens the global dir if a name-form resolution exists.
  • Containment, same rule as file:: a target inside the project is always allowed, including one declared by a transitive file: package. One that leaves the project (.. or absolute) is allowed only when the root or a workspace declared the dependency, or a root overrides/resolutions entry names it (Lockfile::link_target_allowed_for_*). Checked at resolve time and again in both installers, because an install driven by an existing lockfile never resolves (hoisted: per package in PackageInstaller; isolated: in the pass over resolutions that runs before any task is scheduled, so no worker can create the link first). Refusal is its own error (UnsafeLinkTarget, "refusing to link ...") rather than a missing package.json.
  • bun add --filter (add_remove_with_filter.rs::local_relative_path) re-spells a local path relative to each target; it now decides "is this link: value a path" with is_link_path too, so bun add link:vendor/foo --filter api writes link:../../vendor/foo into the workspace rather than a value the workspace would read as packages/api/vendor/foo. Name-form values are still passed through untouched.
  • Version::eql for Symlink compares literal like Folder does; bun.lock round-trips literal, so comparing the re-based value made every second install see a diff.
  • Missing target reports Could not find package.json at "<path>" instead of the bun link hint.

Why this is right: link: with a path has meant "symlink this directory, do not install its dependencies" in yarn and pnpm for years, and file: in bun already defines where a relative path is anchored and who may point outside the project; this makes link: follow both.

Lockfile note: bun.lock entries of the form x@link:../y or x@link:/abs that main already writes (and then fails to install) start installing once this lands, subject to the containment rule above. Name-form entries are unaffected. Migrated pnpm lockfiles that previously carried importer-relative targets are now written root-relative; ones migrated by older versions were never installable.

Verified:

  • test/cli/install/bun-link.test.ts: ./, bare lib/x (asserts the ./ stored form), ../, absolute, scoped, workspace-member-relative, missing target; each under both linkers with a --frozen-lockfile re-install. Containment block: transitive escape refused at resolve and from an existing lockfile (asserting nothing by that name exists anywhere under node_modules, isolated store entries included; the released binary creates one at .bun/<pkg>/node_modules/<name>), transitive in-project target installed, overrides/resolutions exemption honoured; both linkers.
  • test/cli/install/bun-add-filter.test.ts: link:./vendor/foo and link:vendor/foo are written to the target as link:../../vendor/foo, recorded as foo@link:./vendor/foo, install, and survive --frozen-lockfile; a missing target is refused naming the per-target spelling and writes nothing. This replaces the test that asserted the old "is not linked" failure for a link: path.
  • test/cli/install/migration/pnpm-lock-migration.test.ts: file: specifier from a non-root importer migrates to a root-relative target and installs.
  • Rebased onto main after install: pnpm parity — dedupe, prune, pm licenses, audit fix, add --filter/--catalog, nested overrides, transitive update, and workspace fixes #38333 (which added bun add --filter, scoped overrides and pnpm-lock-v9.test.ts); that file, the other pnpm migration suites, the file: containment tests in bun-install.test.ts, bun-add-catalog.test.ts local-path tests, and the name-form link: tests in bun-prune / bun-pm-licenses pass locally. In bun-link.test.ts, the pre-existing should link dependency without crashing fails locally under a debug build because the install failure it expects dumps a symbolized trace to stdout (cfg(bun_debug) in PackageInstaller.rs, not touched here); it is green in CI.
  • cargo check / clippy on linux and cargo check for x86_64-pc-windows-msvc.

Fixes #4719
Fixes #5045

Background

  • bun link / name form: bun link in a package dir symlinks it into ~/.bun/install/global/node_modules/<name>; "dep": "link:<name>" elsewhere then symlinks from there. That is the only form bun supported; this PR adds the path form beside it.
  • Symlink resolution: the lockfile entry for a link: dependency. Its one string field was always a name; it is now either a name or a root-relative/absolute path, and is_link_path tells them apart.
  • Features::LINK: the package.json parse mode for link targets; it reads the manifest but does not enqueue the target's dependencies (unchanged).
  • bin_target_escapes_package_dir: the existing file: check for "does this relative path have a .. that climbs above its base, or is it absolute"; reused here with the project root as the base.
  • Resolve vs install: bun install with no usable lockfile runs resolution (PackageManagerEnqueue.rs); with one, it goes straight to the installers. Any policy about a resolution therefore has to be enforced in both places, which is how the file: checks are laid out.
  • bun add --filter: adds a positional to the selected workspaces. A local path on the command line is relative to the cwd, so it is re-spelled relative to each workspace's package.json before being written; local_relative_path decides which positionals get that treatment.

no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-add-filter.test.ts test/cli/install/bun-link.test.ts

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The installer now recognizes filesystem-shaped link: dependencies, normalizes and validates their lockfile paths, resolves them relative to the appropriate package, avoids unnecessary global-link-directory setup, and adds coverage for supported path forms, security checks, and missing packages.

Filesystem link dependencies

Layer / File(s) Summary
Link-path contracts and lockfile resolution
src/install/dependency.rs, src/install/error.rs, src/install/lockfile/..., src/install/pnpm.rs, src/install/resolvers/folder_resolver.rs, src/install/migration.rs
Adds link-path classification, lockfile normalization and validation, relative folder resolution, pnpm path rebasing, and updated migration errors.
Package resolution and target validation
src/install/PackageManager/PackageManagerEnqueue.rs, src/install/PackageInstaller.rs, src/install/isolated_install.rs
Validates link targets, rejects unauthorized paths, resolves allowed paths relative to the project, and reports target-specific missing-package errors.
Cache and installed path routing
src/install/PackageManager/PackageManagerDirectories.rs, src/install/isolated_install/Installer.rs
Routes filesystem links through direct or relative paths while preserving global-link-directory routing for named symlinks.
Filesystem link integration coverage
test/cli/install/bun-link.test.ts, test/cli/install/migration/pnpm-lock-migration.test.ts
Tests path forms, hoisted and isolated linkers, missing packages, root-escape rejection, in-project links, overrides, resolutions, and pnpm migration.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address local directory links, avoid requiring bun link, preserve named links, and support scoped linked packages [#4719, #5045].
Out of Scope Changes check ✅ Passed The lockfile, migration, security validation, installation, and test changes directly support path-based link dependency resolution.
Description check ✅ Passed The description clearly explains the problem, fix, security behavior, testing, and related issues, although it does not use the repository’s exact template headings.
Title check ✅ Passed The title clearly and concisely summarizes the main change: support for path-form link: dependencies.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:05 PM PT - Aug 14th, 2026

@robobun, your commit 96f05e2 has some failures in Build #97320 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 35461

That installs a local version of the PR into your bun-35461 executable, so you can run:

bun-35461 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. bun install: support non-existent workspaces and relative link dependencies #23026 - Explicitly requests support for relative path link: dependencies (link:./my-pkg), which is exactly what this PR implements
  2. Install package from local directory #5045 - Requests link:../local-package to work as a filesystem path like pnpm, which this PR now enables
  3. Installing local packages will link the npm published one #5742 - bun i ../firstpackage produces a path-form link: entry that was incorrectly resolved as a global name, pulling the npm version instead of the local directory

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #23026
Fixes #5045
Fixes #5742

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/install/PackageManager/PackageManagerEnqueue.rs:2695-2711 — When a link:./path target is missing (no directory or no package.json), install still emits Package "…" is not linked with the bun link my-pkg-name-from-package-json remedy — which is actively wrong for the path form this PR adds. Consider adding an is_link_path check in the else-branch here (and the mirrored verbose-warning branch at ~1451-1459) that instead says something like package.json not found at "./lib/mypkg".

    Extended reasoning...

    What happens

    With this PR, "mypkg": "link:./does-not-exist" now takes the new Relative(Symlink) path in get_or_put_resolved_package (PackageManagerEnqueue.rs:2691+). FolderResolution::get_or_put calls read_package_json_from_disk, which fails to open ./does-not-exist/package.json with ENOENT; folder_resolver.rs maps that to FolderResolution::Err(MissingPackageJSON).

    Back in enqueue_dependency, the Err(MissingPackageJSON) result is mapped to None (~line 1370). With _result == None and the dependency required, control reaches the if dependency_tag == Workspace { … } else { … } block. dependency_tag is Symlink, so the else-branch at 1428-1436 fires and prints:

    error: Package "mypkg" is not linked
    
    To install a linked package:
       bun link my-pkg-name-from-package-json
    
    Tip: the package name is from package.json, which can differ from the folder name.
    

    The verbose-warning branch at 1451-1459 has the identical text.

    Why it is wrong for the new syntax

    That message and remedy were written for link:<name>, where the fix genuinely is "run bun link in the target package first". For link:./path, the user never intended to touch the global link registry — the path they wrote in package.json is bad (typo, directory not created yet, missing package.json). Telling them to run bun link sends them in the wrong direction. Per REVIEW.md: "Error messages are reviewed word-for-word as code. Name what failed and why: the specific resource (quoted path/URL) … a concrete remedy."

    Step-by-step repro

    1. package.json contains { "dependencies": { "mypkg": "link:./lib/mypkg" } } and ./lib/mypkg does not exist.
    2. bun installTag::infer sees link:Tag::Symlink; version.symlink() is ./lib/mypkg.
    3. get_or_put_resolved_package: is_link_path("./lib/mypkg") is true → resolves <top_level_dir>/lib/mypkg and calls FolderResolution::get_or_put(Relative(Symlink), …).
    4. read_package_json_from_diskFile::openat(cwd, "<abs>/lib/mypkg/package.json", O_RDONLY)ENOENT → returned as FolderResolution::Err(MissingPackageJSON).
    5. Caller maps Err(MissingPackageJSON)None; dependency.behavior.is_required() is true; dependency_tag == Symlink (not Workspace) → else-branch at 1428 emits Package "mypkg" is not linked with the bun link hint.
    6. Install exits nonzero (correct), but the diagnostic misdirects.

    Why nothing prevents it

    The else-branch at 1428 only distinguishes Workspace from everything else. There is no third arm that checks dependency::is_link_path(this.lockfile.str(version.symlink())) before falling back to the global-link message.

    Suggested fix

    Add that third arm in both the required-error branch (1428-1436) and the verbose-warning branch (1451-1459), e.g.:

    } else if dependency::is_link_path(this.lockfile.str(version.symlink())) {
        bun_ast::add_error_pretty!(
            this.log_mut(), None, bun_ast::Loc::EMPTY,
            "package.json not found at \"{}\" for <b>link:<r> dependency \"{}\"\n\n",
            bstr::BStr::new(this.lockfile.str(version.symlink())),
            bstr::BStr::new(this.lockfile.str(&name)),
        );
    } else {
        // existing "is not linked" message
    }

    Impact / severity

    This is diagnostic quality only — the happy path works, and the failure path still exits nonzero, so nothing silently succeeds. Filed as a nit; worth polishing since it affects the first error users of the new syntax will hit, but should not block the fix for #4719.

Comment thread src/install/isolated_install/Installer.rs
Comment thread src/install/dependency.rs Outdated
Comment thread src/install/PackageManager/PackageManagerEnqueue.rs Outdated
Comment thread src/install/dependency.rs Outdated
Comment thread src/install/PackageManager/PackageManagerEnqueue.rs
Comment thread test/cli/install/bun-link.test.ts
Comment thread test/cli/install/bun-link.test.ts
Comment thread test/cli/install/bun-link.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/install/lockfile/Package.rs`:
- Around line 1759-1796: Guard the manual "./" prefix construction in the
symlink handling at src/install/lockfile/Package.rs:1789-1793 and
src/install/resolvers/folder_resolver.rs:480-486 with a capacity check before
writing to PathBuffer; when relative.len() + 2 exceeds the buffer capacity, use
each location’s existing unsafe-folder-path error path and return the
appropriate install/resolution error instead of indexing the buffer. Preserve
the current path formatting for values that fit.

In `@test/cli/install/bun-link.test.ts`:
- Around line 477-608: Add a regression test within the “link: with a filesystem
path” suite where a non-workspace transitive dependency declares a path-form
link: pointing outside the project. Use the existing install helpers and
assertions to verify installation fails closed with the specific path-form/link
error, and confirm it does not resolve or link the host path; keep the existing
direct root and workspace-member cases unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 907656df-892c-4337-b36b-61d6c32fee50

📥 Commits

Reviewing files that changed from the base of the PR and between 028f7a3 and 0e2f601.

📒 Files selected for processing (10)
  • src/install/PackageInstaller.rs
  • src/install/PackageManager/PackageManagerDirectories.rs
  • src/install/PackageManager/PackageManagerEnqueue.rs
  • src/install/dependency.rs
  • src/install/isolated_install.rs
  • src/install/isolated_install/Installer.rs
  • src/install/lockfile/Package.rs
  • src/install/migration.rs
  • src/install/resolvers/folder_resolver.rs
  • test/cli/install/bun-link.test.ts

Comment thread src/install/lockfile/Package.rs
Comment thread test/cli/install/bun-link.test.ts
Comment thread src/install/lockfile/Package.rs Outdated
Comment thread src/install/PackageManager/PackageManagerEnqueue.rs Outdated
Comment thread test/cli/install/bun-link.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new issues found; the fixes in 911a9a6 address the three items from the last round (Symlink Version::eql now compares literal, the enqueue path-form gate now rejects non-workspace/non-override deps and honours overrides, and the test helper uses runBunInstall's returned strings directly). Given this is a feature addition to the package manager touching lockfile resolution shape and both installers, a human should sign off.

What was reviewed:

  • is_link_path classification vs. Tag::infer's folder shapes; ~/ is classified but not expanded (documented, matches file:).
  • Transitive/override trust gate in PackageManagerEnqueue.rs — now fails closed with MissingPackageJSON and mirrors the Tag::Folder arm.
  • Lockfile round-trip: Version::eql for Symlink compares literal; workspace-member test runs a second --frozen-lockfile install.
  • ./-prefix buffer writes in Package.rs and folder_resolver.rs guarded against MAX_PATH_BYTES overflow.
Extended reasoning...

Overview

This PR adds support for link:./path (yarn/pnpm syntax) in bun install, resolving the path relative to the declaring package instead of treating it as a global-link-dir name. It touches 9 Rust files across dependency parsing (is_link_path), package.json normalization (Package.rs), resolution enqueue (PackageManagerEnqueue.rs), the folder resolver, and both hoisted (PackageInstaller.rs, PackageManagerDirectories.rs) and isolated (isolated_install/{Installer.rs,mod}) installers, plus a migration warning rewording. ~130 lines of new tests cover ./, ../, absolute, scoped, workspace-member, and error-path cases under both linkers.

Security risks

Path-form link: from an untrusted transitive package could symlink an arbitrary host directory into node_modules. The enqueue arm now gates on is_workspace_dependency || overrides.contains_name and returns Err(MissingPackageJSON) otherwise — the same fail-closed shape as the sibling file: arm. The Package.rs normalization still runs unconditionally (no features gate), but since the enqueue rejection precedes any filesystem access this is fail-closed. Practical exposure was already narrow (npm registries reject link: deps; git/tarball packages already run lifecycle scripts).

Level of scrutiny

High. This is the package manager: it changes what gets written into bun.lock for a resolution tag, adds a new GlobalOrRelative::Relative(Symlink) resolver arm, and branches both installers on a new predicate. Lockfile-shape changes are hard to revert once shipped, and the isolated installer's append_store_path runs on worker threads. Five prior review rounds surfaced a lockfile round-trip bug (spurious --frozen-lockfile diff via Version::eql), a trust-gate bypass, a stale migration warning, and several test-hygiene issues — all now addressed.

Other factors

Test coverage is solid for the happy paths and includes a --frozen-lockfile re-install to guard the round-trip fix. CodeRabbit's transitive-escape test suggestion was reasonably declined (would need a synthetic registry package with a link: dep; the gate is identical to the covered file: arm). Not approving because this is a user-facing feature in a critical subsystem with lockfile-format implications — a maintainer should confirm the design (storing project-relative paths as the Symlink resolution, keeping literal unchanged) is the intended long-term shape.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the three items from the last pass are all addressed in 911a9a6 (Symlink Version::eql now compares literal; path-form link: from a non-workspace/non-override dep is rejected outright instead of falling through to the global link dir; checkLink uses out/err directly and the --frozen-lockfile round-trip is asserted). No new findings this pass.

Deferring to a human reviewer given this adds a new link:./path protocol form across the resolver, both linkers, and lockfile serialization, and the is_workspace_dependency || overrides trust gate is security-adjacent.

What was reviewed:

  • is_link_path shape parity with Tag::infer (., /, ~/, Windows drive/\).
  • Lockfile round-trip: Version::eql on literal + --frozen-lockfile re-install in tests.
  • Enqueue trust gate now rejects (not falls through) and includes the override exemption; error/warning wording for path vs name form.
  • ./-prefix buffer writes in Package.rs / folder_resolver.rs are length-checked before copy_from_slice.
Extended reasoning...

Overview

The PR teaches bun install to treat link:./path (and ../, /abs, ~/, Windows-drive) as a filesystem symlink target relative to the project, instead of a globally-registered package name. It touches 10 files: the shared is_link_path classifier in dependency.rs, the Tag::Symlink normalization arm in lockfile/Package.rs, the enqueue branch in PackageManagerEnqueue.rs, three installers (PackageInstaller.rs, PackageManagerDirectories.rs, isolated_install/Installer.rs + its main-thread pre-init in isolated_install.rs), a new Relative(Symlink) arm in folder_resolver.rs, a reworded pnpm-migration warning, and 12 new test cases across hoisted/isolated linkers.

Security risks

The material risk is a downloaded (transitive) package declaring link:/etc or link:../../.. and having that resolve to a host path. Commit 911a9a6 addresses my prior finding here: the enqueue branch now computes trusted = is_workspace_dependency || overrides.contains_name(...) and returns Err(MissingPackageJSON) when untrusted, rather than falling through to the global-link-dir lookup (which the earlier Package.rs normalization could have bypassed). This mirrors the sibling file: gate. The ./-prefix buffer writes in Package.rs and folder_resolver.rs are both bounds-checked before the copy_from_slice.

Level of scrutiny

High. This is package-manager core: dependency parsing, lockfile serialization/diff, and both install linkers. It introduces a new user-facing protocol form (yarn/pnpm parity), and the link: value is untrusted input that becomes a symlink target. The PR went through three review iterations with substantive corrections each time (workspace-relative normalization, Version::eql asymmetry breaking --frozen-lockfile, the trust-gate fallthrough), which is exactly why a human should give the final shape a look before it lands.

Other factors

Test coverage is reasonable — ./, ../, scoped, absolute, workspace-member-relative, and the missing-target error path, each × {hoisted, isolated}, plus a --frozen-lockfile re-install to prove the lockfile round-trips cleanly after the Version::eql fix. All prior review threads are resolved. The one design decision worth a human eye is whether the stored resolution being a project-relative path (kept path-shaped with a ./ prefix so installers can distinguish it from link:<name>) is the right lockfile representation long-term, and whether Features::LINK is the correct feature set for the target's own package.json parse.

Comment thread src/install/PackageManager/PackageManagerEnqueue.rs
Comment thread src/install/PackageManager/PackageManagerEnqueue.rs Outdated
Comment thread src/install/dependency.rs Outdated
Comment thread src/install/isolated_install.rs Outdated
Comment thread src/install/isolated_install/Installer.rs
Comment thread src/install/lockfile/Package.rs Outdated
Comment thread src/install/resolvers/folder_resolver.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/cli/install/bun-link.test.ts (1)

585-608: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Verify that the missing-package diagnostic is path-specific.

Checking only Could not find package.json does not prove that the rejected link: value or resolved target path is identified. Assert the requested path in the error, ideally against the complete expected diagnostic. The PR objective specifically requires a path-specific missing-target error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/install/bun-link.test.ts` around lines 585 - 608, Strengthen the
assertion in the “errors with the path when package.json is missing” test to
verify the complete missing-target diagnostic, including the requested
`./does-not-exist` path (or its resolved target path), rather than only checking
the generic `Could not find package.json` text. Keep the existing assertions
that reject linked-package guidance and confirm exit status 1.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/install/lockfile/Package.rs`:
- Around line 1790-1800: Update the error message in the unsafe-folder-path
branch to include the rejected link target, the maximum permitted path length,
and a remedy such as shortening the path. Preserve the existing error location,
logging call, and InstallFailed return.

In `@test/cli/install/bun-link.test.ts`:
- Around line 491-492: Update the symlink assertions around the readlink calls
in bun-link tests to compare the fully normalized resolved target with the
expected path, replacing basename-based containment checks in both locations.
Preserve platform-independent slash normalization and assert exact equality
rather than matching only a target fragment.

---

Outside diff comments:
In `@test/cli/install/bun-link.test.ts`:
- Around line 585-608: Strengthen the assertion in the “errors with the path
when package.json is missing” test to verify the complete missing-target
diagnostic, including the requested `./does-not-exist` path (or its resolved
target path), rather than only checking the generic `Could not find
package.json` text. Keep the existing assertions that reject linked-package
guidance and confirm exit status 1.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: deb0d16b-541d-4574-aeb1-143a852d8a6e

📥 Commits

Reviewing files that changed from the base of the PR and between 0e2f601 and c325929.

📒 Files selected for processing (5)
  • src/install/PackageManager/PackageManagerEnqueue.rs
  • src/install/dependency.rs
  • src/install/lockfile/Package.rs
  • src/install/resolvers/folder_resolver.rs
  • test/cli/install/bun-link.test.ts

Comment thread src/install/lockfile/Package.rs Outdated
Comment thread test/cli/install/bun-link.test.ts
Comment thread test/cli/install/bun-link.test.ts
@robobun

robobun commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

CI is green on build #80252 (c325929). All review threads are addressed; the two remaining bot suggestions (error-message wording, symlink basename assertion) are intentionally kept consistent with the sibling file: arm and backed by the stronger .toEqual(package.json) check respectively. Ready for review.

@alii alii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. The path form itself and the six tests for it look right; the problems are around it.

  • The trust check on the Symlink arm is not the file: check, only runs on a fresh resolve, reports a refusal as a missing package.json, and nothing exercises it.
  • is_link_path is a second answer to a question pnpm.rs already answers with !is_npm_package_name, and the two disagree on link:lib/foo. bun.lock stores whichever rule ships, so settle it in this PR.
  • pnpm.rs still writes importer-relative link: strings into Symlink resolutions, and the new installer arms now read those as root-relative.
    One correction for the body: link:../x does not fail with "is not linked" on main. It resolves, writes the same link:../x resolution this branch writes, and fails at link time (FileNotFound, verified with bun-debug on main). So lockfiles main already wrote with ../ entries start installing after this lands, which is worth saying since it is a lockfile behavior change.

Comment thread src/install/PackageManager/PackageManagerEnqueue.rs Outdated
Comment thread src/install/PackageManager/PackageManagerEnqueue.rs Outdated
Comment thread src/install/dependency.rs Outdated
Comment thread src/install/migration.rs Outdated
Comment thread src/install/PackageInstaller.rs Outdated
Comment thread src/install/dependency.rs Outdated
Comment thread src/install/dependency.rs Outdated
Comment thread src/install/dependency.rs Outdated
Comment thread src/install/isolated_install.rs Outdated
Comment thread src/install/lockfile.rs Outdated
Comment thread src/install/lockfile.rs
@robobun robobun changed the title install: resolve link:./path relative to the project, not the global link dir install: support path-form link: dependencies Aug 13, 2026
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, all four points addressed in 02f42b0 (comments trimmed in f29d94e); body rewritten to match, including the correction on what main does with link:../x (it resolves and fails at link time, so those bun.lock entries start installing after this; called out under "Lockfile note").

Summary of the rework:

  • One rule: is_link_path is !is_npm_package_name, shared with pnpm.rs; link:lib/foo pinned by a test that also checks the ./lib/foo stored form.
  • Containment is the file: rule (escapes-root check on the stored target, in-project always allowed, root/workspace/override may escape), enforced at resolve time and in both installers; own error message. Tests for refusal at resolve, refusal from an existing lockfile, in-project transitive, and the overrides/resolutions exemption, each under both linkers.
  • pnpm.rs re-bases importer-relative targets onto the root; fixture added with a file: specifier from a non-root importer.

Re-requesting review.

@robobun
robobun requested a review from alii August 13, 2026 02:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/install/pnpm.rs (1)

702-731: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the pnpm link path before calling join_string_buf. Oversized relative paths can exceed the fixed PathBuffer; the unchecked normalizer then panics before link_path_for_lockfile can return invalid_pnpm_lockfile(). Return the existing invalid-lockfile error when the combined path does not fit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/install/pnpm.rs` around lines 702 - 731, Guard the relative-path branch
in the link-path rebasing logic before calling join_string_buf, using the
available PathBuffer capacity or checked join result to detect overflow. When
combining workspace_path and link_path cannot fit, return
invalid_pnpm_lockfile() instead of invoking the unchecked normalizer; preserve
absolute-path handling and successful link_path_for_lockfile processing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/install/PackageInstaller.rs`:
- Around line 1454-1477: Update the DanglingSymlink diagnostic handler to branch
on is_link_path(folder): for missing path-form links, report the target path and
its resolved project-relative location instead of instructing the user to run
bun link, while preserving the existing diagnostic for non-path links. Add a
regression test covering a missing link:../missing-package target.
- Around line 1458-1468: The combined condition in the dependency-link
validation must distinguish `folder.len() >= self.folder_path_buf.len()` from
`link_target_allowed_for_package(...)` failures. Emit a dedicated path-length
error for overlong targets that identifies the rejected target and supported
limit; retain the existing authorization error only for containment failures and
include the concrete remedy and relevant target details.

In `@src/install/PackageManager/PackageManagerEnqueue.rs`:
- Around line 1371-1383: Update the UnsafeLinkTarget handling in the dependency
enqueue flow so optional dependencies emit a warning when
this.options.log_level.is_verbose() is enabled before returning Ok(()). Match
the existing verbose skip-warning pattern and include the dependency and link
target context; preserve the current error diagnostic for required dependencies
and the immediate successful return.

---

Outside diff comments:
In `@src/install/pnpm.rs`:
- Around line 702-731: Guard the relative-path branch in the link-path rebasing
logic before calling join_string_buf, using the available PathBuffer capacity or
checked join result to detect overflow. When combining workspace_path and
link_path cannot fit, return invalid_pnpm_lockfile() instead of invoking the
unchecked normalizer; preserve absolute-path handling and successful
link_path_for_lockfile processing.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 02f51b18-7ca0-4552-b60b-5a3fd82c7d84

📥 Commits

Reviewing files that changed from the base of the PR and between c325929 and 02f42b0.

📒 Files selected for processing (11)
  • src/install/PackageInstaller.rs
  • src/install/PackageManager/PackageManagerEnqueue.rs
  • src/install/dependency.rs
  • src/install/error.rs
  • src/install/isolated_install.rs
  • src/install/lockfile.rs
  • src/install/lockfile/Package.rs
  • src/install/pnpm.rs
  • src/install/resolvers/folder_resolver.rs
  • test/cli/install/bun-link.test.ts
  • test/cli/install/migration/pnpm-lock-migration.test.ts

Comment thread src/install/PackageInstaller.rs
Comment thread src/install/PackageInstaller.rs
Comment thread src/install/PackageManager/PackageManagerEnqueue.rs

@alii alii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks ready to merge from this side. Checked the new head against each point from the last round: is_link_path is now the one rule and pnpm.rs calls it, the containment check is the file: rule with its own error and runs at resolve time and in both installers, pnpm migration re-bases onto the root, and the body's account of what main does with link:../x is now right; each has a test (read, not run here, and the body itself says bun-link.test.ts was deferred to CI, so confirm CI on this head ran it). One non-blocking note inline. The branch now conflicts with main in src/install/dependency.rs and test/cli/install/bun-link.test.ts and needs a rebase before it can land.

Comment thread src/install/isolated_install.rs Outdated
Comment thread test/cli/install/bun-link.test.ts Outdated
Comment thread src/install/isolated_install.rs Outdated
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Comment thread src/install/isolated_install.rs
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (bdb7382). The branch is now the reviewed change squashed into one commit (7a65e13, conflicts were the pub(crate)/cfg(windows) neighbour in dependency.rs and the stderrForInstall import, which #37000 removed) plus one commit with everything that changed since your review (8aa43aa; a5b7327 only shortens a comment):

  • The isolated refusal moved into the pass over resolutions that runs before the first start_task, as suggested; the per-entry arm is back to main's text.
  • The refusal tests now assert that nothing by that name exists anywhere under node_modules (store entries and dangling links included). You were right that the isolated half was vacuous: against the released binary that fixture produces .bun/evil@file+evil/node_modules/loot, which the old assertions missed and the new one catches.
  • Transitive block pinned to the dummy registry; migration test converted to tempDir to match its file after test: convert tempDirWithFiles callers to using tempDir #36194.

On CI running bun-link.test.ts: build 93900 (f29d94e, the head you reviewed) passed 156 test lanes with that file outside the failure set; the only lanes without a result were darwin-x64, whose build step failed downloading the WebKit tarball. Locally on the rebased tree the file passes except should link dependency without crashing, which times out identically on pristine main in this container (it installs from the registry) and is green in CI. Build 94180 is running on the current head.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

CI on a5b7327 (build 94180): 155 of 156 test lanes green, bun-link.test.ts and the migration test in none of the failure sets. The one red is test/bake/deinitialization.test.ts segfaulting in dev-server teardown on Windows x64 (reported separately; unrelated to install). The retry-passes are tagged flakes; the only install one, bun-patch.test.ts on Windows aarch64, is the long local-tarball spec from #37469, which never touches link:.

Comment thread src/install/resolvers/folder_resolver.rs

@alii alii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still looks ready to merge from this side. The rebased commit is the reviewed diff apart from main's pub(crate) and stderrForInstall changes; the isolated refusal now runs in the pre-scheduling pass over resolutions and the per-entry arm is back to main's text, and the refusal tests now glob for the name under node_modules, which does catch the .bun//node_modules/ link the current build creates from that fixture. CI on this head is as reported by the author, not checked here.

@alii

alii commented Aug 15, 2026

Copy link
Copy Markdown
Member

@robobun this conflicts with main now, please rebase and get a fresh CI run so it can be merged.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main and pushed (96f05e2); the rebase notes are in the PR description (Error::PathTooLong is gone on main, and the new bun add --filter code now re-spells link: paths with this PR's path-or-name rule, with its test updated accordingly).

CI on the rebased head (build 97320): every Linux lane (x64, aarch64, ASAN), both Windows lanes and both macOS builds passed; the only test failures recorded are unrelated flakes that passed on retry. The two remaining jobs are the darwin 14 aarch64 test shards, which no agent has picked up in over five hours (the same is true of every other branch build in the queue right now), so I am not retriggering. Ready for review from my side.

robobun and others added 5 commits August 15, 2026 03:32
"pkg": "link:./dir" (and ../dir, lib/dir, /abs) now symlinks that directory,
as in yarn and pnpm. A link: value is a path unless it is a package name;
names keep going through the bun link global directory.

Path targets are re-based onto the project root when the declaring
package.json is parsed (as file: already is) and stored ./-prefixed and
/-separated, so a stored value always reads back as a path. The pnpm
migration produces the same form from pnpm's importer-relative value.

Containment follows the file: rule: a target inside the project is always
allowed; one that leaves it is allowed only when the root, a workspace, or
a root override declared the dependency. Checked at resolve time and in
both installers, since an install from an existing lockfile never resolves.

Version::eql for Symlink compares the literal like Folder does; bun.lock
round-trips the literal, so comparing the re-based value produced a diff on
every second install.

Fixes #4719
Fixes #5045
…schedules any task

Entries are scheduled parent first, so a dependent's SymlinkDependencies
step could create the link before the per-entry refusal exited. Do the
check in the pass over resolutions that already runs before the first
start_task; the per-entry arm goes back to what it was.

Tests: assert that nothing named after the refused package exists
anywhere under node_modules, store entries included, instead of the two
hoisted-layout paths (which the isolated half never created, so it passed
on main too). Pin the transitive block to the dummy registry like the
first block. Migration test uses tempDir like the rest of its file.
Error::PathTooLong was removed on main; report an overlong link: target as
ENAMETOOLONG like the other path-buffer overflows in this crate.

`bun add --filter` re-spells a local path relative to each target. It only
did so for link: values starting with ".", but a link: value is a path
whenever it is not a package name, so use the same predicate; otherwise
`bun add link:vendor/foo --filter api` writes a value that the target
workspace reads as packages/api/vendor/foo. The add --filter test that
asserted the old "is not linked" failure for a link: path now asserts the
re-spelled value is written and installs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install package from local directory Bun link works but installs says package is not linked

2 participants