Repository navigation
Conversation
The default lookup joined `<reactNativePath>/../hermes-compiler` lexically. Under pnpm `node_modules/react-native` is a symlink into `.pnpm`, where `hermes-compiler` is installed next to the real react-native, so the join missed it and the plugin fell back to `sdks/hermesc`, which RN 0.82+ no longer ships (ENOENT when spawning hermesc). The existing two locations are still checked first, in the same order, and resolve to the same path. Only when neither exists does the lookup try `hermes-compiler` next to the symlink-resolved react-native, then the react-native `node_modules` hierarchy, then react-native resolved from the compiler context when the default `reactNativePath` doesn't exist. If nothing is found it throws an error listing every checked path. The lookup runs lazily on the first transformed asset and is cached, so builds that register the plugin but emit no matching assets are unaffected.
`bundle` awaited its async error handler inside the compiler's `close` callback, so a compiler error (e.g. a failed hermesc run in HermesBytecodePlugin) became an unhandled rejection and the command promise never settled. LoggerPlugin's `uncaughtException` listener then logged it and, because registering the listener disables Node's default exit code, the process exited 0 with un-compiled chunks on disk. `bundle` now rejects with the handler's error in run and watch mode, and LoggerPlugin sets `process.exitCode = 1` for uncaught exceptions.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 47e35c7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
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 |
This branch was successfully deployed
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.
Summary
Fixes two
HermesBytecodePluginbugs. Both showed up when validating5.4.3-canary-20261104085523in a pnpm monorepo (super-app-showcase: pnpm 11, RN 0.88.1, Rspack 2.3.1, Node 26). Neither comes from #1516: the hermesc lookup is byte-identical in 5.4.2.Important
Behaviour change: a failed Hermes bytecode compilation now fails the build instead of exiting 0. Builds where hermesc fails used to pass and ship un-compiled chunks; they now exit non-zero. Both changesets are
patch. Maintainers may prefer to ship this in a minor release.Bug 1: hermesc lookup under pnpm
Root cause
reactNativePathdefaults to<context>/node_modules/react-native.getHermesCLIPathtriedpath.join(reactNativePath, '..', 'hermes-compiler', …), then fell back to<reactNativePath>/sdks/hermesc/…without checking that it exists.Under pnpm,
<context>/node_modules/react-nativeis a symlink intonode_modules/.pnpm/react-native@…/node_modules/react-native.hermes-compileris a dependency ofreact-nativeand is installed next to the real package inside.pnpm/…/node_modules. The lexical..join doesn't follow the symlink, so it checked<context>/node_modules/hermes-compilerand missed. It then returnedsdks/hermesc, which RN 0.82+ no longer ships:Fix (backwards compatible)
Lookup order:
hermesCLIPath, if set, still wins and no lookup runs.<reactNativePath>/../hermes-compiler/hermesc/<os>/hermesc, then<reactNativePath>/sdks/hermesc/<os>/hermesc. The first check is the samefs.existsSync. The legacy path is now only returned if it exists (on Windows,hermescorhermesc.exe), and it is returned as the same extensionless string as before.hermes-compilernext tofs.realpathSync(reactNativePath). This is the pnpm case.hermes-compilerfound by walkingnode_modulesup from the real and the lexicalreact-nativedirs. This covers npm/yarn nesting (react-native/node_modules/hermes-compilerwhen versions conflict) and hoisting further up.reactNativePathwas not set and<context>/node_modules/react-nativeis not a directory: the same candidates forreact-nativeresolved fromcompiler.context. This fallback is used only for the hermesc lookup.hermesc.exe. Before, it never matched there, because the binary ishermesc.exe.hermesCLIPath/reactNativePath. It no longer spawns a missing binary.Other properties:
apply()and runs on the firstafterEmitthat actually has matching assets. The result is cached. Projects that register the plugin but emit no matching assets, or that disable it for a mode, can't start failing because of the new error.reactNativePathdefault untouched. It is still used as-is to findscripts/compose-source-maps.js. Symlinks are only resolved inside the hermesc lookup, so re-resolving can't pick a different RN copy for source-map composition in monorepos.require.resolve(…, { paths })? Even withpaths, Node's resolution still consultsNODE_PATH/global folders, and pnpm's bin shims setNODE_PATHtonode_modules/.pnpm/node_modules. That can return an arbitraryhermes-compilerorreact-nativeversion. A mismatched hermesc emits bytecode the app's Hermes runtime can't load, which is worse than a build error. The lookup therefore walksnode_modulesdirectories explicitly: the same algorithm, minus global folders. Jest's resolver also showed this leak in tests.<context>/node_modules/react-nativedoesn't exist. Resolving RN fromcompiler.contextlets hermesc be found there. I kept it to the hermesc lookup, as asked, so thatreactNativePathsemantics forcompose-source-maps.jsdon't change. For such projects, source-map composition still needsreactNativePathset explicitly; that is unchanged from today. If maintainers want it, a follow-up could apply the same fallback tocompose-source-maps.js.ChunksToHermesBytecodePluginextendsHermesBytecodePlugin, so it gets the fix automatically. No other code in the repo uses thereactNativePath/..pattern.Bug 2: a failed Hermes transform exits 0
Root cause
commands/bundle.tscalledcompiler.run(cb), andcbcalledcompiler.close(async (closeErr) => { await errorHandler(error, stats); resolve(); }).errorHandleris async and throwsCLIErrorfor compiler errors. Errors fromafterEmitreachrun's callback aserrorin both Rspack and webpack. The throw rejected theclosecallback's promise, which nobody observed, so the outernew Promise((resolve) => …)never settled and the rejection was unhandled.--watchpassed the asyncerrorHandlerstraight tocompiler.watch, with the same problem.LoggerPluginregistersprocess.on('uncaughtException', …). Node routes the unhandled rejection there, and having a listener disables Node's default crash with exit code 1. The listener only logged and flushed, so the process exited 0 once the event loop drained.Fix
bundle: the run/watch wiring moved into an exportedrunCompilerhelper, which also serves as the test seam. The promise now settles with the handler's result:onResult(error, stats).then(resolve, reject)forrun, and.catch(reject)forwatch. The CLI'shandleErrorthen exits 1, consistent with the existingprocess.exit(2)forstats.hasErrors(). In--watch, a fatal compiler error now ends the command. Before, it was logged and watching continued. That matches how stats errors already behave there (process.exit(2)).LoggerPlugin: theuncaughtExceptionlistener setsprocess.exitCode = 1. It does not callprocess.exitsynchronously, so the reporter still flushes.Not done: reporting hermesc failures as compilation errors
I skipped pushing a
WebpackErrorintocompilation.errorsfromafterEmit. With the two fixes above, a thrown error inafterEmitalready reachesrun's callback aserrorin both bundlers and exits non-zero. I haven't verified that Rspack'sStats.hasErrors()picks up errors pushed after emit. That change would also move this failure from exit 1 to exit 2 and change its formatting. It can be a follow-up if wanted.Tests
plugins/__tests__/getHermesCLIPath.test.ts(new). It builds real temp-dir layouts, using junction symlinks so it also works on the Windows CI job, and covers:sdks/hermescfallback: unchanged, and it wins over the new candidatesreact-native/node_modules/hermes-compiler: resolved candidatereactNativePathdoesn't exist, and that it's not used whenreactNativePathexistshermesCLIPath.exehandling (legacy path still returned extensionless)plugins/__tests__/HermesBytecodePlugin.test.ts:hermesCLIPathoverride skips the lookupgetHermesCLIPathcases moved to the new file.commands/__tests__/bundle.test.ts(new):runCompilerresolves on success, rejects on compiler error in run and watch mode, and resolves when watching stops.plugins/__tests__/LoggerPlugin.test.ts(new): an uncaught exception setsprocess.exitCode = 1.pnpm lint:ci,pnpm typecheckandpnpm testall pass locally (@callstack/repack: 45 suites, 397 tests).End-to-end verification
The checks ran in super-app-showcase
packages/trading(branchchore/rn-0.88-repack-canary-skia-rename), with an untracked probe config that addsHermesBytecodePlugin({ enabled: env.mode === 'production', test: /\.(js)?bundle$/, exclude: /index\.bundle$/ })and noreactNativePath:5.4.3-canary-20261104085523ENOENT …/react-native/sdks/hermesc/osx-bin/hermesc, chunks left as JS, exit 0c6 1f bc 03 c1 03 19 1f, and all 66.mapfiles containx_hermes_function_offsetshermesCLIPath: '/nonexistent/hermesc'With this branch, the default lookup resolved to
node_modules/.pnpm/react-native@0.88.1_…/node_modules/hermes-compiler/hermesc/osx-bin/hermesc.This branch's
distwent into a copy of the installed package, placed next to it inside the.pnpmentry so its dependencies resolve.packages/trading's symlink pointed at that copy for the test, then was restored and the copy deleted. The pnpm store and the showcase repo were not modified:git statusis clean, andpnpm installreports up to date. Only iOS was exercised end-to-end. The webpack path is covered by reading the code (anafterEmitrejection is passed torun's callback) and by unit tests, not by an end-to-end webpack build.