@samitouri / QOS-React-1 / commits / 0a3d1c3d31

Add type defs for reanimated

The reanimated babel plugin specifically looks for args to their hooks that are callbacks, then it workletizes the body of that callback so it can run on the main thread. But, forget extracts that callback into a temporary variable and then replaces the previously inlined callback as an identifier, so that breaks reanimated's babel plugin. so what happens is some of the previously workletized functions no longer do after forget runs, which throws a runtime error about a non-worklet function running on the main thread. Reanimated expects this: ``` const animatedGProps = useAnimatedProp(function () { ... }) ``` But forget does this: ``` const t0 =function () { ... } const animatedGProps = useAnimatedProp(t0) ``` With the type definitions, Forget no longer assumes the args to reanimated APIs escape so Forget does not memoize and they stay as is.

Sathya Gunasekaran committed Mar 28, 2024 at 12:26 UTC 0a3d1c3d31ce83d18906357c29381e722cbec8b4
5 files changed +195
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+15
@@ -17,6 +17,7 @@ import {
17 DEFAULT_SHAPES,
18 Global,
19 GlobalRegistry,
20 + installReAnimatedTypes,
21 } from "./Globals";
22 import {
23 BlockId,
@@ -357,6 +358,16 @@ const EnvironmentConfigSchema = z.object({
358 */
359 enableTreatFunctionDepsAsConditional: z.boolean().default(false),
360
361 + /**
362 + * The react native re-animated library uses custom Babel transforms that
363 + * requires the calls to library API remain unmodified.
364 + *
365 + * If this flag is turned on, the React compiler will use custom type
366 + * definitions for reanimated library to make it's Babel plugin work
367 + * with the compiler.
368 + */
369 + enableCustomTypeDefinitionForReAnimated: z.boolean().default(false),
370 +
371 /**
372 * If specified, this value is used as a pattern for determing which global values should be
373 * treated as hooks. The pattern should have a single capture group, which will be used as
@@ -469,6 +480,10 @@ export class Environment {
480 );
481 }
482
483 + if (config.enableCustomTypeDefinitionForReAnimated) {
484 + installReAnimatedTypes(this.#globals, this.#shapes);
485 + }
486 +
487 this.#contextIdentifiers = contextIdentifiers;
488 this.#hoistedIdentifiers = new Set();
489 }
compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts
+74
@@ -9,6 +9,7 @@ import { Effect, ValueKind, ValueReason } from "./HIR";
9 import {
10 BUILTIN_SHAPES,
11 BuiltInArrayId,
12 + BuiltInMixedReadonlyId,
13 BuiltInUseEffectHookId,
14 BuiltInUseInsertionEffectHookId,
15 BuiltInUseLayoutEffectHookId,
@@ -413,3 +414,76 @@ DEFAULT_GLOBALS.set(
414 "globalThis",
415 addObject(DEFAULT_SHAPES, "globalThis", TYPED_GLOBALS)
416 );
417 +
418 +export function installReAnimatedTypes(
419 + globals: GlobalRegistry,
420 + registry: ShapeRegistry
421 +): void {
422 + // hooks that freeze args and return frozen value
423 + const frozenHooks = [
424 + "useFrameCallback",
425 + "useAnimatedStyle",
426 + "useAnimatedProps",
427 + "useAnimatedScrollHandler",
428 + "useAnimatedReaction",
429 + "useWorkletCallback",
430 + ];
431 + for (const hook of frozenHooks) {
432 + globals.set(
433 + hook,
434 + addHook(registry, {
435 + positionalParams: [],
436 + restParam: Effect.Freeze,
437 + returnType: { kind: "Object", shapeId: BuiltInMixedReadonlyId },
438 + returnValueKind: ValueKind.Frozen,
439 + noAlias: true,
440 + calleeEffect: Effect.Read,
441 + hookKind: "Custom",
442 + })
443 + );
444 + }
445 +
446 + /**
447 + * hooks that return a mutable value. ideally these should be modelled as a
448 + * ref, but this works for now.
449 + */
450 + const mutableHooks = ["useSharedValue", "useDerivedValue"];
451 + for (const hook of mutableHooks) {
452 + globals.set(
453 + hook,
454 + addHook(registry, {
455 + positionalParams: [],
456 + restParam: Effect.Freeze,
457 + returnType: { kind: "Poly" },
458 + returnValueKind: ValueKind.Mutable,
459 + noAlias: true,
460 + calleeEffect: Effect.Read,
461 + hookKind: "Custom",
462 + })
463 + );
464 + }
465 +
466 + // functions that return mutable value
467 + const funcs = [
468 + "withTiming",
469 + "withSpring",
470 + "createAnimatedPropAdapter",
471 + "withDecay",
472 + "withRepeat",
473 + "runOnUI",
474 + "executeOnUIRuntimeSync",
475 + ];
476 + for (const fn of funcs) {
477 + globals.set(
478 + fn,
479 + addFunction(registry, [], {
480 + positionalParams: [],
481 + restParam: Effect.Read,
482 + returnType: { kind: "Poly" },
483 + calleeEffect: Effect.Read,
484 + returnValueKind: ValueKind.Mutable,
485 + noAlias: true,
486 + })
487 + );
488 + }
489 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.expect.md new
+76
@@ -0,0 +1,76 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableCustomTypeDefinitionForReAnimated
6 +function Component() {
7 + const radius = useSharedValue(50);
8 +
9 + const animatedProps = useAnimatedProps(() => {
10 + // draw a circle
11 + const path = `
12 + M 100, 100
13 + m -${radius.value}, 0
14 + a ${radius.value},${radius.value} 0 1,0 ${radius.value * 2},0
15 + a ${radius.value},${radius.value} 0 1,0 ${-radius.value * 2},0
16 + `;
17 + return {
18 + d: path,
19 + };
20 + });
21 +
22 + // attach animated props to an SVG path using animatedProps
23 + return (
24 + <Svg>
25 + <AnimatedPath animatedProps={animatedProps} fill="black" />
26 + </Svg>
27 + );
28 +}
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: Component,
31 + params: [],
32 + isComponent: false,
33 +};
34 +
35 +```
36 +
37 +## Code
38 +
39 +```javascript
40 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableCustomTypeDefinitionForReAnimated
41 +function Component() {
42 + const $ = useMemoCache(2);
43 + const radius = useSharedValue(50);
44 +
45 + const animatedProps = useAnimatedProps(() => {
46 + const path = `
47 + M 100, 100
48 + m -${radius.value}, 0
49 + a ${radius.value},${radius.value} 0 1,0 ${radius.value * 2},0
50 + a ${radius.value},${radius.value} 0 1,0 ${-radius.value * 2},0
51 + `;
52 + return { d: path };
53 + });
54 + let t0;
55 + if ($[0] !== animatedProps) {
56 + t0 = (
57 + <Svg>
58 + <AnimatedPath animatedProps={animatedProps} fill="black" />
59 + </Svg>
60 + );
61 + $[0] = animatedProps;
62 + $[1] = t0;
63 + } else {
64 + t0 = $[1];
65 + }
66 + return t0;
67 +}
68 +
69 +export const FIXTURE_ENTRYPOINT = {
70 + fn: Component,
71 + params: [],
72 + isComponent: false,
73 +};
74 +
75 +```
76 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.js new
+29
@@ -0,0 +1,29 @@
1 +// @enableCustomTypeDefinitionForReAnimated
2 +function Component() {
3 + const radius = useSharedValue(50);
4 +
5 + const animatedProps = useAnimatedProps(() => {
6 + // draw a circle
7 + const path = `
8 + M 100, 100
9 + m -${radius.value}, 0
10 + a ${radius.value},${radius.value} 0 1,0 ${radius.value * 2},0
11 + a ${radius.value},${radius.value} 0 1,0 ${-radius.value * 2},0
12 + `;
13 + return {
14 + d: path,
15 + };
16 + });
17 +
18 + // attach animated props to an SVG path using animatedProps
19 + return (
20 + <Svg>
21 + <AnimatedPath animatedProps={animatedProps} fill="black" />
22 + </Svg>
23 + );
24 +}
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: Component,
27 + params: [],
28 + isComponent: false,
29 +};
compiler/packages/snap/src/SproutTodoFilter.ts
+1
@@ -490,6 +490,7 @@ const skipFilter = new Set([
490 "fbt/fbt-preserve-jsxtext",
491 "todo.useContext-mutate-context-in-callback",
492 "loop-unused-let",
493 + "reanimated-no-memo-arg",
494
495 // Tested e2e in forget-feedback repo
496 "userspace-use-memo-cache",