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')

Open
#10,000 0 comments 0 reactions 0 assignees View on GitHub
Stale
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.