ITADN

`bun install` from a workspace member fails to resolve `workspace:*` when the member is glob-matched and its path is under 3 characters

#39566Closedandreiujica 创建于 3 天前
A
andreiujicacommented
### 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 条评论