react-native-impeller-platform 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +165 -2
  2. package/package.json +52 -4
  3. package/react-native.config.js +24 -0
  4. package/src/bin/engine-path.js +12 -0
  5. package/src/cli/index.js +20 -0
  6. package/src/cli/init.js +112 -0
  7. package/src/cli/run.js +73 -0
  8. package/src/externaltexture/index.js +58 -0
  9. package/src/index.js +12 -0
  10. package/src/metro-shims/index.js +382 -0
  11. package/src/metro-shims/shims/Alert.impeller.js +89 -0
  12. package/src/metro-shims/shims/BaseViewConfig.impeller.js +81 -0
  13. package/src/metro-shims/shims/Button.impeller.js +100 -0
  14. package/src/metro-shims/shims/Image.impeller.js +97 -0
  15. package/src/metro-shims/shims/Platform.impeller-as-ios.js +44 -0
  16. package/src/metro-shims/shims/Platform.impeller.js +53 -0
  17. package/src/metro-shims/shims/ReactNativeFeatureFlags.impeller.js +40 -0
  18. package/src/metro-shims/shims/ReactNativeStyleAttributes.impeller.js +20 -0
  19. package/src/metro-shims/shims/TextInputState.impeller.js +100 -0
  20. package/src/metro-shims/shims/ViewConfigIgnore.impeller.js +55 -0
  21. package/src/metro-shims/shims/reanimated/clock.js +41 -0
  22. package/src/metro-shims/shims/reanimated/component.js +83 -0
  23. package/src/metro-shims/shims/reanimated/css.js +193 -0
  24. package/src/metro-shims/shims/reanimated/easing.js +47 -0
  25. package/src/metro-shims/shims/reanimated/hooks.js +191 -0
  26. package/src/metro-shims/shims/reanimated/interpolate.js +133 -0
  27. package/src/metro-shims/shims/reanimated/values.js +264 -0
  28. package/src/metro-shims/shims/reanimated.impeller.js +90 -0
  29. package/src/platformview/index.js +23 -0
  30. package/src/resolve.js +238 -0
  31. package/src/selftest.js +123 -0
  32. package/src/storage/index.js +161 -0
  33. package/src/viewshot/index.js +98 -0
  34. package/src/worldraster/index.js +75 -0
  35. package/templates/app/README.md.tmpl +18 -0
  36. package/templates/app/_gitignore +1 -0
  37. package/templates/app/impeller.config.json.tmpl +11 -0
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+ // <Image> for the `impeller` platform (A19 / SC-30; replaces the parity item
3
+ // 12 placeholder View).
4
+ //
5
+ // WHY a shim: Libraries/Image/Image.js is a self re-export (`import Image from
6
+ // './Image'`), so on impeller -- no .impeller.js sibling -- it resolves to
7
+ // ITSELF and its default export is undefined. Image.ios.js cannot be used
8
+ // either: it getEnforcing's the ImageLoader TurboModule at module load.
9
+ //
10
+ // This is Image.ios.js's render path: it mounts the RCTImageView host
11
+ // component (Fabric "Image", painted by the engine) with `source` as an array,
12
+ // resizeMode (objectFit wins, default 'cover') and tintColor (prop or style).
13
+ // onLoadStart/onLoad/onError/onLoadEnd and defaultSource ride the native
14
+ // component (the engine fires them, SC-31). The statics are Image.ios.js's,
15
+ // over the engine's "ImageLoader" module.
16
+ const React = require('react');
17
+ const interop = (m) => (m && m.default) || m;
18
+ const flattenStyle = interop(require('react-native/Libraries/StyleSheet/flattenStyle'));
19
+ const StyleSheet = interop(require('react-native/Libraries/StyleSheet/StyleSheet'));
20
+ const ImageViewNativeComponent = interop(require('react-native/Libraries/Image/ImageViewNativeComponent'));
21
+ const {getImageSourcesFromImageProps} = require('react-native/Libraries/Image/ImageSourceUtils');
22
+ const {convertObjectFitToResizeMode} = require('react-native/Libraries/Image/ImageUtils');
23
+
24
+ const styles = StyleSheet.create({base: {overflow: 'hidden'}});
25
+
26
+ function Image({ref, ...props}) {
27
+ const source = getImageSourcesFromImageProps(props) || {
28
+ uri: undefined,
29
+ width: undefined,
30
+ height: undefined,
31
+ };
32
+ let style;
33
+ let sources;
34
+ if (Array.isArray(source)) {
35
+ style = [styles.base, props.style];
36
+ sources = source;
37
+ } else {
38
+ const width = source.width ?? props.width;
39
+ const height = source.height ?? props.height;
40
+ style = [{width, height}, styles.base, props.style];
41
+ sources = [source];
42
+ }
43
+ const flat = flattenStyle(style);
44
+ const objectFit = convertObjectFitToResizeMode(flat?.objectFit);
45
+ const resizeMode = objectFit || props.resizeMode || flat?.resizeMode || 'cover';
46
+ const tintColor = props.tintColor ?? flat?.tintColor;
47
+ if (props.children != null) {
48
+ throw new Error(
49
+ 'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.',
50
+ );
51
+ }
52
+ const {src, 'aria-hidden': ariaHidden, ...rest} = props;
53
+ const accessible = ariaHidden !== true && (props.alt !== undefined ? true : props.accessible);
54
+ return React.createElement(ImageViewNativeComponent, {
55
+ ...rest,
56
+ ref,
57
+ accessible,
58
+ accessibilityLabel: props['aria-label'] ?? props.accessibilityLabel ?? props.alt,
59
+ style,
60
+ resizeMode,
61
+ tintColor,
62
+ source: sources,
63
+ });
64
+ }
65
+ Image.displayName = 'Image';
66
+
67
+ const TurboModuleRegistry = require('react-native/Libraries/TurboModule/TurboModuleRegistry');
68
+ const loader = () => {
69
+ const m = TurboModuleRegistry.get('ImageLoader');
70
+ if (m == null) throw new Error('ImageLoader native module is not available');
71
+ return m;
72
+ };
73
+ const settle = (promise, uri, success, failure) => {
74
+ if (typeof success !== 'function') return promise;
75
+ promise
76
+ .then((sizes) => success(sizes.width, sizes.height))
77
+ .catch(failure || (() => console.warn('Failed to get size for image: ' + uri)));
78
+ return undefined;
79
+ };
80
+ Image.getSize = (uri, success, failure) =>
81
+ settle(
82
+ loader().getSize(uri).then(([width, height]) => ({width, height})),
83
+ uri,
84
+ success,
85
+ failure,
86
+ );
87
+ Image.getSizeWithHeaders = (uri, headers, success, failure) =>
88
+ settle(loader().getSizeWithHeaders(uri, headers), uri, success, failure);
89
+ Image.prefetch = (url) => loader().prefetchImage(url);
90
+ Image.prefetchWithMetadata = (url, queryRootName, rootTag) =>
91
+ loader().prefetchImageWithMetadata(url, queryRootName, rootTag || 0);
92
+ Image.queryCache = async (urls) => loader().queryCache(urls);
93
+ Image.resolveAssetSource = (source) =>
94
+ require('react-native/Libraries/Image/resolveAssetSource').default(source);
95
+
96
+ module.exports = Image;
97
+ module.exports.default = Image;
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+ // ENG-01a. Platform variant handed ONLY to react-native files that branch
3
+ // `if (ios) ... else if (android) ...` with no else, and whose iOS arm is the
4
+ // desktop-shaped one.
5
+ //
6
+ // WHY THIS FILE EXISTS AT ALL
7
+ // ---------------------------
8
+ // TextInput.js does not fail. It BRANCHES ITSELF OUT OF EXISTENCE, exactly the
9
+ // way Alert.js did (see Alert.impeller.js). Read the real file:
10
+ //
11
+ // line 555 let textInput = null;
12
+ // line 677 if (Platform.OS === 'ios') { textInput = <RCTSingleline.../> }
13
+ // line 727 else if (Platform.OS === 'android') { textInput = <AndroidTextInput/> }
14
+ // (no else)
15
+ // line 802 return <TextAncestorContext value={true}>{textInput}</TextAncestorContext>
16
+ //
17
+ // On `impeller` neither arm runs, so a perfectly good <TextInput> renders
18
+ // `null` inside a context provider. React mounts nothing, Fabric is never
19
+ // asked for a host component, and the log is clean. Registering a
20
+ // TextInput ComponentDescriptor in FabricHost.cpp would have changed NOTHING,
21
+ // because the JS never reaches C++ at all. That was the whole ENG-01 dispute.
22
+ //
23
+ // WHY NOT JUST SET Platform.OS = 'ios'
24
+ // ------------------------------------
25
+ // Because it is a lie the whole bundle would read. Platform.OS === 'impeller'
26
+ // is what selects our shims, our .impeller.js extensions, and what app code
27
+ // branches on. Flipping it globally to 'ios' would unpick every one of those.
28
+ // So the lie is scoped: this module is served ONLY to the specific RN files
29
+ // named in IOS_BRANCHED, by resolved-origin path, and every other importer of
30
+ // Platform still gets the honest 'impeller'.
31
+ //
32
+ // Everything except OS is delegated to the real shim, so constants/Version/
33
+ // select/isTesting cannot drift between the two copies.
34
+ const Platform = require('./Platform.impeller.js');
35
+
36
+ const PlatformAsIos = Object.create(Platform);
37
+ Object.defineProperty(PlatformAsIos, 'OS', {value: 'ios', enumerable: true});
38
+ // `select` must keep honouring 'impeller' first — an app that wrote
39
+ // Platform.select({impeller: x, ios: y}) means x, and this file is not a
40
+ // licence to hand it y. Only the OS string is forged.
41
+ PlatformAsIos.select = Platform.select.bind(Platform);
42
+
43
+ module.exports = PlatformAsIos;
44
+ module.exports.default = PlatformAsIos;
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+ // Platform shim for Platform.OS === 'impeller'.
3
+ //
4
+ // The literal below is the PACKAGING MARKER. tools/pack-and-consume.sh greps the
5
+ // produced Metro bundle for it: if the marker is absent, the shim never made it
6
+ // into the tarball or metro never resolved it, and the consumer path is broken --
7
+ // regardless of whether the build "succeeded".
8
+ const RN_IMPELLER_SHIM_MARKER = '__RN_IMPELLER_SHIM_V1__';
9
+
10
+ const Platform = {
11
+ __constants: null,
12
+ OS: 'impeller',
13
+ get Version() {
14
+ return this.constants.osVersion || 0;
15
+ },
16
+ // P4: actually READ the PlatformConstants TurboModule. Until now this
17
+ // returned `{}`, so `Platform.constants.reactNativeVersion` was undefined and
18
+ // `Platform.Version` was 0 — while the native module sat there answering
19
+ // correctly and nothing indicated the two were not connected.
20
+ //
21
+ // TurboModuleRegistry.get (not getEnforcing): if the module is somehow
22
+ // absent, a Platform that reports empty constants is far better than a
23
+ // bundle that dies at import over a version string.
24
+ get constants() {
25
+ if (this.__constants == null) {
26
+ const TurboModuleRegistry = require('react-native/Libraries/TurboModule/TurboModuleRegistry');
27
+ const NativePlatformConstants = TurboModuleRegistry.get('PlatformConstants');
28
+ this.__constants = NativePlatformConstants
29
+ ? NativePlatformConstants.getConstants()
30
+ : {};
31
+ }
32
+ return this.__constants;
33
+ },
34
+ get isTesting() {
35
+ return this.constants.isTesting === true;
36
+ },
37
+ get isTV() {
38
+ return false;
39
+ },
40
+ get isVision() {
41
+ return false;
42
+ },
43
+ select(spec) {
44
+ if ('impeller' in spec) return spec.impeller;
45
+ if ('native' in spec) return spec.native;
46
+ return spec.default;
47
+ },
48
+ __marker: RN_IMPELLER_SHIM_MARKER,
49
+ };
50
+
51
+ module.exports = Platform;
52
+ module.exports.default = Platform;
53
+ module.exports.RN_IMPELLER_SHIM_MARKER = RN_IMPELLER_SHIM_MARKER;
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ // parity item 20 (moved here from HostApp's app-level metro-shims, ENG-09b).
3
+ // Redirect target for react-native/src/private/featureflags/
4
+ // ReactNativeFeatureFlags on platform 'impeller' only.
5
+ //
6
+ // WHY THIS FILE EXISTS
7
+ // Upstream defines shouldPressibilityUseW3CPointerEventsForHover through
8
+ // createNativeFlagGetter, which reads NativeReactNativeFeatureFlagsCxx (a
9
+ // TurboModule this engine does not register) and falls back to its default,
10
+ // false, when the module is absent. An app's override() call only feeds the
11
+ // OTHER getter family (createJavaScriptFlagGetter's shared `overrides`), so
12
+ // overriding this flag was silently a no-op and Pressability never called
13
+ // onHoverIn. Measured in HostApp: a synthesised hover_enter on a Button logged
14
+ // {"hover":false}.
15
+ //
16
+ // The default is TRUE (SC-25): the engine delivers hover_enter/leave as W3C
17
+ // pointer events, so a stock app (NativeWind hover:, group-hover:) works with
18
+ // no override. An app can still override() it to false.
19
+ //
20
+ // Fix, scoped to this ONE flag: re-export every other flag from the real
21
+ // module untouched, and redefine this one through createJavaScriptFlagGetter,
22
+ // which reads the same `overrides` object override()/setOverrides install.
23
+ //
24
+ // The require() below reaches the REAL upstream file: the resolver exempts
25
+ // requests whose origin IS this shim, so it is not redirected back here.
26
+ const real = require('react-native/src/private/featureflags/ReactNativeFeatureFlags');
27
+ const {
28
+ createJavaScriptFlagGetter,
29
+ } = require('react-native/src/private/featureflags/ReactNativeFeatureFlagsBase');
30
+
31
+ const shim = Object.create(real);
32
+ Object.defineProperty(shim, 'shouldPressibilityUseW3CPointerEventsForHover', {
33
+ value: createJavaScriptFlagGetter(
34
+ 'shouldPressibilityUseW3CPointerEventsForHover',
35
+ true,
36
+ ),
37
+ enumerable: true,
38
+ });
39
+
40
+ module.exports = shim;
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+ // SC-18. RN sends style.fontFamily to Fabric untouched, and the C++ prop parser
3
+ // only accepts a string, so NativeWind's `font-mono` (an array) failed with
4
+ // "Error while converting prop 'fontFamily'". Re-export RN's own table with a
5
+ // fontFamily processor that joins arrays into one CSS stack; the engine picks
6
+ // the first family in the stack that resolves (engine/text/FontFamily.cpp).
7
+ // Required by explicit path from this file, which index.js does not redirect.
8
+ const real = require('react-native/Libraries/Components/View/ReactNativeStyleAttributes');
9
+ const attributes = real.default || real;
10
+
11
+ function processFontFamily(value) {
12
+ if (Array.isArray(value)) {
13
+ return value.filter((v) => typeof v === 'string' && v.length > 0).join(', ');
14
+ }
15
+ return value;
16
+ }
17
+
18
+ attributes.fontFamily = {process: processFontFamily};
19
+
20
+ module.exports = real;
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+ // parity item 7. TextInputState for the `impeller` platform.
3
+ //
4
+ // WHY THIS EXISTS: RN's TextInputState.js sends the native focus/blur COMMAND
5
+ // only inside `if (Platform.OS === 'ios') ... else if (Platform.OS ===
6
+ // 'android')`, with no else. It reads Platform through a top-level
7
+ // `const Platform = require(...).default`, which Metro's inline-platform
8
+ // transform (metro-transform-plugins/src/utils/createInlinePlatformChecks.js)
9
+ // rewrites to the BUILD platform string ('impeller') before any resolver shim
10
+ // runs. Both branches are then dead code, the minifier deletes them, and
11
+ // TextInput ref.focus()/ref.blur() update JS bookkeeping and send nothing.
12
+ // The Platform-swap trick that fixes TextInput.js cannot reach this file
13
+ // (that file uses an ES import, which the inline pass does not match).
14
+ //
15
+ // NOT A DELEGATING WRAPPER: there is no .ios.js sibling to delegate to, and
16
+ // requiring the real file would redirect straight back here (SHIM_TARGETS
17
+ // matches the resolved path). So this is the upstream module with the
18
+ // platform branch fixed to the iOS commands, which target the same
19
+ // RCTSinglelineTextInputView that TextInput.js mounts on impeller. Every
20
+ // importer resolves here, so the focused-input bookkeeping stays single.
21
+ const {findNodeHandle} = require('react-native/Libraries/ReactNative/RendererProxy');
22
+ const {
23
+ Commands: iOSTextInputCommands,
24
+ } = require('react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent');
25
+
26
+ let currentlyFocusedInputRef = null;
27
+ const inputs = new Set();
28
+
29
+ function currentlyFocusedInput() {
30
+ return currentlyFocusedInputRef;
31
+ }
32
+
33
+ function currentlyFocusedField() {
34
+ return findNodeHandle(currentlyFocusedInputRef);
35
+ }
36
+
37
+ function focusInput(textField) {
38
+ if (currentlyFocusedInputRef !== textField && textField != null) {
39
+ currentlyFocusedInputRef = textField;
40
+ }
41
+ }
42
+
43
+ function blurInput(textField) {
44
+ if (currentlyFocusedInputRef === textField && textField != null) {
45
+ currentlyFocusedInputRef = null;
46
+ }
47
+ }
48
+
49
+ function focusField() {}
50
+ function blurField() {}
51
+
52
+ function focusTextInput(textField) {
53
+ if (typeof textField === 'number' || textField == null) return;
54
+ const fieldCanBeFocused =
55
+ currentlyFocusedInputRef !== textField &&
56
+ textField.currentProps?.editable !== false;
57
+ if (!fieldCanBeFocused) return;
58
+ focusInput(textField);
59
+ iOSTextInputCommands.focus(textField);
60
+ }
61
+
62
+ function blurTextInput(textField) {
63
+ if (typeof textField === 'number') return;
64
+ if (currentlyFocusedInputRef === textField && textField != null) {
65
+ blurInput(textField);
66
+ iOSTextInputCommands.blur(textField);
67
+ }
68
+ }
69
+
70
+ function registerInput(textField) {
71
+ if (typeof textField === 'number') return;
72
+ inputs.add(textField);
73
+ }
74
+
75
+ function unregisterInput(textField) {
76
+ if (typeof textField === 'number') return;
77
+ inputs.delete(textField);
78
+ }
79
+
80
+ function isTextInput(textField) {
81
+ if (typeof textField === 'number') return false;
82
+ return inputs.has(textField);
83
+ }
84
+
85
+ const TextInputState = {
86
+ currentlyFocusedInput,
87
+ focusInput,
88
+ blurInput,
89
+ currentlyFocusedField,
90
+ focusField,
91
+ blurField,
92
+ focusTextInput,
93
+ blurTextInput,
94
+ registerInput,
95
+ unregisterInput,
96
+ isTextInput,
97
+ };
98
+
99
+ module.exports = TextInputState;
100
+ module.exports.default = TextInputState;
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+ // desktop-input-gap item 6. ConditionallyIgnoredEventHandlers for 'impeller'.
3
+ //
4
+ // WHY THIS EXISTS: RN core's ViewConfigIgnore.js strips onClick/onPointerDown/
5
+ // onPointerUp/onPointerEnter/... out of BaseViewConfig's validAttributes
6
+ // unless Platform.OS === 'ios' (ConditionallyIgnoredEventHandlers returns the
7
+ // object UNCHANGED on ios, `undefined` on every other platform, including our
8
+ // own 'impeller'). A stripped validAttribute means React never forwards the
9
+ // onClick/onPointer* PROP into the native view config at all, so
10
+ // PointerEventsProcessor::shouldEmitPointerEvent reads an all-zero ViewEvents
11
+ // bitset and swallows the event before it reaches JS -- even though
12
+ // FabricHost dispatches a perfectly correct PointerEvent on the C++ side.
13
+ // NATIVE FIRES, JS NEVER HEARS. Measured 2026-09-30: a real --hover-after-frames
14
+ // run printed native `hover_enter`/`cursor=` lines while the fixture's JS
15
+ // onPointerEnter console.log never fired, with a clean exit and no error.
16
+ //
17
+ // NOT A DELEGATING WRAPPER. ViewConfigIgnore.js has no `.ios.js`/`.android.js`
18
+ // sibling, so BaseViewConfig.impeller.js's trick (require the sibling by its
19
+ // EXPLICIT platform extension, bypassing the redirect machinery) is
20
+ // unavailable here: requiring the bare specifier from inside this file would
21
+ // be redirected straight back to this same file by shimForResolvedPath
22
+ // (SHIM_TARGETS matches the RESOLVED path, not who asked), an infinite
23
+ // require cycle. So this reimplements the ~15 lines of real upstream logic
24
+ // instead of delegating to them; every caller on the impeller platform
25
+ // resolves to this one file, so the WeakSet stays globally consistent.
26
+ const ignoredViewConfigProps = new WeakSet();
27
+
28
+ function DynamicallyInjectedByGestureHandler(object) {
29
+ ignoredViewConfigProps.add(object);
30
+ return object;
31
+ }
32
+
33
+ // THE FIX: unconditionally honour onPointerDown/onPointerUp/onClick/... on
34
+ // the impeller platform, instead of upstream's `OS === 'ios' ? value :
35
+ // undefined`. See the file header for why stripping these is a native-fires/
36
+ // JS-never-hears bug, not a feature gate.
37
+ function ConditionallyIgnoredEventHandlers(value) {
38
+ return value;
39
+ }
40
+
41
+ function isIgnored(value) {
42
+ if (typeof value === 'object' && value != null) {
43
+ return ignoredViewConfigProps.has(value);
44
+ }
45
+ return false;
46
+ }
47
+
48
+ module.exports = {
49
+ DynamicallyInjectedByGestureHandler,
50
+ ConditionallyIgnoredEventHandlers,
51
+ isIgnored,
52
+ };
53
+ module.exports.DynamicallyInjectedByGestureHandler = DynamicallyInjectedByGestureHandler;
54
+ module.exports.ConditionallyIgnoredEventHandlers = ConditionallyIgnoredEventHandlers;
55
+ module.exports.isIgnored = isIgnored;
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+ // SC-26. The one JS-thread frame loop behind every animation in the
3
+ // reanimated stub. requestAnimationFrame on impeller is a setTimeout(0), so a
4
+ // rAF loop busy-spins the JS thread; frames are paced with setTimeout(16) and
5
+ // timed with performance.now(). The loop stops when nothing animates, so an
6
+ // idle app holds no timer (headless captures wait for JS idle).
7
+ const FRAME_MS = 16;
8
+ const subscribers = new Set();
9
+ let timer = null;
10
+
11
+ function now() {
12
+ return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
13
+ }
14
+
15
+ function tick() {
16
+ timer = null;
17
+ const t = now();
18
+ for (const fn of Array.from(subscribers)) {
19
+ let keep = false;
20
+ try {
21
+ keep = fn(t);
22
+ } catch (e) {
23
+ console.error(e);
24
+ }
25
+ if (!keep) subscribers.delete(fn);
26
+ }
27
+ schedule();
28
+ }
29
+
30
+ function schedule() {
31
+ if (timer == null && subscribers.size > 0) timer = setTimeout(tick, FRAME_MS);
32
+ }
33
+
34
+ // fn(now) runs once per frame while it returns true.
35
+ function onFrame(fn) {
36
+ subscribers.add(fn);
37
+ schedule();
38
+ return () => subscribers.delete(fn);
39
+ }
40
+
41
+ module.exports = {now, onFrame};
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+ // SC-26. createAnimatedComponent for the stub: a wrapper that resolves
3
+ // useAnimatedStyle / useAnimatedProps handles and CSS animation/transition
4
+ // style props to plain values on every render, and re-renders itself on the
5
+ // clock while anything is moving.
6
+ const React = require('react');
7
+ const {StyleSheet} = require('react-native');
8
+ const {now, onFrame} = require('./clock');
9
+ const {HANDLE} = require('./hooks');
10
+ const css = require('./css');
11
+
12
+ // Replace every animated-style handle in a (nested) style with its current
13
+ // values, collecting the handles to subscribe to.
14
+ function resolveHandles(style, handles) {
15
+ if (Array.isArray(style)) return style.map((s) => resolveHandles(s, handles));
16
+ if (style && typeof style === 'object' && style[HANDLE]) {
17
+ handles.push(style[HANDLE]);
18
+ return style[HANDLE].current();
19
+ }
20
+ return style;
21
+ }
22
+
23
+ function useFrames(activeRef, force) {
24
+ const stop = React.useRef(null);
25
+ React.useEffect(() => {
26
+ if (activeRef.current && !stop.current) {
27
+ stop.current = onFrame(() => {
28
+ if (!activeRef.current) {
29
+ stop.current = null;
30
+ return false;
31
+ }
32
+ force();
33
+ return true;
34
+ });
35
+ }
36
+ });
37
+ React.useEffect(() => () => stop.current && stop.current(), []);
38
+ }
39
+
40
+ const cache = new WeakMap();
41
+
42
+ function createAnimatedComponent(Component) {
43
+ if (Component == null || (typeof Component !== 'function' && typeof Component !== 'object')) return Component;
44
+ if (cache.has(Component)) return cache.get(Component);
45
+ function Animated(props, ref) {
46
+ const {style, animatedProps, entering, exiting, layout, ...rest} = props;
47
+ const [, force] = React.useReducer((x) => x + 1, 0);
48
+ const handles = [];
49
+ const resolved = resolveHandles(style, handles);
50
+ let extra = null;
51
+ if (animatedProps && animatedProps[HANDLE]) {
52
+ handles.push(animatedProps[HANDLE]);
53
+ extra = animatedProps[HANDLE].current();
54
+ }
55
+ React.useEffect(() => {
56
+ const unsubs = handles.map((h) => h.subscribe(force));
57
+ return () => unsubs.forEach((u) => u());
58
+ });
59
+ const state = React.useRef(null);
60
+ if (!state.current) state.current = css.newState();
61
+ const activeRef = React.useRef(false);
62
+ const flat = StyleSheet.flatten(resolved) || {};
63
+ const parts = css.split(flat);
64
+ let finalStyle = style;
65
+ if (parts.css || state.current.last) {
66
+ const r = css.evaluate(state.current, parts.base, parts.css, now());
67
+ activeRef.current = r.active;
68
+ finalStyle = r.style;
69
+ } else if (handles.length) {
70
+ finalStyle = flat;
71
+ }
72
+ useFrames(activeRef, force);
73
+ return React.createElement(Component, {...rest, ...(extra || {}), style: finalStyle, ref});
74
+ }
75
+ const name = Component.displayName || Component.name || 'Component';
76
+ Animated.displayName = `AnimatedComponent(${name})`;
77
+ const Wrapped = React.forwardRef(Animated);
78
+ Wrapped.displayName = Animated.displayName;
79
+ cache.set(Component, Wrapped);
80
+ return Wrapped;
81
+ }
82
+
83
+ module.exports = {createAnimatedComponent};