Skip to content

Commit 6377bb8

Browse files
ramonclaudiokitten
andcommitted
Babel preset: Add Hermes V1 native runtime workarounds for hermes#1761
## Summary: Three Babel plugins added to `@react-native/babel-preset` that work around runtime codegen bugs in the bundled Hermes V1 (250829098.0.13, branch cut 2025-08-29) by rewriting source patterns before they reach the engine. Each plugin papers over a different bug that is fixed on Hermes `static_h` but has not been backported to the V1 stable branch (verified absent through the .0.15 stable tip; the only in-flight graft, facebook/hermes#2030, is still open). - `fix-hermes-v1-async-arrow-non-simple-params`: facebook/hermes#1761 (fixed in static_h by 68bfb3a48b31, 2025-09-11). Async arrow functions with destructured, defaulted, or rest parameters silently resolve `await` with `undefined` while the function body continues executing in the background. The plugin rewrites the params to a simple identifier with inline destructuring so Hermes never sees the buggy shape. Gated on `isHermesProfile && preserveAsync` since `plugin-transform-async-to-generator` rewrites the pattern away when `preserveAsync` is false. - `fix-hermes-v1-class-in-finally`: Fixed in static_h by 1e94fbe0ebb4 (2026-02-12). Class declarations inside a `finally` block trip Hermes V1's variable caching path. The plugin wraps them in an IIFE so the class lives in its own scope, and preserves the inferred name of a class expression bound to a plain identifier. - `fix-hermes-v1-super-in-object-accessor`: Fixed in static_h by 18a963465944 (2025-11-04). Object-literal getters and setters using `super.x` trip the `genFunctionExpression` home object path, which segfaults hermesc at compile time. The plugin marks the accessor as computed with a string key, covering identifier, string, and numeric keys. All three gate on `isHermesProfile` and bail out fast on the common case. The plugins are a direct port of work by @kitten (Phil Pluckthun, Expo team) in `babel-preset-expo` (expo/expo#45601, MIT licensed). All plugin design and implementation credit belongs to him. This PR carries his work over to `@react-native/babel-preset` so bare React Native consumers benefit from the same fix. ## Changelog: [GENERAL] [FIXED] - Work around three Hermes V1 source-level codegen bugs in `@react-native/babel-preset` (async-arrow non-simple params, class-in-finally, super-in-object-accessor) ## Test Plan: - 21 inline-snapshot test cases across three new test files verify the transformed output, the fast-bail behavior on irrelevant patterns, and the numeric-key and class-name edge cases. - `yarn jest packages/react-native-babel-preset` clean (76 tests, 27 snapshots). - Full repo `yarn test` clean (278 suites, 5692 tests passed, 1857 snapshots). - `yarn lint`, `yarn format-check`, `yarn flow-check`, `yarn test-typescript`, `yarn featureflags --verify-unchanged`, `yarn build-types --validate` all clean. - Existing `transform-snapshot-test` passes byte-identical: the kitchen-sink fixture has none of the buggy patterns so the new plugins do not change its output for any profile. - Compiled the bug patterns through the bundled compiler (hermes-compiler 250829098.0.14): `get x() { return super.x }` and `get 0() { return super.x }` both segfault hermesc, while the transformed computed-key output compiles clean. - Device repro at ramonclaudio/hermes-1761-repro: 55/55 PASS on iPhone 17 Pro / iOS 26.5 in Debug and Release. ## Caveats: `fix-hermes-v1-class-in-finally` rewrites block-scoped `class` declarations inside `finally` blocks to function-scoped `var` initializers, matching the babel-preset-expo source. Observable only when code in the same `finally` references the class before its declaration or relies on its block scoping (rare). The same plugin shape has shipped in `babel-preset-expo` and Expo SDK 56 without reported regressions, but flagging in case Meta's internal test fleet surfaces an edge case the OSS suite does not catch. ## References: - facebook/hermes#1761 (root-cause issue, fixed in static_h) - facebook/hermes#2030 (in-flight graft of the async-arrow fix onto the V1 stable branch, still open) - expo/expo#45601 (source of the ported plugins, by @kitten) - expo/expo#45592 (user-facing bug report on SDK 56 preview) Co-authored-by: Phil Pluckthun <phil@kitten.sh>
1 parent aea8785 commit 6377bb8

