fix: support ESM app entrypoints in the universal shim#210
Merged
Conversation
When the x64 and arm64 app.asar (or app dir) contents diverge, the
generated entry shim used `require()` to load the per-arch archive. If
the inner app's entrypoint is an ES module this throws ERR_REQUIRE_ESM
at launch.
Detect each arch's entrypoint module format from its package.json and,
only when both arches agree on ESM, ship an ESM shim that uses dynamic
import. If the two arches disagree on module system, fail the build with
a clear error instead of producing a broken bundle.
- Add detectEntrypointModule / resolveShimModule to asar-utils
- Add ESM shim sources entry-asar/esm/{has,no}-asar.mts compiled to .mjs
- Wire both the HAS_ASAR and NO_ASAR shim sites in makeUniversalApp
- Add unit tests for the detection logic and ESM integration tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPgSb3xj4vdi1mUNSzpVUD
MarshallOfSound
approved these changes
Jun 28, 2026
The no-asar ESM fixtures are copied straight into the bundle tree, so diverging via a loose plain `extra-file.txt` was classified as a unique PLAIN file and tripped makeUniversalApp's mach-o parity guard. Diverge via uniquely-named `.bin` files instead (classified as V8 snapshots and excluded from the parity check), exactly the way the existing non-ESM `should shim two different app folders` test does.
The heavy verifyApp integration tests run ~60s each on their own and the suite is contended under maxConcurrency, so 80s left too little margin and one test timed out at ~140s. Raise VERIFY_APP_TIMEOUT to 180s.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
All alerts resolved. Learn more about Socket for GitHub. This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored. |
georgexu99
approved these changes
Jul 1, 2026
Bump the aliased electron dev dependency used by the ESM fixtures from electron@^28.0.0 to the latest stable electron@^43.0.0 and rename the alias/constant accordingly (electron28 -> electron43, ELECTRON_28_VERSION -> ELECTRON_43_VERSION).
erickzhao
approved these changes
Jul 2, 2026
|
🎉 This PR is included in version 3.0.6 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Samuel Attard · Slack thread
Before / After
Before: building a universal binary from an Electron app whose main-process entrypoint is ESM (a
.mjsentry, or"type": "module") crashed at launch withERR_REQUIRE_ESMwhenever the x64 and arm64 ASARs diverged. The generated outerapp.asarshim doesrequire()of the per-arch ASAR, andrequire()can't load an ESM entrypoint. (#90)After: when both arches' entrypoints are ESM, the shim is emitted as an ESM module that uses dynamic
import()instead, so ESM apps launch correctly. CommonJS apps are unchanged.Safety guard
We only ship the ESM shim when we've positively confirmed the entrypoint's module format. Format is read from each ASAR's own
package.jsonusing Node's rules (.mjs/.cjsextension wins; otherwise the"type"field decides), and anything we can't positively identify as ESM is treated as CommonJS — so we never switch on a guess. Both arches must agree: if one arch is ESM and the other CommonJS, we throw a clear error rather than ship a binary that would crash on one of them.How
src/asar-utils.ts: newdetectEntrypointModule(packageJson)andresolveShimModule(x64, arm64)helpers.src/index.ts: both shim copy sites now read each arch'spackage.json(NO_ASAR from disk, HAS_ASAR viaasar.extractFile), resolve the module, and on a confirmed ESM verdict copyentry-asar/esm/{has,no}-asar.mjs→index.mjsand setpj.main = 'index.mjs'; otherwise the existing.js/index.jspath is unchanged.entry-asar/esm/usecreateRequire+ top-levelawait import(pathToFileURL(process._archPath).href), compiled by a dedicatedentry-asar/esm/tsconfig.json(nodenext/es2022) added as a thirdtscbuild pass.Tests
test/detect-entrypoint.spec.ts: 16 unit tests covering the detection matrix and the divergence guard (written first, TDD).test/index.spec.ts(HAS_ASAR + NO_ASAR ESM shim) with ESM fixtures generated intest/globalSetup.ts. These exercise on macOS CI (require lipo/clang/arch).Closes #90.
Generated by Claude Code