diff --git a/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap b/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap index 7a20480853d..7f1bf7f7934 100644 --- a/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap +++ b/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap @@ -926,6 +926,30 @@ var animatedStyle = useAnimatedStyle(function () { }());" `; +exports[`babel plugin for function hooks workletizes hook wrapped worklet reference automatically 1`] = ` +"var _worklet_377598887181_init_data = { + code: "function style(){return{color:'red',backgroundColor:'blue'};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var style = function () { + const _e = [new global.Error(), 1, -27]; + const style = function () { + return { + color: 'red', + backgroundColor: 'blue' + }; + }; + style.__closure = {}; + style.__workletHash = 377598887181; + style.__initData = _worklet_377598887181_init_data; + style.__stackDetails = _e; + return style; +}(); +var animatedStyle = useAnimatedStyle(style);" +`; + exports[`babel plugin for generators makes a generator worklet factory 1`] = ` "var _worklet_4939499253486_init_data = { code: "function*foo(){yield'hello';yield'world';}", @@ -1573,64 +1597,353 @@ var foo = Gesture.Tap().numberOfTaps(2).onBegin(function () { }());" `; -exports[`babel plugin for runOnUI workletizes ArrowFunctionExpression inside runOnUI automatically 1`] = ` -"var _worklet_15854903236968_init_data = { - code: "function anonymous(){console.log('Hello from the UI thread!');}", +exports[`babel plugin for referenced worklets prefers AssignmentExpression over VariableDeclarator 1`] = ` +"var styleFactory = function styleFactory() { + return 1; +}; +var _worklet_2302253389457_init_data = { + code: "function styleFactory(){return'AssignmentExpression';}", location: "/dev/null", sourceMap: "\\"mock source map\\"", version: "x.y.z" }; -runOnUI(function () { +styleFactory = function () { + const _e = [new global.Error(), 1, -27]; + const styleFactory = function () { + return 'AssignmentExpression'; + }; + styleFactory.__closure = {}; + styleFactory.__workletHash = 2302253389457; + styleFactory.__initData = _worklet_2302253389457_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; +}(); +animatedStyle = useAnimatedStyle(styleFactory);" +`; + +exports[`babel plugin for referenced worklets prefers FunctionDeclaration over AssignmentExpression 1`] = ` +"var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +var _readOnlyError2 = _interopRequireDefault(require("@babel/runtime/helpers/readOnlyError")); +var _worklet_8893014081746_init_data = { + code: "function styleFactory(){return'FunctionDeclaration';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var styleFactory = function () { var _e = [new global.Error(), 1, -27]; - var anonymous = function anonymous() { - console.log('Hello from the UI thread!'); + var styleFactory = function styleFactory() { + return 'FunctionDeclaration'; }; - anonymous.__closure = {}; - anonymous.__workletHash = 15854903236968; - anonymous.__initData = _worklet_15854903236968_init_data; - anonymous.__stackDetails = _e; - return anonymous; -}())();" + styleFactory.__closure = {}; + styleFactory.__workletHash = 8893014081746; + styleFactory.__initData = _worklet_8893014081746_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; +}(); +(function styleFactory() { + return 'AssignmentExpression'; +}), (0, _readOnlyError2.default)("styleFactory"); +animatedStyle = useAnimatedStyle(styleFactory);" `; -exports[`babel plugin for runOnUI workletizes named FunctionExpression inside runOnUI automatically 1`] = ` -"var _worklet_5662051517689_init_data = { - code: "function hello(){console.log('Hello from the UI thread!');}", +exports[`babel plugin for referenced worklets workletizes ArrowFunctionExpression on its AssignmentExpression 1`] = ` +"var styleFactory; +var _worklet_155831007318_init_data = { + code: "function styleFactory(){return{};}", location: "/dev/null", sourceMap: "\\"mock source map\\"", version: "x.y.z" }; -runOnUI(function () { +styleFactory = function () { + const _e = [new global.Error(), 1, -27]; + const styleFactory = function () { + return {}; + }; + styleFactory.__closure = {}; + styleFactory.__workletHash = 155831007318; + styleFactory.__initData = _worklet_155831007318_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; +}(); +animatedStyle = useAnimatedStyle(styleFactory);" +`; + +exports[`babel plugin for referenced worklets workletizes ArrowFunctionExpression on its VariableDeclarator 1`] = ` +"var _worklet_155831007318_init_data = { + code: "function styleFactory(){return{};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var styleFactory = function () { + const _e = [new global.Error(), 1, -27]; + const styleFactory = function () { + return {}; + }; + styleFactory.__closure = {}; + styleFactory.__workletHash = 155831007318; + styleFactory.__initData = _worklet_155831007318_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; +}(); +var animatedStyle = useAnimatedStyle(styleFactory);" +`; + +exports[`babel plugin for referenced worklets workletizes ArrowFunctionExpression only on last AssignmentExpression 1`] = ` +"var styleFactory; +styleFactory = function styleFactory() { + return 1; +}; +var _worklet_2302253389457_init_data = { + code: "function styleFactory(){return'AssignmentExpression';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +styleFactory = function () { + const _e = [new global.Error(), 1, -27]; + const styleFactory = function () { + return 'AssignmentExpression'; + }; + styleFactory.__closure = {}; + styleFactory.__workletHash = 2302253389457; + styleFactory.__initData = _worklet_2302253389457_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; +}(); +animatedStyle = useAnimatedStyle(styleFactory);" +`; + +exports[`babel plugin for referenced worklets workletizes FunctionDeclaration 1`] = ` +"var _worklet_155831007318_init_data = { + code: "function styleFactory(){return{};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var styleFactory = function () { var _e = [new global.Error(), 1, -27]; - var hello = function hello() { - console.log('Hello from the UI thread!'); - }; - hello.__closure = {}; - hello.__workletHash = 5662051517689; - hello.__initData = _worklet_5662051517689_init_data; - hello.__stackDetails = _e; - return hello; -}())();" + var styleFactory = function styleFactory() { + return {}; + }; + styleFactory.__closure = {}; + styleFactory.__workletHash = 155831007318; + styleFactory.__initData = _worklet_155831007318_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; +}(); +var animatedStyle = useAnimatedStyle(styleFactory);" `; -exports[`babel plugin for runOnUI workletizes unnamed FunctionExpression inside runOnUI automatically 1`] = ` -"var _worklet_15854903236968_init_data = { - code: "function anonymous(){console.log('Hello from the UI thread!');}", +exports[`babel plugin for referenced worklets workletizes FunctionExpression on its AssignmentExpression 1`] = ` +"var styleFactory; +var _worklet_155831007318_init_data = { + code: "function styleFactory(){return{};}", location: "/dev/null", sourceMap: "\\"mock source map\\"", version: "x.y.z" }; -runOnUI(function () { +styleFactory = function () { + const _e = [new global.Error(), 1, -27]; + const styleFactory = function () { + return {}; + }; + styleFactory.__closure = {}; + styleFactory.__workletHash = 155831007318; + styleFactory.__initData = _worklet_155831007318_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; +}(); +animatedStyle = useAnimatedStyle(styleFactory);" +`; + +exports[`babel plugin for referenced worklets workletizes FunctionExpression on its VariableDeclarator 1`] = ` +"var _worklet_155831007318_init_data = { + code: "function styleFactory(){return{};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var styleFactory = function () { + const _e = [new global.Error(), 1, -27]; + const styleFactory = function () { + return {}; + }; + styleFactory.__closure = {}; + styleFactory.__workletHash = 155831007318; + styleFactory.__initData = _worklet_155831007318_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; +}(); +var animatedStyle = useAnimatedStyle(styleFactory);" +`; + +exports[`babel plugin for referenced worklets workletizes FunctionExpression only on last AssignmentExpression 1`] = ` +"var styleFactory; +styleFactory = function styleFactory() { + return 1; +}; +var _worklet_2302253389457_init_data = { + code: "function styleFactory(){return'AssignmentExpression';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +styleFactory = function () { + const _e = [new global.Error(), 1, -27]; + const styleFactory = function () { + return 'AssignmentExpression'; + }; + styleFactory.__closure = {}; + styleFactory.__workletHash = 2302253389457; + styleFactory.__initData = _worklet_2302253389457_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; +}(); +animatedStyle = useAnimatedStyle(styleFactory);" +`; + +exports[`babel plugin for referenced worklets workletizes ObjectExpression on its AssignmentExpression 1`] = ` +"var handler; +var _worklet_16988157363598_init_data = { + code: "function onScroll(){}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +handler = { + onScroll: function () { + const _e = [new global.Error(), 1, -27]; + const onScroll = function () {}; + onScroll.__closure = {}; + onScroll.__workletHash = 16988157363598; + onScroll.__initData = _worklet_16988157363598_init_data; + onScroll.__stackDetails = _e; + return onScroll; + }() +}; +var scrollHandler = useAnimatedScrollHandler(handler);" +`; + +exports[`babel plugin for referenced worklets workletizes ObjectExpression on its VariableDeclarator 1`] = ` +"var _worklet_16988157363598_init_data = { + code: "function onScroll(){}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var handler = { + onScroll: function () { + const _e = [new global.Error(), 1, -27]; + const onScroll = function () {}; + onScroll.__closure = {}; + onScroll.__workletHash = 16988157363598; + onScroll.__initData = _worklet_16988157363598_init_data; + onScroll.__stackDetails = _e; + return onScroll; + }() +}; +var scrollHandler = useAnimatedScrollHandler(handler);" +`; + +exports[`babel plugin for referenced worklets workletizes ObjectExpression only on last AssignmentExpression 1`] = ` +"var handler; +handler = { + onScroll: function onScroll() { + return 1; + } +}; +var _worklet_9384619123806_init_data = { + code: "function onScroll(){return'AssignmentExpression';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +handler = { + onScroll: function () { + const _e = [new global.Error(), 1, -27]; + const onScroll = function () { + return 'AssignmentExpression'; + }; + onScroll.__closure = {}; + onScroll.__workletHash = 9384619123806; + onScroll.__initData = _worklet_9384619123806_init_data; + onScroll.__stackDetails = _e; + return onScroll; + }() +}; +var scrollHandler = useAnimatedScrollHandler(handler);" +`; + +exports[`babel plugin for referenced worklets workletizes assigments that appear after the worklet is used 1`] = ` +"var styleFactory = function styleFactory() { + return {}; +}; +animatedStyle = useAnimatedStyle(styleFactory); +var _worklet_12348123604788_init_data = { + code: "function anonymous(){return'AssignmentAfterUse';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +styleFactory = function () { var _e = [new global.Error(), 1, -27]; var anonymous = function anonymous() { - console.log('Hello from the UI thread!'); + return 'AssignmentAfterUse'; }; anonymous.__closure = {}; - anonymous.__workletHash = 15854903236968; - anonymous.__initData = _worklet_15854903236968_init_data; + anonymous.__workletHash = 12348123604788; + anonymous.__initData = _worklet_12348123604788_init_data; anonymous.__stackDetails = _e; return anonymous; -}())();" +}();" +`; + +exports[`babel plugin for referenced worklets workletizes in immediate scope 1`] = ` +"var _worklet_155831007318_init_data = { + code: "function styleFactory(){return{};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var styleFactory = function () { + const _e = [new global.Error(), 1, -27]; + const styleFactory = function () { + return {}; + }; + styleFactory.__closure = {}; + styleFactory.__workletHash = 155831007318; + styleFactory.__initData = _worklet_155831007318_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; +}(); +animatedStyle = useAnimatedStyle(styleFactory);" +`; + +exports[`babel plugin for referenced worklets workletizes in nested scope 1`] = ` +"var _worklet_155831007318_init_data = { + code: "function styleFactory(){return{};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +function outerScope() { + var styleFactory = function () { + const _e = [new global.Error(), 1, -27]; + const styleFactory = function () { + return {}; + }; + styleFactory.__closure = {}; + styleFactory.__workletHash = 155831007318; + styleFactory.__initData = _worklet_155831007318_init_data; + styleFactory.__stackDetails = _e; + return styleFactory; + }(); + function innerScope() { + animatedStyle = useAnimatedStyle(styleFactory); + } +}" `; exports[`babel plugin for sequence expressions supports SequenceExpression 1`] = ` diff --git a/packages/react-native-reanimated/__tests__/plugin.test.ts b/packages/react-native-reanimated/__tests__/plugin.test.ts index 289f447321c..1ebb9ca33bd 100644 --- a/packages/react-native-reanimated/__tests__/plugin.test.ts +++ b/packages/react-native-reanimated/__tests__/plugin.test.ts @@ -246,7 +246,7 @@ describe('babel plugin', () => { const { code, ast } = runPlugin(input, { ast: true }); let closureBindings; - traverse(ast, { + traverse(ast!, { enter(path) { if ( path.isAssignmentExpression() && @@ -534,38 +534,16 @@ describe('babel plugin', () => { expect(code).toHaveWorkletData(); expect(code).toMatchSnapshot(); }); - }); - - describe('for runOnUI', () => { - it('workletizes ArrowFunctionExpression inside runOnUI automatically', () => { - const input = html``; - - const { code } = runPlugin(input); - expect(code).toHaveWorkletData(); - expect(code).toMatchSnapshot(); - }); - it('workletizes unnamed FunctionExpression inside runOnUI automatically', () => { + it('workletizes hook wrapped worklet reference automatically', () => { const input = html``; - - const { code } = runPlugin(input); - expect(code).toHaveWorkletData(); - expect(code).toMatchSnapshot(); - }); - - it('workletizes named FunctionExpression inside runOnUI automatically', () => { - const input = html``; const { code } = runPlugin(input); @@ -1013,7 +991,7 @@ describe('babel plugin', () => { const { code, ast } = runPlugin(input, { ast: true }); let closureBindings; - traverse(ast, { + traverse(ast!, { enter(path) { if ( path.isAssignmentExpression() && @@ -1768,4 +1746,218 @@ describe('babel plugin', () => { expect(code).toMatchSnapshot(); }); }); + + describe('for referenced worklets', () => { + it('workletizes ArrowFunctionExpression on its VariableDeclarator', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ArrowFunctionExpression on its AssignmentExpression', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ArrowFunctionExpression only on last AssignmentExpression', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toIncludeInWorkletString('AssignmentExpression'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes FunctionExpression on its VariableDeclarator', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toMatchSnapshot(); + }); + + it('workletizes FunctionExpression on its AssignmentExpression', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toMatchSnapshot(); + }); + + it('workletizes FunctionExpression only on last AssignmentExpression', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toIncludeInWorkletString('AssignmentExpression'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes FunctionDeclaration', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ObjectExpression on its VariableDeclarator', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ObjectExpression on its AssignmentExpression', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ObjectExpression only on last AssignmentExpression', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toIncludeInWorkletString('AssignmentExpression'); + expect(code).toMatchSnapshot(); + }); + + it('prefers FunctionDeclaration over AssignmentExpression', () => { + const input = html``; + console.log(input); + const { code } = runPlugin(input); + console.log(code); + + expect(code).toHaveWorkletData(1); + expect(code).toIncludeInWorkletString('FunctionDeclaration'); + expect(code).toMatchSnapshot(); + }); + + it('prefers AssignmentExpression over VariableDeclarator', () => { + // This is an anti-pattern, but let's at least have a defined behavior here. + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toIncludeInWorkletString('AssignmentExpression'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes in immediate scope', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toMatchSnapshot(); + }); + + it('workletizes in nested scope', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toMatchSnapshot(); + }); + + it('workletizes assigments that appear after the worklet is used', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(1); + expect(code).toIncludeInWorkletString('AssignmentAfterUse'); + expect(code).toMatchSnapshot(); + }); + }); }); diff --git a/packages/react-native-reanimated/plugin/build/plugin.js b/packages/react-native-reanimated/plugin/build/plugin.js index cbc222c18cf..6ffae2f7ab2 100644 --- a/packages/react-native-reanimated/plugin/build/plugin.js +++ b/packages/react-native-reanimated/plugin/build/plugin.js @@ -30,12 +30,17 @@ var require_types = __commonJS({ "lib/types.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.isWorkletizableFunctionType = exports2.WorkletizableFunction = void 0; + exports2.isWorkletizableObjectType = exports2.isWorkletizableFunctionType = exports2.WorkletizableObject = exports2.WorkletizableFunction = void 0; exports2.WorkletizableFunction = "FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ObjectMethod"; + exports2.WorkletizableObject = "ObjectExpression"; function isWorkletizableFunctionType(path) { return path.isFunctionDeclaration() || path.isFunctionExpression() || path.isArrowFunctionExpression() || path.isObjectMethod(); } exports2.isWorkletizableFunctionType = isWorkletizableFunctionType; + function isWorkletizableObjectType(path) { + return path.isObjectExpression(); + } + exports2.isWorkletizableObjectType = isWorkletizableObjectType; } }); @@ -817,6 +822,88 @@ var require_layoutAnimationAutoworkletization = __commonJS({ } }); +// lib/referencedWorklets.js +var require_referencedWorklets = __commonJS({ + "lib/referencedWorklets.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.findReferencedWorklet = void 0; + var types_12 = require_types(); + function findReferencedWorklet(workletIdentifier, acceptWorkletizableFunction, acceptObject) { + const workletName = workletIdentifier.node.name; + const scope = workletIdentifier.scope; + const workletBinding = scope.getBinding(workletName); + if (!workletBinding) { + return void 0; + } + if (acceptWorkletizableFunction && workletBinding.path.isFunctionDeclaration()) { + return workletBinding.path; + } + const isConstant = workletBinding.constant; + if (isConstant) { + return findReferencedWorkletFromVariableDeclarator(workletBinding, acceptWorkletizableFunction, acceptObject); + } + return findReferencedWorkletFromAssignmentExpression(workletBinding, acceptWorkletizableFunction, acceptObject); + } + exports2.findReferencedWorklet = findReferencedWorklet; + function findReferencedWorkletFromVariableDeclarator(workletBinding, acceptWorkletizableFunction, acceptObject) { + const workletDeclaration = workletBinding.path; + if (!workletDeclaration.isVariableDeclarator()) { + return void 0; + } + const worklet = workletDeclaration.get("init"); + if (acceptWorkletizableFunction && (0, types_12.isWorkletizableFunctionType)(worklet)) { + return worklet; + } + if (acceptObject && (0, types_12.isWorkletizableObjectType)(worklet)) { + return worklet; + } + return void 0; + } + function findReferencedWorkletFromAssignmentExpression(workletBinding, acceptWorkletizableFunction, acceptObject) { + const workletDeclaration = workletBinding.constantViolations.reverse().find((constantViolation) => constantViolation.isAssignmentExpression() && (acceptWorkletizableFunction && (0, types_12.isWorkletizableFunctionType)(constantViolation.get("right")) || acceptObject && (0, types_12.isWorkletizableObjectType)(constantViolation.get("right")))); + if (!workletDeclaration || !workletDeclaration.isAssignmentExpression()) { + return void 0; + } + const workletDefinition = workletDeclaration.get("right"); + if (acceptWorkletizableFunction && (0, types_12.isWorkletizableFunctionType)(workletDefinition)) { + return workletDefinition; + } + if (acceptObject && (0, types_12.isWorkletizableObjectType)(workletDefinition)) { + return workletDefinition; + } + return void 0; + } + } +}); + +// lib/objectWorklets.js +var require_objectWorklets = __commonJS({ + "lib/objectWorklets.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.processWorkletizableObject = void 0; + var types_12 = require_types(); + var workletSubstitution_12 = require_workletSubstitution(); + function processWorkletizableObject(path, state) { + const properties = path.get("properties"); + for (const property of properties) { + if (property.isObjectMethod()) { + (0, workletSubstitution_12.processWorklet)(property, state); + } else if (property.isObjectProperty()) { + const value = property.get("value"); + if ((0, types_12.isWorkletizableFunctionType)(value)) { + (0, workletSubstitution_12.processWorklet)(value, state); + } + } else { + throw new Error(`[Reanimated] '${property.type}' as to-be workletized argument is not supported for object hooks.`); + } + } + } + exports2.processWorkletizableObject = processWorkletizableObject; + } +}); + // lib/autoworkletization.js var require_autoworkletization = __commonJS({ "lib/autoworkletization.js"(exports2) { @@ -825,11 +912,33 @@ var require_autoworkletization = __commonJS({ exports2.processCalleesAutoworkletizableCallbacks = exports2.processIfAutoworkletizableCallback = void 0; var types_12 = require("@babel/types"); var types_2 = require_types(); - var assert_1 = require("assert"); var workletSubstitution_12 = require_workletSubstitution(); var gestureHandlerAutoworkletization_1 = require_gestureHandlerAutoworkletization(); var layoutAnimationAutoworkletization_1 = require_layoutAnimationAutoworkletization(); + var referencedWorklets_1 = require_referencedWorklets(); + var objectWorklets_1 = require_objectWorklets(); + var objectHooks = /* @__PURE__ */ new Set([ + "useAnimatedGestureHandler", + "useAnimatedScrollHandler" + ]); + var functionHooks = /* @__PURE__ */ new Set([ + "useFrameCallback", + "useAnimatedStyle", + "useAnimatedProps", + "createAnimatedPropAdapter", + "useDerivedValue", + "useAnimatedScrollHandler", + "useAnimatedReaction", + "useWorkletCallback", + "withTiming", + "withSpring", + "withDecay", + "withRepeat", + "runOnUI", + "executeOnUIRuntimeSync" + ]); var functionArgsToWorkletize = /* @__PURE__ */ new Map([ + ["useAnimatedGestureHandler", [0]], ["useFrameCallback", [0]], ["useAnimatedStyle", [0]], ["useAnimatedProps", [0]], @@ -845,10 +954,6 @@ var require_autoworkletization = __commonJS({ ["runOnUI", [0]], ["executeOnUIRuntimeSync", [0]] ]); - var objectHooks = /* @__PURE__ */ new Set([ - "useAnimatedGestureHandler", - "useAnimatedScrollHandler" - ]); function processIfAutoworkletizableCallback(path, state) { if ((0, gestureHandlerAutoworkletization_1.isGestureHandlerEventCallback)(path) || (0, layoutAnimationAutoworkletization_1.isLayoutAnimationCallback)(path)) { (0, workletSubstitution_12.processWorklet)(path, state); @@ -863,52 +968,40 @@ var require_autoworkletization = __commonJS({ if (name === void 0) { return; } - if (objectHooks.has(name)) { - const maybeWorklet = path.get("arguments.0"); - (0, assert_1.strict)(!Array.isArray(maybeWorklet), "[Reanimated] `workletToProcess` is an array."); - if (maybeWorklet.isObjectExpression()) { - processObjectHook(maybeWorklet, state); - } else if (name === "useAnimatedScrollHandler") { - if ((0, types_2.isWorkletizableFunctionType)(maybeWorklet)) { - (0, workletSubstitution_12.processWorklet)(maybeWorklet, state); - } - } - } else { - const indices = functionArgsToWorkletize.get(name); - if (indices === void 0) { - return; - } - processArguments(path, indices, state); + if (functionHooks.has(name) || objectHooks.has(name)) { + const acceptWorkletizableFunction = functionHooks.has(name); + const acceptObject = objectHooks.has(name); + const argIndices = functionArgsToWorkletize.get(name); + const args = path.get("arguments").filter((_, index) => argIndices.includes(index)); + processArgs(args, state, acceptWorkletizableFunction, acceptObject); } } exports2.processCalleesAutoworkletizableCallbacks = processCalleesAutoworkletizableCallbacks; - function processObjectHook(path, state) { - const properties = path.get("properties"); - for (const property of properties) { - if (property.isObjectMethod()) { - (0, workletSubstitution_12.processWorklet)(property, state); - } else if (property.isObjectProperty()) { - const value = property.get("value"); - if ((0, types_2.isWorkletizableFunctionType)(value)) { - (0, workletSubstitution_12.processWorklet)(value, state); - } - } else { - throw new Error(`[Reanimated] '${property.type}' as to-be workletized argument is not supported for object hooks.`); - } - } - } - function processArguments(path, indices, state) { - const argumentsArray = path.get("arguments"); - indices.forEach((index) => { - const maybeWorklet = argumentsArray[index]; + function processArgs(args, state, acceptWorkletizableFunction, acceptObject) { + args.forEach((arg) => { + const maybeWorklet = findWorklet(arg, acceptWorkletizableFunction, acceptObject); if (!maybeWorklet) { return; } if ((0, types_2.isWorkletizableFunctionType)(maybeWorklet)) { (0, workletSubstitution_12.processWorklet)(maybeWorklet, state); + } else if ((0, types_2.isWorkletizableObjectType)(maybeWorklet)) { + (0, objectWorklets_1.processWorkletizableObject)(maybeWorklet, state); } }); } + function findWorklet(arg, acceptWorkletizableFunction, acceptObject) { + if (acceptWorkletizableFunction && (0, types_2.isWorkletizableFunctionType)(arg)) { + return arg; + } + if (acceptObject && (0, types_2.isWorkletizableObjectType)(arg)) { + return arg; + } + if (arg.isReferencedIdentifier() && arg.isIdentifier()) { + return (0, referencedWorklets_1.findReferencedWorklet)(arg, acceptWorkletizableFunction, acceptObject); + } + return void 0; + } } }); diff --git a/packages/react-native-reanimated/plugin/src/autoworkletization.ts b/packages/react-native-reanimated/plugin/src/autoworkletization.ts index 792025391d3..a9e3eaa141d 100644 --- a/packages/react-native-reanimated/plugin/src/autoworkletization.ts +++ b/packages/react-native-reanimated/plugin/src/autoworkletization.ts @@ -1,14 +1,47 @@ import type { NodePath } from '@babel/core'; -import type { CallExpression, ObjectExpression } from '@babel/types'; +import type { CallExpression } from '@babel/types'; import { isSequenceExpression } from '@babel/types'; -import { isWorkletizableFunctionType } from './types'; -import type { ReanimatedPluginPass, WorkletizableFunction } from './types'; -import { strict as assert } from 'assert'; +import { + isWorkletizableFunctionType, + isWorkletizableObjectType, +} from './types'; +import type { + WorkletizableFunction, + WorkletizableObject, + ReanimatedPluginPass, +} from './types'; import { processWorklet } from './workletSubstitution'; import { isGestureHandlerEventCallback } from './gestureHandlerAutoworkletization'; import { isLayoutAnimationCallback } from './layoutAnimationAutoworkletization'; +import { findReferencedWorklet } from './referencedWorklets'; +import { processWorkletizableObject } from './objectWorklets'; + +const objectHooks = new Set([ + 'useAnimatedGestureHandler', + 'useAnimatedScrollHandler', +]); + +const functionHooks = new Set([ + 'useFrameCallback', + 'useAnimatedStyle', + 'useAnimatedProps', + 'createAnimatedPropAdapter', + 'useDerivedValue', + 'useAnimatedScrollHandler', + 'useAnimatedReaction', + 'useWorkletCallback', + // animations' callbacks + 'withTiming', + 'withSpring', + 'withDecay', + 'withRepeat', + // scheduling functions + 'runOnUI', + 'executeOnUIRuntimeSync', +]); const functionArgsToWorkletize = new Map([ + ['useAnimatedGestureHandler', [0]], ['useFrameCallback', [0]], ['useAnimatedStyle', [0]], ['useAnimatedProps', [0]], @@ -17,21 +50,14 @@ const functionArgsToWorkletize = new Map([ ['useAnimatedScrollHandler', [0]], ['useAnimatedReaction', [0, 1]], ['useWorkletCallback', [0]], - // animations' callbacks ['withTiming', [2]], ['withSpring', [2]], ['withDecay', [1]], ['withRepeat', [3]], - // scheduling functions ['runOnUI', [0]], ['executeOnUIRuntimeSync', [0]], ]); -const objectHooks = new Set([ - 'useAnimatedGestureHandler', - 'useAnimatedScrollHandler', -]); - /** * * @returns `true` if the function was workletized, `false` otherwise. @@ -67,65 +93,58 @@ export function processCalleesAutoworkletizableCallbacks( return; } - if (objectHooks.has(name)) { - const maybeWorklet = path.get('arguments.0'); - assert( - !Array.isArray(maybeWorklet), - '[Reanimated] `workletToProcess` is an array.' - ); - if (maybeWorklet.isObjectExpression()) { - processObjectHook(maybeWorklet, state); - // useAnimatedScrollHandler can take a function as an argument instead of an ObjectExpression - // but useAnimatedGestureHandler can't - } else if (name === 'useAnimatedScrollHandler') { - if (isWorkletizableFunctionType(maybeWorklet)) { - processWorklet(maybeWorklet, state); - } - } - } else { - const indices = functionArgsToWorkletize.get(name); - if (indices === undefined) { - return; - } - processArguments(path, indices, state); - } -} + if (functionHooks.has(name) || objectHooks.has(name)) { + const acceptWorkletizableFunction = functionHooks.has(name); + const acceptObject = objectHooks.has(name); + const argIndices = functionArgsToWorkletize.get(name)!; + const args = path + .get('arguments') + .filter((_, index) => argIndices.includes(index)); -function processObjectHook( - path: NodePath, - state: ReanimatedPluginPass -): void { - const properties = path.get('properties'); - for (const property of properties) { - if (property.isObjectMethod()) { - processWorklet(property, state); - } else if (property.isObjectProperty()) { - const value = property.get('value'); - if (isWorkletizableFunctionType(value)) { - processWorklet(value, state); - } - } else { - throw new Error( - `[Reanimated] '${property.type}' as to-be workletized argument is not supported for object hooks.` - ); - } + processArgs(args, state, acceptWorkletizableFunction, acceptObject); } } -function processArguments( - path: NodePath, - indices: number[], - state: ReanimatedPluginPass +function processArgs( + args: NodePath[], + state: ReanimatedPluginPass, + acceptWorkletizableFunction: boolean, + acceptObject: boolean ): void { - const argumentsArray = path.get('arguments'); - indices.forEach((index) => { - const maybeWorklet = argumentsArray[index]; + args.forEach((arg) => { + const maybeWorklet = findWorklet( + arg, + acceptWorkletizableFunction, + acceptObject + ); if (!maybeWorklet) { - // workletizable argument doesn't always have to be specified return; } if (isWorkletizableFunctionType(maybeWorklet)) { processWorklet(maybeWorklet, state); + } else if (isWorkletizableObjectType(maybeWorklet)) { + processWorkletizableObject(maybeWorklet, state); } }); } + +function findWorklet( + arg: NodePath, + acceptWorkletizableFunction: boolean, + acceptObject: boolean +): NodePath | NodePath | undefined { + if (acceptWorkletizableFunction && isWorkletizableFunctionType(arg)) { + return arg; + } + if (acceptObject && isWorkletizableObjectType(arg)) { + return arg; + } + if (arg.isReferencedIdentifier() && arg.isIdentifier()) { + return findReferencedWorklet( + arg, + acceptWorkletizableFunction, + acceptObject + ); + } + return undefined; +} diff --git a/packages/react-native-reanimated/plugin/src/jestMatchers.ts b/packages/react-native-reanimated/plugin/src/jestMatchers.ts index caf3b52f03e..34b8dfbab1c 100644 --- a/packages/react-native-reanimated/plugin/src/jestMatchers.ts +++ b/packages/react-native-reanimated/plugin/src/jestMatchers.ts @@ -7,6 +7,7 @@ declare global { toHaveWorkletData(times?: number): R; toHaveInlineStyleWarning(times?: number): R; toHaveLocation(location: string): R; + toIncludeInWorkletString(expected: string): R; } } } @@ -32,6 +33,7 @@ expect.extend({ pass: false, }; }, + toHaveInlineStyleWarning(received: string, expectedMatchCount = 1) { const receivedMatchCount = received.match(INLINE_STYLE_WARNING_REGEX)?.length || 0; @@ -49,6 +51,7 @@ expect.extend({ pass: false, }; }, + toHaveLocation(received: string, expectedLocation: string) { const expectedString = `location: "${expectedLocation}"`; const hasLocation = received.includes(expectedLocation); @@ -64,4 +67,27 @@ expect.extend({ pass: false, }; }, + + toIncludeInWorkletString(received: string, expected: string) { + // Regular expression pattern to match the code field + const pattern = /code: "((?:[^"\\]|\\.)*)"/s; + const match = received.match(pattern); + + // If a match was found and the match group 1 (content within quotes) includes the expected string + if (match && match[1].includes(expected)) { + // return true; + return { + message: () => `Reanimated: found ${expected} in worklet string`, + pass: true, + }; + } + + // If no match was found or the expected string is not a substring of the code field + // return false; + return { + message: () => + `Reanimated: expected to find ${expected} in worklet string, but it's not present`, + pass: false, + }; + }, }); diff --git a/packages/react-native-reanimated/plugin/src/objectWorklets.ts b/packages/react-native-reanimated/plugin/src/objectWorklets.ts new file mode 100644 index 00000000000..4bb8302ba23 --- /dev/null +++ b/packages/react-native-reanimated/plugin/src/objectWorklets.ts @@ -0,0 +1,25 @@ +import type { NodePath } from '@babel/core'; +import { isWorkletizableFunctionType } from './types'; +import type { WorkletizableObject, ReanimatedPluginPass } from './types'; +import { processWorklet } from './workletSubstitution'; + +export function processWorkletizableObject( + path: NodePath, + state: ReanimatedPluginPass +): void { + const properties = path.get('properties'); + for (const property of properties) { + if (property.isObjectMethod()) { + processWorklet(property, state); + } else if (property.isObjectProperty()) { + const value = property.get('value'); + if (isWorkletizableFunctionType(value)) { + processWorklet(value, state); + } + } else { + throw new Error( + `[Reanimated] '${property.type}' as to-be workletized argument is not supported for object hooks.` + ); + } + } +} diff --git a/packages/react-native-reanimated/plugin/src/referencedWorklets.ts b/packages/react-native-reanimated/plugin/src/referencedWorklets.ts new file mode 100644 index 00000000000..e9d4d52ec0b --- /dev/null +++ b/packages/react-native-reanimated/plugin/src/referencedWorklets.ts @@ -0,0 +1,97 @@ +import type { NodePath } from '@babel/core'; +import type { AssignmentExpression, Identifier } from '@babel/types'; +import { + isWorkletizableFunctionType, + isWorkletizableObjectType, +} from './types'; +import type { WorkletizableFunction, WorkletizableObject } from './types'; +import type { Binding } from '@babel/traverse'; + +export function findReferencedWorklet( + workletIdentifier: NodePath, + acceptWorkletizableFunction: boolean, + acceptObject: boolean +): NodePath | NodePath | undefined { + const workletName = workletIdentifier.node.name; + const scope = workletIdentifier.scope; + + const workletBinding = scope.getBinding(workletName); + if (!workletBinding) { + return undefined; + } + + if ( + acceptWorkletizableFunction && + workletBinding.path.isFunctionDeclaration() + ) { + return workletBinding.path; + } + + const isConstant = workletBinding.constant; + if (isConstant) { + return findReferencedWorkletFromVariableDeclarator( + workletBinding, + acceptWorkletizableFunction, + acceptObject + ); + } + return findReferencedWorkletFromAssignmentExpression( + workletBinding, + acceptWorkletizableFunction, + acceptObject + ); +} + +function findReferencedWorkletFromVariableDeclarator( + workletBinding: Binding, + acceptWorkletizableFunction: boolean, + acceptObject: boolean +): NodePath | NodePath | undefined { + const workletDeclaration = workletBinding.path; + if (!workletDeclaration.isVariableDeclarator()) { + return undefined; + } + const worklet = workletDeclaration.get('init'); + + if (acceptWorkletizableFunction && isWorkletizableFunctionType(worklet)) { + return worklet; + } + if (acceptObject && isWorkletizableObjectType(worklet)) { + return worklet; + } + return undefined; +} + +function findReferencedWorkletFromAssignmentExpression( + workletBinding: Binding, + acceptWorkletizableFunction: boolean, + acceptObject: boolean +): NodePath | NodePath | undefined { + const workletDeclaration = workletBinding.constantViolations + .reverse() + .find( + (constantViolation) => + constantViolation.isAssignmentExpression() && + ((acceptWorkletizableFunction && + isWorkletizableFunctionType(constantViolation.get('right'))) || + (acceptObject && + isWorkletizableObjectType(constantViolation.get('right')))) + ) as NodePath | undefined; + + if (!workletDeclaration || !workletDeclaration.isAssignmentExpression()) { + return undefined; + } + + const workletDefinition = workletDeclaration.get('right'); + + if ( + acceptWorkletizableFunction && + isWorkletizableFunctionType(workletDefinition) + ) { + return workletDefinition; + } + if (acceptObject && isWorkletizableObjectType(workletDefinition)) { + return workletDefinition; + } + return undefined; +} diff --git a/packages/react-native-reanimated/plugin/src/types.ts b/packages/react-native-reanimated/plugin/src/types.ts index a13442c615d..228669f1b87 100644 --- a/packages/react-native-reanimated/plugin/src/types.ts +++ b/packages/react-native-reanimated/plugin/src/types.ts @@ -4,6 +4,7 @@ import type { FunctionExpression, ObjectMethod, ArrowFunctionExpression, + ObjectExpression, } from '@babel/types'; export interface ReanimatedPluginOptions { @@ -35,8 +36,12 @@ export type WorkletizableFunction = export const WorkletizableFunction = 'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ObjectMethod'; +export type WorkletizableObject = ObjectExpression; + +export const WorkletizableObject = 'ObjectExpression'; + export function isWorkletizableFunctionType( - path: NodePath + path: NodePath ): path is NodePath { return ( path.isFunctionDeclaration() || @@ -45,3 +50,9 @@ export function isWorkletizableFunctionType( path.isObjectMethod() ); } + +export function isWorkletizableObjectType( + path: NodePath +): path is NodePath { + return path.isObjectExpression(); +}