8 files changed

Lines changed: 774 additions & 0 deletions
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @noflow
9+
*/
10+
11+
'use strict';
12+
13+
const {transform} = require('../__mocks__/test-helpers');
14+
const fixHermesV1AsyncArrowNonSimpleParams = require('../fix-hermes-v1-async-arrow-non-simple-params');
15+
16+
test('rewrites destructured object param with default to simple identifier', () => {
17+
const code = `
18+
const fn = async ({a = 1, b} = {}) => {
19+
return await fetch(a + b);
20+
};
21+
`;
22+
23+
expect(transform(code, [fixHermesV1AsyncArrowNonSimpleParams]))
24+
.toMatchInlineSnapshot(`
25+
"const fn = async _p => {
26+
var {
27+
a = 1,
28+
b
29+
} = _p === undefined ? {} : _p;
30+
return await fetch(a + b);
31+
};"
32+
`);
33+
});
34+
35+
test('rewrites destructured array param to simple identifier', () => {
36+
const code = `
37+
const fn = async ([a, b]) => await Promise.resolve(a + b);
38+
`;
39+
40+
expect(transform(code, [fixHermesV1AsyncArrowNonSimpleParams]))
41+
.toMatchInlineSnapshot(`
42+
"const fn = async _p => {
43+
var [a, b] = _p;
44+
return await Promise.resolve(a + b);
45+
};"
46+
`);
47+
});
48+
49+
test('rewrites assignment-pattern param without enclosing destructure', () => {
50+
const code = `
51+
const fn = async (x = 5) => await use(x);
52+
`;
53+
54+
expect(transform(code, [fixHermesV1AsyncArrowNonSimpleParams]))
55+
.toMatchInlineSnapshot(`
56+
"const fn = async _p => {
57+
var x = _p === undefined ? 5 : _p;
58+
return await use(x);
59+
};"
60+
`);
61+
});
62+
63+
test('wraps body in inner async arrow when rest param is present', () => {
64+
const code = `
65+
const fn = async (...args) => await handle(args);
66+
`;
67+
68+
expect(transform(code, [fixHermesV1AsyncArrowNonSimpleParams]))
69+
.toMatchInlineSnapshot(`
70+
"const fn = (...args) => (async () => {
71+
return await handle(args);
72+
})();"
73+
`);
74+
});
75+
76+
test('leaves async arrow with only simple identifier params alone', () => {
77+
const code = `
78+
const fn = async (a, b) => await fetch(a + b);
79+
`;
80+
81+
expect(
82+
transform(code, [fixHermesV1AsyncArrowNonSimpleParams]),
83+
).toMatchInlineSnapshot(`"const fn = async (a, b) => await fetch(a + b);"`);
84+
});
85+
86+
test('leaves non-async arrow alone', () => {
87+
const code = `
88+
const fn = ({a = 1, b} = {}) => a + b;
89+
`;
90+
91+
expect(transform(code, [fixHermesV1AsyncArrowNonSimpleParams]))
92+
.toMatchInlineSnapshot(`
93+
"const fn = ({
94+
a = 1,
95+
b
96+
} = {}) => a + b;"
97+
`);
98+
});
99+
100+
test('handles multiple params mixing simple and complex', () => {
101+
const code = `
102+
const fn = async (a, {b}, c = 1) => await all(a, b, c);
103+
`;
104+
105+
expect(transform(code, [fixHermesV1AsyncArrowNonSimpleParams]))
106+
.toMatchInlineSnapshot(`
107+
"const fn = async (a, _p, _p2) => {
108+
var {
109+
b
110+
} = _p;
111+
var c = _p2 === undefined ? 1 : _p2;
112+
return await all(a, b, c);
113+
};"
114+
`);
115+
});
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @noflow
9+
*/
10+
11+
'use strict';
12+
13+
const {transform} = require('../__mocks__/test-helpers');
14+
const fixHermesV1ClassInFinally = require('../fix-hermes-v1-class-in-finally');
15+
16+
test('wraps class declaration in finally block in IIFE', () => {
17+
const code = `
18+
function run() {
19+
try {
20+
risky();
21+
} finally {
22+
class Logger {
23+
log() { console.log('done'); }
24+
}
25+
new Logger().log();
26+
}
27+
}
28+
`;
29+
30+
expect(transform(code, [fixHermesV1ClassInFinally])).toMatchInlineSnapshot(`
31+
"function run() {
32+
try {
33+
risky();
34+
} finally {
35+
var Logger = (() => {
36+
class Logger {
37+
log() {
38+
console.log('done');
39+
}
40+
}
41+
return Logger;
42+
})();
43+
new Logger().log();
44+
}
45+
}"
46+
`);
47+
});
48+
49+
test('wraps class expression in finally block in IIFE', () => {
50+
const code = `
51+
function run() {
52+
try {
53+
risky();
54+
} finally {
55+
const Logger = class {
56+
log() {}
57+
};
58+
new Logger().log();
59+
}
60+
}
61+
`;
62+
63+
expect(transform(code, [fixHermesV1ClassInFinally])).toMatchInlineSnapshot(`
64+
"function run() {
65+
try {
66+
risky();
67+
} finally {
68+
const Logger = (() => class Logger {
69+
log() {}
70+
})();
71+
new Logger().log();
72+
}
73+
}"
74+
`);
75+
});
76+
77+
test('leaves class outside finally block alone', () => {
78+
const code = `
79+
function run() {
80+
try {
81+
class Inside {}
82+
return new Inside();
83+
} catch (e) {
84+
class Caught {}
85+
return new Caught();
86+
}
87+
}
88+
`;
89+
90+
expect(transform(code, [fixHermesV1ClassInFinally])).toMatchInlineSnapshot(`
91+
"function run() {
92+
try {
93+
class Inside {}
94+
return new Inside();
95+
} catch (e) {
96+
class Caught {}
97+
return new Caught();
98+
}
99+
}"
100+
`);
101+
});
102+
103+
test('leaves class declared at module scope alone', () => {
104+
const code = `
105+
class Module {}
106+
new Module();
107+
`;
108+
109+
expect(transform(code, [fixHermesV1ClassInFinally])).toMatchInlineSnapshot(`
110+
"class Module {}
111+
new Module();"
112+
`);
113+
});
114+
115+
test('does not enter nested function scope', () => {
116+
const code = `
117+
try {} finally {
118+
function inner() {
119+
class NestedFn {}
120+
return new NestedFn();
121+
}
122+
inner();
123+
}
124+
`;
125+
126+
expect(transform(code, [fixHermesV1ClassInFinally])).toMatchInlineSnapshot(`
127+
"try {} finally {
128+
function inner() {
129+
class NestedFn {}
130+
return new NestedFn();
131+
}
132+
inner();
133+
}"
134+
`);
135+
});
136+
137+
test('preserves inferred name when wrapping a named-binding class expression', () => {
138+
const code = `
139+
function run() {
140+
try {} finally {
141+
const Service = class {
142+
ping() {}
143+
};
144+
return new Service();
145+
}
146+
}
147+
`;
148+
149+
expect(transform(code, [fixHermesV1ClassInFinally])).toMatchInlineSnapshot(`
150+
"function run() {
151+
try {} finally {
152+
const Service = (() => class Service {
153+
ping() {}
154+
})();
155+
return new Service();
156+
}
157+
}"
158+
`);
159+
});
160+
161+
test('leaves an unbound class expression in finally anonymous', () => {
162+
const code = `
163+
function run() {
164+
try {} finally {
165+
return register(class {
166+
run() {}
167+
});
168+
}
169+
}
170+
`;
171+
172+
expect(transform(code, [fixHermesV1ClassInFinally])).toMatchInlineSnapshot(`
173+
"function run() {
174+
try {} finally {
175+
return register((() => class {
176+
run() {}
177+
})());
178+
}
179+
}"
180+
`);
181+
});

0 commit comments

Comments
 (0)