diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts index 901dfd6ff6..676e9ecd0b 100644 --- a/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts @@ -17,6 +17,7 @@ import { DEFAULT_SHAPES, Global, GlobalRegistry, + installReAnimatedTypes, } from "./Globals"; import { BlockId, @@ -357,6 +358,16 @@ const EnvironmentConfigSchema = z.object({ */ enableTreatFunctionDepsAsConditional: z.boolean().default(false), + /** + * The react native re-animated library uses custom Babel transforms that + * requires the calls to library API remain unmodified. + * + * If this flag is turned on, the React compiler will use custom type + * definitions for reanimated library to make it's Babel plugin work + * with the compiler. + */ + enableCustomTypeDefinitionForReAnimated: z.boolean().default(false), + /** * If specified, this value is used as a pattern for determing which global values should be * treated as hooks. The pattern should have a single capture group, which will be used as @@ -469,6 +480,10 @@ export class Environment { ); } + if (config.enableCustomTypeDefinitionForReAnimated) { + installReAnimatedTypes(this.#globals, this.#shapes); + } + this.#contextIdentifiers = contextIdentifiers; this.#hoistedIdentifiers = new Set(); } diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts index 899bb64108..2679833c0b 100644 --- a/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts @@ -9,6 +9,7 @@ import { Effect, ValueKind, ValueReason } from "./HIR"; import { BUILTIN_SHAPES, BuiltInArrayId, + BuiltInMixedReadonlyId, BuiltInUseEffectHookId, BuiltInUseInsertionEffectHookId, BuiltInUseLayoutEffectHookId, @@ -413,3 +414,76 @@ DEFAULT_GLOBALS.set( "globalThis", addObject(DEFAULT_SHAPES, "globalThis", TYPED_GLOBALS) ); + +export function installReAnimatedTypes( + globals: GlobalRegistry, + registry: ShapeRegistry +): void { + // hooks that freeze args and return frozen value + const frozenHooks = [ + "useFrameCallback", + "useAnimatedStyle", + "useAnimatedProps", + "useAnimatedScrollHandler", + "useAnimatedReaction", + "useWorkletCallback", + ]; + for (const hook of frozenHooks) { + globals.set( + hook, + addHook(registry, { + positionalParams: [], + restParam: Effect.Freeze, + returnType: { kind: "Object", shapeId: BuiltInMixedReadonlyId }, + returnValueKind: ValueKind.Frozen, + noAlias: true, + calleeEffect: Effect.Read, + hookKind: "Custom", + }) + ); + } + + /** + * hooks that return a mutable value. ideally these should be modelled as a + * ref, but this works for now. + */ + const mutableHooks = ["useSharedValue", "useDerivedValue"]; + for (const hook of mutableHooks) { + globals.set( + hook, + addHook(registry, { + positionalParams: [], + restParam: Effect.Freeze, + returnType: { kind: "Poly" }, + returnValueKind: ValueKind.Mutable, + noAlias: true, + calleeEffect: Effect.Read, + hookKind: "Custom", + }) + ); + } + + // functions that return mutable value + const funcs = [ + "withTiming", + "withSpring", + "createAnimatedPropAdapter", + "withDecay", + "withRepeat", + "runOnUI", + "executeOnUIRuntimeSync", + ]; + for (const fn of funcs) { + globals.set( + fn, + addFunction(registry, [], { + positionalParams: [], + restParam: Effect.Read, + returnType: { kind: "Poly" }, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + noAlias: true, + }) + ); + } +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.expect.md new file mode 100644 index 0000000000..94e4ba4d07 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.expect.md @@ -0,0 +1,76 @@ + +## Input + +```javascript +// @enableCustomTypeDefinitionForReAnimated +function Component() { + const radius = useSharedValue(50); + + const animatedProps = useAnimatedProps(() => { + // draw a circle + const path = ` + M 100, 100 + m -${radius.value}, 0 + a ${radius.value},${radius.value} 0 1,0 ${radius.value * 2},0 + a ${radius.value},${radius.value} 0 1,0 ${-radius.value * 2},0 + `; + return { + d: path, + }; + }); + + // attach animated props to an SVG path using animatedProps + return ( + + + + ); +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false, +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableCustomTypeDefinitionForReAnimated +function Component() { + const $ = useMemoCache(2); + const radius = useSharedValue(50); + + const animatedProps = useAnimatedProps(() => { + const path = ` + M 100, 100 + m -${radius.value}, 0 + a ${radius.value},${radius.value} 0 1,0 ${radius.value * 2},0 + a ${radius.value},${radius.value} 0 1,0 ${-radius.value * 2},0 + `; + return { d: path }; + }); + let t0; + if ($[0] !== animatedProps) { + t0 = ( + + + + ); + $[0] = animatedProps; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false, +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.js new file mode 100644 index 0000000000..eca548f748 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.js @@ -0,0 +1,29 @@ +// @enableCustomTypeDefinitionForReAnimated +function Component() { + const radius = useSharedValue(50); + + const animatedProps = useAnimatedProps(() => { + // draw a circle + const path = ` + M 100, 100 + m -${radius.value}, 0 + a ${radius.value},${radius.value} 0 1,0 ${radius.value * 2},0 + a ${radius.value},${radius.value} 0 1,0 ${-radius.value * 2},0 + `; + return { + d: path, + }; + }); + + // attach animated props to an SVG path using animatedProps + return ( + + + + ); +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false, +}; diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts index fef339c363..2faaf15443 100644 --- a/compiler/packages/snap/src/SproutTodoFilter.ts +++ b/compiler/packages/snap/src/SproutTodoFilter.ts @@ -490,6 +490,7 @@ const skipFilter = new Set([ "fbt/fbt-preserve-jsxtext", "todo.useContext-mutate-context-in-callback", "loop-unused-let", + "reanimated-no-memo-arg", // Tested e2e in forget-feedback repo "userspace-use-memo-cache",