Skip to content

Commit 615c96b

Browse files
committed
Harden web helper loading and add regression tests
- webUtils.web.ts: load the react-native-web helpers in a single top-level try/catch and fall back to the dist/cjs build on any require() failure, so a partial load is repaired; collapse the duplicated preprocess require. Kept as a plain try/catch (no top-level if / IIFE) so the module stays tree-shakable. - index.ts: presence test in the _touchableNode branch so a real falsy update (0 / '' / false) is applied instead of silently dropped. - Add web regression tests for _updatePropsJS (no throw on a prop-less ref, #9854) and web helper loading plus the dist/cjs fallback specifiers (#9844).
1 parent 61ed84f commit 615c96b

3 files changed

Lines changed: 79 additions & 43 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
3+
import { _updatePropsJS } from '../index';
4+
import * as webUtils from '../webUtils';
5+
6+
describe('web style helpers (#9844)', () => {
7+
test('react-native-web style helpers are loaded on web', () => {
8+
expect(typeof webUtils.createReactDOMStyle).toBe('function');
9+
expect(typeof webUtils.createTransformValue).toBe('function');
10+
expect(typeof webUtils.createTextShadowValue).toBe('function');
11+
});
12+
13+
test('the dist/cjs import() fallback specifiers resolve', () => {
14+
const fallbackSpecifiers = [
15+
'react-native-web/dist/cjs/exports/StyleSheet/compiler/createReactDOMStyle.js',
16+
'react-native-web/dist/cjs/exports/StyleSheet/preprocess.js',
17+
];
18+
for (const specifier of fallbackSpecifiers) {
19+
expect(() => require.resolve(specifier)).not.toThrow();
20+
}
21+
});
22+
});
23+
24+
describe('_updatePropsJS (#9854)', () => {
25+
test('does not throw on a ref without setNativeProps, style or props', () => {
26+
// Shape of a react-native-gifted-chat useAnimatedKeyboard ref on web.
27+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
28+
expect(() => _updatePropsJS({ opacity: 1 }, {} as any)).not.toThrow();
29+
});
30+
31+
test('applies animated styles to a DOM element', () => {
32+
const element = document.createElement('div');
33+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
34+
_updatePropsJS({ opacity: 0.5 } as any, element as any);
35+
expect(element.style.opacity).toBe('0.5');
36+
});
37+
38+
test('applies a falsy update (0) via _touchableNode instead of dropping it', () => {
39+
const setAttribute = jest.fn();
40+
const component = {
41+
props: { opacity: 1 },
42+
_touchableNode: { setAttribute },
43+
};
44+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
45+
_updatePropsJS({ opacity: 0 } as any, component as any);
46+
expect(setAttribute).toHaveBeenCalledWith('opacity', 0);
47+
});
48+
});

packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ export const _updatePropsJS = (
6969
updatePropsDOM(component, rawStyles, isAnimatedProps);
7070
} else if (component.props && Object.keys(component.props).length > 0) {
7171
Object.keys(component.props).forEach((key) => {
72-
if (!rawStyles[key]) {
72+
// Presence test, not truthiness - a real update of 0 / '' / false is
73+
// still an update and must be applied.
74+
if (!(key in rawStyles)) {
7375
return;
7476
}
7577
const dashedKey = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());

packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/webUtils.web.ts

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,54 +10,40 @@ export let createTransformValue: (transform: any) => any;
1010
export let createTextShadowValue: (style: any) => void | string;
1111

1212
// react-native-web exposes these style helpers only via internal modules. Metro
13-
// has require() so we load them synchronously (unchanged); strict-ESM bundlers
14-
// and SSR don't, which left them undefined and crashed _updatePropsJS (#9844), so
15-
// there we import() the dist/cjs build instead (dist/exports' extensionless
16-
// imports fail Node's ESM resolver). If neither resolves, the helpers stay
17-
// undefined and the DOM branch is skipped, as on native.
18-
19-
let loadedSynchronously = false;
20-
21-
if (typeof require === 'function') {
22-
try {
23-
createReactDOMStyle =
24-
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, n/no-unpublished-require
25-
require('react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle').default;
26-
loadedSynchronously = createReactDOMStyle !== undefined;
27-
// eslint-disable-next-line no-empty
28-
} catch (_e) {}
29-
30-
try {
31-
// React Native Web 0.19+
32-
createTransformValue =
33-
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, n/no-unpublished-require
34-
require('react-native-web/dist/exports/StyleSheet/preprocess').createTransformValue;
35-
// eslint-disable-next-line no-empty
36-
} catch (_e) {}
37-
38-
try {
39-
createTextShadowValue =
40-
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, n/no-unpublished-require
41-
require('react-native-web/dist/exports/StyleSheet/preprocess').createTextShadowValue;
42-
// eslint-disable-next-line no-empty
43-
} catch (_e) {}
44-
}
45-
46-
if (!loadedSynchronously) {
47-
// The cjs export may arrive as `.default` (bundler/Node interop) or as the
48-
// namespace itself; handle both.
49-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50-
const interopDefault = (moduleExports: any) =>
51-
moduleExports?.default ?? moduleExports;
52-
13+
// has require() so we load them synchronously; strict-ESM bundlers and SSR don't,
14+
// so require() throws and we fall back to import()ing the dist/cjs build
15+
// (dist/exports' extensionless imports fail Node's ESM resolver). If neither
16+
// resolves, the helpers stay undefined and the DOM branch is skipped, as on native.
17+
//
18+
// Kept as a single top-level try/catch (no top-level `if` or IIFE) so the module
19+
// stays tree-shakable - see the is-tree-shakable check.
20+
try {
21+
createReactDOMStyle =
22+
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, n/no-unpublished-require
23+
require('react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle').default;
24+
// React Native Web 0.19+
25+
const preprocess =
26+
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, n/no-unpublished-require
27+
require('react-native-web/dist/exports/StyleSheet/preprocess');
28+
createTransformValue = preprocess.createTransformValue;
29+
createTextShadowValue = preprocess.createTextShadowValue;
30+
} catch (_e) {
31+
// No require() (strict ESM / SSR), or a helper module was missing: load the
32+
// helpers from the dist/cjs build instead.
5333
void (async () => {
34+
// The cjs export may arrive as `.default` (bundler/Node interop) or as the
35+
// namespace itself; handle both.
36+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
37+
const interopDefault = (moduleExports: any) =>
38+
moduleExports?.default ?? moduleExports;
39+
5440
try {
5541
const styleModule = await import(
5642
// eslint-disable-next-line n/no-unpublished-import
5743
'react-native-web/dist/cjs/exports/StyleSheet/compiler/createReactDOMStyle.js'
5844
);
5945
createReactDOMStyle = interopDefault(styleModule);
60-
} catch (_e) {
46+
} catch (_e2) {
6147
// Unresolved: the DOM update branch stays disabled, as on native.
6248
}
6349

@@ -70,7 +56,7 @@ if (!loadedSynchronously) {
7056
const preprocess = interopDefault(preprocessModule);
7157
createTransformValue = preprocess.createTransformValue;
7258
createTextShadowValue = preprocess.createTextShadowValue;
73-
} catch (_e) {
59+
} catch (_e2) {
7460
// Optional: leaving them undefined just skips transform/textShadow.
7561
}
7662
})();

0 commit comments

Comments
 (0)