Skip to content

feat(app-builder-lib): exclude default production deps from node_modules instead of erroring (BREAKING) - #9994

Merged
mmaietta merged 7 commits into
electron-userland:masterfrom
liamcmitchell:prod-deps
Jul 15, 2026
Merged

feat(app-builder-lib): exclude default production deps from node_modules instead of erroring (BREAKING)#9994
mmaietta merged 7 commits into
electron-userland:masterfrom
liamcmitchell:prod-deps

Conversation

@liamcmitchell

Copy link
Copy Markdown
Contributor

I want to install packages like electron and react as production deps for other tooling (SBOM generation, license/vulnerability checking). I want production dep to mean "shipped with my application".

electron-builder currently forces production dep to mean "copy this into node_modules in my application".

  • electron as a prod dep fails to build with error ⨯ Package "electron" is only allowed in "devDependencies". Please remove it from the "dependencies" section in your package.json.
  • react as a prod dep and bundled using vite/electron produces an unneeded copy in the ASAR

This PR replaces the previous hard-coded error with a configurable ignore list, making the prod/dev definition more flexible.

The ignore list takes precedence over files matching. Ignored modules and their exclusive dependencies are removed before file matching.

I'm hoping this can get into the upcoming v27 so I shortened the default list to just electron and electron-builder assuming the others on the prev list are no longer needed.

In a bundling world, an externals whitelist is probably more useful than an ignore blacklist but I figured this as implemented is a more manageable change for now.

Tested on a local project using the yalc symlinks as described in CONTRIBUTING.md:

  • excluded production dependencies from the app's node_modules (see ignoredProductionDependencies)  dependencies=@electron-internal/extract-zip, @electron/get, @types/node, electron, env-paths, progress, react, react-dom, scheduler, sumchecker, undici, undici-types

Modules in app.asar were as expected.

@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 185d1ae

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
app-builder-lib Major
electron-builder Major
dmg-builder Major
electron-builder-squirrel-windows Major
electron-forge-maker-appimage Major
electron-forge-maker-nsis-web Major
electron-forge-maker-nsis Major
electron-forge-maker-snap Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@mmaietta

Copy link
Copy Markdown
Collaborator

@liamcmitchell thanks for the contribution! Mind if I/claude push some updates to this PR?


Some identified gaps I'd like to discuss (or that I can push changes to cover):

