`bun install` from a workspace member fails to resolve `workspace:*` when the member is glob-matched and its path is under 3 characters
### What version of Bun is running?
1.3.6+d530ed993 (reproduced locally). The responsible code is byte-identical in 1.3.14, the latest release. Also reproduced on 1.3.14 on Linux (Vercel build image).
Verified **fixed** on `1.4.0-canary.1+4c689909e`.
### What platform is your computer?
Darwin 25.5.0 arm64 (also seen on Vercel's Linux build image)
### What steps can reproduce the bug?
No dependencies, no network:
```sh
mkdir -p ~/bunrepro/{aa,aaa,lib} && cd ~/bunrepro
echo '{ "name": "root", "private": true, "workspaces": ["*"] }' > package.json
echo '{ "name": "@x/lib", "version": "1.0.0" }' > lib/package.json
echo '{ "name": "aa", "version": "1.0.0", "dependencies": { "@x/lib": "workspace:*" } }' > aa/package.json
echo '{ "name": "aaa", "version": "1.0.0", "dependencies": { "@x/lib": "workspace:*" } }' > aaa/package.json
(cd aa && bun install) # fails
(cd aaa && bun install) # succeeds
```
`aa` fails, `aaa` succeeds. The two directories are identical apart from one letter in the name.
### What is the expected behavior?
Both succeed. A workspace member's directory name length should not affect whether `bun install`
run from inside it can find the workspace root.
### What do you see instead?
```
$ cd aa && bun install
bun install v1.3.6 (d530ed99)
error: Workspace dependency "@x/lib" not found
Searched in "./*"
Workspace documentation: https://bun.com/docs/install/workspaces
error: @x/lib@workspace:* failed to resolve
```
### Additional information
#### The rule
It is the length of the member's path **relative to the workspace root**, and only when the member
is matched by a **glob** entry. Threshold is 3.
| root `workspaces` | member | rel. path | len | result |
| --- | --- | --- | --- | --- |
| `["*"]` | `a/` | `a` | 1 | fail |
| `["*"]` | `aa/` | `aa` | 2 | fail |
| `["*"]` | `aaa/` | `aaa` | 3 | ok |
| `["p/*"]` | `p/a/` | `p/a` | 3 | ok |
| `["pkgs/*"]` | `pkgs/a/` | `pkgs/a` | 6 | ok |
| `["aa"]` (literal, no glob) | `aa/` | `aa` | 2 | **ok** |
The glob pattern's own length is irrelevant — `["pkgs/*"]` + `pkgs/a` (path 6, pattern 6) passes
while `["*"]` + `aa` (path 2, pattern 1) fails.
#### Second symptom, same cause
Listing a short-path member **both** explicitly and via a glob produces a spurious duplicate-name
error that cites the same `package.json` twice:
```sh
# root: { "workspaces": ["*", "aa"] }
$ cd aa && bun install
1 | { "name": "@x/mem", ... }
^
error: Workspace name "@x/mem" already exists
at /tmp/x/aa/package.json:1:11
note: Package name is also declared here # <- same file
```
With a 3+ character path (`["*", "aaa"]`) there is no duplicate. This is the useful fingerprint:
it is only reachable if the two code paths are minting **two different keys for one directory**.
#### Root cause
Two lines interact (paths/lines from `bun-v1.3.14`):
1. **`src/install/lockfile/Package/WorkspaceMap.zig:304`** — the glob branch strips
`"package.json"` but not the separator before it, so the directory path keeps a trailing slash:
```zig
const abs_workspace_dir_path: string = strings.withoutSuffixComptime(abs_package_json_path, "package.json");
// "/repo/aa/package.json" -> "/repo/aa/"
```
The literal branch at **`WorkspaceMap.zig:191`** strips `std.fs.path.sep_str ++ "package.json"`
and is correct — which is why explicit entries work at any length.
2. **`src/paths/resolve_path.zig:466`** — `relativeToCommonPath` only trims a trailing separator
from its result when that result is longer than 3 bytes:
```zig
if (out_slice.len > 3 and out_slice[out_slice.len - 1] == separator) {
out_slice.len -= 1;
}
```
So `relativePlatform("/repo", "/repo/aa/")` returns `"aa/"` (len 3, not trimmed) and is inserted as
the `WorkspaceMap` key, while `"/repo/aaa/"` returns `"aaa"` (len 4, trimmed).
Root detection in **`src/install/PackageManager.zig:750`** then compares the map key against the
relative path of the cwd, which never has a trailing slash:
```zig
const child_path = bun.path.relativeNormalized(json_source.path.name.dir, child_cwd, .auto, true); // "aa"
...
if (strings.eqlLong(maybe_workspace_path, path, true)) { /* found root */ } // "aa" != "aa/"
```
The comparison fails, no workspace root is found, and Bun falls back to treating the member
directory as a standalone project — whose `package.json` has no `workspaces` field, so the
`workspace:*` dependency has nothing to resolve against.
(The `"./*"` in the error text is unrelated to any search path — it is the dependency's own version
literal `*` with `./` prepended by `PackageWorkspaceSearchPathFormatter` in
`src/install/resolvers/folder_resolver.zig`.)
#### Regression range
The trailing slash was introduced by commit `cabfca403`, PR #11177 ("Fix adding packages in
workspaces"), which replaced `entry_path` with
`withoutSuffixComptime(abs_package_json_path, "package.json")` in the glob branch only. It first
shipped in **1.1.9** and the line is unchanged through **1.3.14**.
PR #11177 is the PR that closed #11064 — the same error string, and also triggered by running
install from inside a workspace member. So this looks like a narrow regression introduced by that
fix, for the subset of members whose relative path is 1–2 characters.
#### Already fixed on `main`, apparently by accident
The Rust rewrite (#30412, merged one day after 1.3.14 was tagged) consolidated both branches onto a
shared helper that strips the separator:
```rust
// src/install/lockfile/Package/WorkspaceMap.rs:177
fn workspace_dir_of(abs_package_json_path: &[u8]) -> &[u8] {
strings::without_suffix_comptime(
abs_package_json_path,
const_format::concatcp!(SEP_STR, "package.json").as_bytes(),
)
}
```
Confirmed against `1.4.0-canary.1+4c689909e`: every case in the table above passes, including the
duplicate-name variant.
That appears incidental rather than deliberate, hence this report:
- there is no regression test pinning the behaviour, so it could be reintroduced;
- the trailing-slash trim it depended on is still present at `src/paths/resolve_path.rs:549`
(`if out_len > 3 && buf[out_len - 1] == separator`), plus three more `len() > 3` guards in that
file. The `> 3` appears intended to protect Windows roots such as `C:\`; on POSIX it silently
suppresses trimming for any 1–3 byte result;
- the entire 1.3.x line remains affected, and canary reports `1.4.0`, so there is no patch-release
path for anyone on 1.3.x today.
Would you accept a test covering "install from a glob-matched workspace whose relative path is 1–2
characters", and is making the `> 3` guard Windows-only the change you'd want, or should the call
site in `WorkspaceMap` simply not pass a trailing slash?
#### Workarounds, for anyone who hits this
- run `bun install` from the workspace root (`"installCommand": "cd .. && bun install"` on Vercel,
where install runs with cwd set to the project's Root Directory);
- or list the short-path member **explicitly** and remove the bare glob — note that adding it
alongside the glob triggers the duplicate-name error above;
- or move/rename it so its relative path is 3+ characters (`pm/` -> `apps/pm/` also works).
关闭于 3 天前 1 条评论