electron-userland / electron-userland/electron-builder
pnpm 11 workspace: collector drops dependencies due to cross-project deduped `pnpm list` output (Cannot find module 'mime-types' / 'mime-db')
- Dominant language
- TypeScript
- Stars
- 14.7k
- Forks
- 1.9k
- Avg merge
- 3d 12h
- Merged PRs (30d)
- 48
Description
### Summary
In a pnpm **11** workspace, the pnpm node-module collector silently drops packages from `app.asar`, and the packaged app crashes on launch with `Cannot find module 'mime-types'`. The cause is pnpm 11's `pnpm list --json` output, which **dedupes entries across workspace projects**: for a given `name@version`, only ONE occurrence anywhere in the workspace carries the fully-expanded subtree; every other occurrence is a stub with `dedupedDependenciesCount > 0` and no children.
Two distinct collector behaviors interact badly with this format:
1. **Released 26.15.3 / 26.15.6:** the `collectedDeps` visited-set (introduced in #9732, same family as #9903) prunes by `name@version`. If the *first* visited occurrence of an intermediate package (e.g. `@trpc/server`) is one whose own children are deduped stubs, the *later* occurrence that carries the real expanded subtree is skipped entirely — everything below it is never collected. In our tree this drops **7 packages**: `mime-types`, `mime-db`, `get-intrinsic`, `es-object-atoms`, `call-bind-apply-helpers`, `dunder-proto`, `get-proto`. They are logged under "unresolved duplicate dependency references" / "cannot find path for dependency" and omitted from the asar.
2. **Current `master` (7a0abca):** much better — 6 of the 7 come back — but it still drops **`mime-db`**, the runtime dependency of `mime-types`. The app's own `mime-types` entry is a deduped stub, so in `extractProductionDependencyGraph` the on-disk `package.json` is used for the `all` check, but the children iterated are still the stub's (empty) `tree.dependencies` — so `mime-db` is never linked into the production graph. It also isn't reachable any other way: `mime-db` sits as a top-level entry of the app project in the pnpm list output (pnpm 11 hoisted-dedupe placement), where the graph extraction filters it out because it isn't a declared dependency of the app's `package.json`. Packaged app then fails with `Cannot find module 'mime-db'` (required from `mime-types/index.js`).
Note: adding the missing package as a *direct* dependency of the app does **not** help — our `mime-types` was a direct dependency (pinned `2.1.35`) and was still dropped, because its entry in the app's tree is a deduped stub either way.
### Environment
- electron-builder / app-builder-lib: 26.15.3 and 26.15.6 (also reproduced against `master` @ 7a0abca via the collector's `getNodeModules()` directly)
- pnpm: **11.9.0** (worked fine before upgrading from pnpm 10.34.4 — same repo, same electron-builder)
- pnpm workspace monorepo (Next.js apps + packages + one Electron app project), default virtual-store layout (no `node-linker=hoisted`)
- Affects **both macOS and Windows** packaging: verified on both platforms (macOS 15 universal dmg/zip, and Windows NSIS) — the packaged artifacts are missing the identical set of modules.
### Shape of the pnpm 11 list output that triggers it
The Electron app (`electron/`) depends on `axios` → `form-data` → `mime-types@2.1.35`, and directly on `mime-types@2.1.35`. Another workspace project (`apps/web`) also reaches `mime-types@2.1.35` via `@trpc/server > express > accepts`. pnpm 11 emits the full expansion **only once**, in the web project's tree:
```
web > @trpc/client > @trpc/server > express > accepts > mime-types@2.1.35 ← only full entry (has children)
web > api > @trpc/server ← visited FIRST; its express child is a deduped stub
app > mime-types@2.1.35 { dedupedDependenciesCount: 1 } ← stub, no children
app > mime-db@1.52.0 ← full, but top-level of the project ⇒ filtered (not a declared dep)
```
On 26.15.x, `@trpc/server@11.0.0-rc.642` gets marked visited at `web > api > @trpc/server` (children deduped), so `web > @trpc/client > @trpc/server` — the copy that actually contains `express > accepts > mime-types` — is skipped by the visited-set, and `mime-types@2.1.35` ends up with no full entry collected at all.
### Results (running `getNodeModules({packageName})` against our workspace)
| collector | result |
|---|---|
| 26.15.3 / 26.15.6 release | ❌ drops `mime-types`, `mime-db`, `get-intrinsic`, `es-object-atoms`, `call-bind-apply-helpers`, `dunder-proto`, `get-proto` → `Cannot find module 'mime-types'` on launch |
| `master` @ 7a0abca | ❌ still drops `mime-db` → `Cannot find module 'mime-db'` on launch |
| 26.15.3 + patch below | ✅ all packages collected, no "unresolved duplicate" warnings |
### Workaround we're shipping (26.15.3, via `pnpm patch`)
In `pnpmNodeModulesCollector.js` `collectDepsRecursively`, re-walk children of already-visited nodes instead of pruning the whole subtree (the visited-set still prevents re-registering/re-locating; since each JSON node is structurally visited at most once, this stays O(total nodes)):
```js
if (this.collectedDeps.has(id)) {
// pnpm v11 dedupes the list output: the fully-expanded subtree for a given
// name@version can appear at a later occurrence than the first one visited.
// Re-walk this occurrence's children (without re-registering the package)
// so those subtrees are not lost.
if (value.dependencies || value.optionalDependencies) {
await this.collectDepsRecursively(value);
}
return;
}
```
For `master`, the remaining gap is different: when a deduped stub's dependencies are discovered from the on-disk `package.json` (the `all` map), the children that get linked into the production graph should come from that map too (or from the deduped target's real entry), not from the stub's empty `tree.dependencies`.
### Suggested regression test
A workspace fixture with two projects where (a) project B contains the only full expansion of a shared `name@version` behind an intermediate package whose first-visited occurrence has deduped children, and (b) the app's own direct dep is a deduped stub whose transitive dep (`mime-types` → `mime-db`) only exists elsewhere as a project-top-level dedupe placement. Must be generated with pnpm ≥ 11 (`pnpm list --prod --json --depth Infinity` output differs fundamentally from pnpm 10).
Happy to test a fix branch against our workspace and report back.
Contributor guide
Research direction
Start in pnpmNodeModulesCollector.js at collectDepsRecursively and extractProductionDependencyGraph, then reproduce the behavior through getNodeModules({packageName}) with pnpm 11 output. Add a regression fixture with two workspace projects and run the collector against it; done means all transitive packages are collected without unresolved-duplicate warnings or missing modules such as mime-types and mime-db.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- electron, typescript
- Domain
- build-system, release
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100