Blocking --- the exclusion defeats the #9945 collection validation. collectNodeModulesWithLogging validates each collected tree against the as-declared deps (appFileCopier.ts:305-310), but the collector has already stripped the ignored names from the tree it returns. Two failure modes:

  • Monorepo app with dependencies: { electron, "@org/shared": "workspace:*" } (the PR's own use case): workspace specs get filtered, so requiredExternalDeps = ["electron"], and every correct collection now fails collectionMatchesAppDependencies (appFileCopier.ts:238-247). If an earlier pm/dir attempt produced the classic wrong-root npm tree, the fallback ??= path returns that wrong tree --- resurrecting exactly the Yarn 4 (Berry) workspaces: production deps of an Electron sub-package are missing from app.asar (26.15.3) #9945 bug in the scenario this PR enables.
  • App whose only prod deps are all ignored (e.g. just electron): every collection is empty, all pm×dir combos get tried and discarded, the user gets the scary "no node modules returned while searching directories" warning on a correct build, and the "excluded production dependencies" info line never fires.

Fix: make validation exclusion-aware --- the collector already returns excludedDependencies, so count a declared dep as accounted-for if it's in that list, and treat non-empty exclusions + empty tree as a successful empty collection.

Should-fix:

  • Ignored names are severed from every node in the graph, not just the app's direct deps (nodeModulesCollector.ts:305-311). Safe for electron, but the docs recommend adding bundler-inlined deps like react --- if any kept, un-bundled dep does require("react") at runtime, its copy is silently deleted and the app crashes with MODULE_NOT_FOUND. Deserves at least a doc note, ideally a warn when a severed edge originates from a kept non-root package.
  • The old hard error also covered electron-prebuilt and electron-rebuild; the new default list and the tripwire warning cover only electron/electron-builder, so those now ship silently (electron-prebuilt drags a full Electron binary along). Cheap to add them (and arguably electron-nightly) to the defaults.
  • The migration table says the ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY removal has "Impact: None", but anyone who set that var did so to bundle electron-builder --- in v27 it's silently excluded and their app breaks at runtime with only an easy-to-miss info log. Excluding a package the app directly declares probably deserves a warn for one major, like the feat(updater): harden NSIS web-installer auto-updates: secure-by-default disableWebInstaller, a v27 grace period, and target self-identification #9979 grace-period pattern.

Nits: exclusion log reports names but diffs name@version, so it can claim debug was excluded while debug@3 still ships via a kept dep; npm aliases ("my-electron": "npm:electron@...") escape matching; the default list is duplicated across code, jsdoc, and scheme.json with nothing keeping the code constant in sync; and null resolves to the default list so [] is the only way to disable --- worth a doc sentence.

Test gap: coverage is all unit-level against the traversal collector with a mocked packager. No pack-level test asserting the final app actually omits electron when declared in dependencies, no test for the blocking interaction above, and nothing exercises the npm/pnpm collectors whose graph-id formats differ.

@mmaietta mmaietta left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Marking as Request Changes to discuss the previous comment 🙂

* origin/master:
  feat(updater): fix manifest sha512 hash-encoding sniffing, add opt-in Linux package-signature verification (electron-userland#9990)
  fix: don't empty locales dir when electronLanguages uses bare language codes (electron-userland#10007)
  fix: extract .tar.7z snap template archives correctly (electron-userland#10003)
@liamcmitchell

Copy link
Copy Markdown
Contributor Author

@liamcmitchell thanks for the contribution! Mind if I/claude push some updates to this PR?

Thanks for the review! Feel free to push changes however you see fit.

Some identified gaps I'd like to discuss (or that I can push changes to cover):

Blocking --- the exclusion defeats the #9945 collection validation. collectNodeModulesWithLogging validates each collected tree against the as-declared deps (appFileCopier.ts:305-310), but the collector has already stripped the ignored names from the tree it returns.

I pushed a change to mark nodes as excluded instead of removing them completely. This means the full tree can be used for collection validation as before. The nodes marked excluded are then ignored in the later file copy step.

The cpu/os exclusion could use the same mechanism but I've left it as-is.

  • Ignored names are severed from every node in the graph, not just the app's direct deps (nodeModulesCollector.ts:305-311). Safe for electron, but the docs recommend adding bundler-inlined deps like react --- if any kept, un-bundled dep does require("react") at runtime, its copy is silently deleted and the app crashes with MODULE_NOT_FOUND. Deserves at least a doc note, ideally a warn when a severed edge originates from a kept non-root package.

Changed the algorithm to only exclude root deps, not nested.

  • The old hard error also covered electron-prebuilt and electron-rebuild; the new default list and the tripwire warning cover only electron/electron-builder, so those now ship silently (electron-prebuilt drags a full Electron binary along). Cheap to add them (and arguably electron-nightly) to the defaults.

When I checked, both those projects seemed abandoned so I thought it would be a good time to clean them up. Feel free to re-add.

That flag is undocumented and the commits/issues I found referencing it are very old. Thought it made sense to remove in a breaking change. I would be surprised if a single user is using this.

Nits: exclusion log reports names but diffs name@version, so it can claim debug was excluded while debug@3 still ships via a kept dep;

Now that only top-level deps are excluded I think it makes sense to log just the name. I don't think logging excluded transitive deps and/or versions are helpful.

npm aliases ("my-electron": "npm:electron@...") escape matching;

Did the previous hard-coded list catch aliases?

the default list is duplicated across code, jsdoc, and scheme.json with nothing keeping the code constant in sync;

Not sure how to better keep that in sync

and null resolves to the default list so [] is the only way to disable --- worth a doc sentence.

Disabling is not a common case. Current doc Overriding this option **replaces** the default list should be sufficient.

Test gap: coverage is all unit-level against the traversal collector with a mocked packager. No pack-level test asserting the final app actually omits electron when declared in dependencies, no test for the blocking interaction above, and nothing exercises the npm/pnpm collectors whose graph-id formats differ.

Let me know if and what coverage you want from me.

claude added 4 commits July 13, 2026 14:34
…usion

Close the ignoredProductionDependencies test gaps:

- pack-level assertPack test: an app declaring electron (default-ignored)
  and ms in dependencies packs with ms bundled in app.asar and electron
  omitted, exercising the real npm collector end-to-end
- exclusion-aware validation: an app whose every external production dep
  is ignored still yields a successful collection (no spurious "no node
  modules returned" warning, exclusion summary still logged), and a
  monorepo whose only non-workspace dep is ignored still validates
- npm collector graph ids: exclusion marking through canned npm list
  trees, pinning down that npm aliases match by alias key, not by the
  underlying package name
- collectionMatchesAppDependencies counts excluded modules as validation
  markers (issue electron-userland#9945 interaction)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaKXX4mAESjjtvTYCgZtaa
…null semantics

The exclusion summary now records the full name@version graph id — like
the other logSummary buckets — so the log stays truthful when another
version of the same name still ships via a kept dependency (previously
it could claim `debug` was excluded while `debug@3` remained bundled).

Also document in the ignoredProductionDependencies jsdoc (source of
truth for scheme.json):
- matching is by the declared dependency name, so npm aliases are
  matched by their alias key, never the underlying package name
- only app-declared dependencies are eligible for exclusion, plus the
  MODULE_NOT_FOUND footgun for kept packages that require() an excluded
  name without declaring it
- null (or omitted) applies the default list; [] disables exclusion

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaKXX4mAESjjtvTYCgZtaa
Move DEFAULT_IGNORED_PRODUCTION_DEPENDENCIES next to the
ignoredProductionDependencies option in configuration.ts so the code
constant, the jsdoc @default, and the generated scheme.json live in one
place, add a test pinning the generated schema default to the constant,
and regenerate scheme.json to pick up the new jsdoc wording.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaKXX4mAESjjtvTYCgZtaa
Cover every breaking change this PR introduces:

- ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY: no longer presented
  as impact-free — describe the behavior flip (the var used to make
  electron-builder bundle; now the package is silently excluded by
  default) and how to bundle it again via ignoredProductionDependencies
- electron-prebuilt / electron-rebuild: document that the v26 hard error
  is removed WITHOUT a default exclusion (they now ship if declared,
  electron-prebuilt dragging a full Electron binary along), that
  electron-nightly was never guarded, why the guard is dropped in a
  major (both packages long deprecated), and what users should do
- exclusion semantics: only app-declared dependencies are eligible,
  exclusive transitive subtrees go with them, and a kept package that
  require()s an excluded name without declaring it crashes with
  MODULE_NOT_FOUND; npm aliases match by alias key
- update the at-a-glance table row and the v26-to-v27 walkthrough
  checklists accordingly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaKXX4mAESjjtvTYCgZtaa
@mmaietta

Copy link
Copy Markdown
Collaborator

Thanks for the replies! Added a few commits to expand test coverage, log the excluded deps w/ version, regenerated the scheme.json to sync schema/jsdoc/code, and updated breaking-changes doc for the removed items (I'm aligned with your removals of them for v27)

@mmaietta
mmaietta merged commit 0721e95 into electron-userland:master Jul 15, 2026
111 of 113 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants