fix(metro): preserve upstream transformer initialization failures#690
Conversation
Split module resolution from execution in the upstream loader so a candidate that resolves but throws during initialization (top-level throw, missing peer/transitive dependency, or runtime-ABI rejection) is no longer masked as absence. `require.resolve` failures (MODULE_NOT_FOUND) prove the requested candidate itself is absent and stay non-fatal during auto-detection; any error from executing the module body is preserved with its original message and stack via the Error `cause`. Explicit configured failures surface the original diagnostic instead of the generic "could not load" message, and auto-detection no longer falls through to a mismatched candidate when an earlier one is broken. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HYCpGEc14zxD99ZPPzAnpz
Self-Review round 1Verified the committed implementation against the RA-15 acceptance criteria and traced the consequence surface of splitting Finding (correctness regression introduced by the split). This is a regression versus the old swallow-everything loader and violates the issue's criterion that genuine absence stays non-fatal during automatic probing. A resolution-time error can only concern the requested specifier (resolve never executes the module body), and Remediation plan.
Applying now. |
The first auto-detection candidate, @expo/metro-config/babel-transformer, is a package subpath. When @expo/metro-config is installed but its babel-transformer subpath is not exported (Expo/React Native version skew), Node's require.resolve throws ERR_PACKAGE_PATH_NOT_EXPORTED, not MODULE_NOT_FOUND. The prior split classified only MODULE_NOT_FOUND / ERR_MODULE_NOT_FOUND as absence, so such a candidate hard-failed auto-detection instead of falling through to the next transformer. Broaden the absence classifier (isModuleNotFound -> isCandidateAbsent) to also treat ERR_PACKAGE_PATH_NOT_EXPORTED as absence. A resolution-time error can only concern the requested specifier (resolve never executes the module body), and this code means the requested entry point is unavailable here -- genuine absence, kept non-fatal during probing. Execution-time failures (top-level throw, missing transitive dependency) are untouched and still surface with cause. Adds a production-path regression driving the real loader with an installed package whose requested subpath is not exported. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HYCpGEc14zxD99ZPPzAnpz
Self-Review round 2 — cleanRe-inspected the full changed surface after the round-1 fix; no sound improvement or new issue found.
Self-review complete at 2 rounds (1 finding remediated, 1 clean pass). |
Final verification — greenRA-15 (#685) implemented and self-reviewed (2 rounds). Local, package-scoped verification on HEAD The
Per the campaign HARD RULES I did not run whole-repo |
Squash-mergedBundle b8 (RA-15, #685) squash-merged into
|
Bundle b8 — Metro upstream transformer errors
This bundle hardens
@ttsc/metro's upstream Babel-transformer loader so a broken-but-installed transformer is no longer masked as absence.Issues
fix(metro): preserve upstream transformer initialization failuresProblem
resolveUpstreamTransformertreated every exception fromrequireing an upstream candidate as "module not present." An installed transformer that throws during top-level initialization, has a missing peer/transitive dependency, or rejects the current runtime ABI was silently discarded:upstreamTransformerfailure surfaced only the genericCould not load the configured upstream transformer, dropping the original message and stack;Fix
tryRequirenow splits resolution from execution.require.resolvenever runs third-party code, so aMODULE_NOT_FOUND/ERR_MODULE_NOT_FOUNDthere proves the requested candidate itself is genuinely absent — reported asundefinedso automatic probing continues to the next optional peer. Any error thrown while executing the (resolvable) module body is a real initialization failure and is preserved with its original message and stack as thecauseof a@ttsc/metrowrapper. A candidate's own missing transitive dependency surfaces that dependency, not a false "absent candidate."Tests
Production-loader regressions under
tests/test-metro/src/features/upstream, run withpnpm --filter @ttsc/test-metro start -- --include=upstream:cause;cause+ stack;Docs:
packages/metro/README.mddocuments the absence-vs-broken distinction.