diff --git a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx index 97a3a00961d8..9441ed22e199 100644 --- a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx +++ b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx @@ -68,6 +68,12 @@ export default function RuntimeTestsExample() { // require('./tests/advancedAPI/measure.test'); // crash on Android }, }, + { + testSuiteName: 'babelPlugin', + importTest: () => { + require('./tests/plugin/fileWorkletization.test'); + }, + }, { skipByDefault: true, testSuiteName: 'self-tests', diff --git a/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.test.tsx new file mode 100644 index 000000000000..a410df2ca3ee --- /dev/null +++ b/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.test.tsx @@ -0,0 +1,37 @@ +import React, { useEffect } from 'react'; +import { View } from 'react-native'; +import { useSharedValue, runOnUI } from 'react-native-reanimated'; +import { + render, + wait, + describe, + getRegisteredValue, + registerValue, + test, + expect, +} from '../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi'; +import { getThree } from './fileWorkletization'; + +const SHARED_VALUE_REF = 'SHARED_VALUE_REF'; + +describe('Test workletization', () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + + useEffect(() => { + runOnUI(() => { + output.value = getThree(); + })(); + }); + + return ; + }; + + test('Test file workletization', async () => { + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(3); + }); +}); diff --git a/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.ts b/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.ts new file mode 100644 index 000000000000..552d0d445900 --- /dev/null +++ b/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.ts @@ -0,0 +1,15 @@ +'worklet'; + +const getOne = () => { + return 1; +}; + +const getterContainer = { + getTwo: () => { + return 2; + }, +}; + +export const getThree = () => { + return getOne() + getterContainer.getTwo(); +}; 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 e2b4ffe89fb6..70193b42bd78 100644 --- a/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap +++ b/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap @@ -860,6 +860,349 @@ var foo = function () { }();" `; +exports[`babel plugin for file workletization doesn't workletize function outside of top level scope 1`] = ` +"{ + function foo() { + return 'bar'; + } +}" +`; + +exports[`babel plugin for file workletization workletizes ArrowFunctionExpression 1`] = ` +"var _worklet_2420910284744_init_data = { + code: "function null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = function () { + var _e = [new global.Error(), 1, -27]; + var null1 = function null1() { + return 'bar'; + }; + null1.__closure = {}; + null1.__workletHash = 2420910284744; + null1.__initData = _worklet_2420910284744_init_data; + null1.__stackDetails = _e; + return null1; +}();" +`; + +exports[`babel plugin for file workletization workletizes ArrowFunctionExpression in default export 1`] = ` +"Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; +var _worklet_2420910284744_init_data = { + code: "function null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var _default = exports.default = function () { + var _e = [new global.Error(), 1, -27]; + var null1 = function null1() { + return 'bar'; + }; + null1.__closure = {}; + null1.__workletHash = 2420910284744; + null1.__initData = _worklet_2420910284744_init_data; + null1.__stackDetails = _e; + return null1; +}();" +`; + +exports[`babel plugin for file workletization workletizes ArrowFunctionExpression in named export 1`] = ` +"Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.foo = void 0; +var _worklet_2420910284744_init_data = { + code: "function null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = exports.foo = function () { + var _e = [new global.Error(), 1, -27]; + var null1 = function null1() { + return 'bar'; + }; + null1.__closure = {}; + null1.__workletHash = 2420910284744; + null1.__initData = _worklet_2420910284744_init_data; + null1.__stackDetails = _e; + return null1; +}();" +`; + +exports[`babel plugin for file workletization workletizes FunctionDeclaration 1`] = ` +"var _worklet_5253890412305_init_data = { + code: "function foo_null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = function () { + var _e = [new global.Error(), 1, -27]; + var foo_null1 = function foo_null1() { + return 'bar'; + }; + foo_null1.__closure = {}; + foo_null1.__workletHash = 5253890412305; + foo_null1.__initData = _worklet_5253890412305_init_data; + foo_null1.__stackDetails = _e; + return foo_null1; +}();" +`; + +exports[`babel plugin for file workletization workletizes FunctionDeclaration in default export 1`] = ` +"Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; +var _worklet_5253890412305_init_data = { + code: "function foo_null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var _default = exports.default = function () { + var _e = [new global.Error(), 1, -27]; + var foo_null1 = function foo_null1() { + return 'bar'; + }; + foo_null1.__closure = {}; + foo_null1.__workletHash = 5253890412305; + foo_null1.__initData = _worklet_5253890412305_init_data; + foo_null1.__stackDetails = _e; + return foo_null1; +}();" +`; + +exports[`babel plugin for file workletization workletizes FunctionDeclaration in named export 1`] = ` +"Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.foo = void 0; +var _worklet_5253890412305_init_data = { + code: "function foo_null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = exports.foo = function () { + var _e = [new global.Error(), 1, -27]; + var foo_null1 = function foo_null1() { + return 'bar'; + }; + foo_null1.__closure = {}; + foo_null1.__workletHash = 5253890412305; + foo_null1.__initData = _worklet_5253890412305_init_data; + foo_null1.__stackDetails = _e; + return foo_null1; +}();" +`; + +exports[`babel plugin for file workletization workletizes FunctionExpression 1`] = ` +"var _worklet_2420910284744_init_data = { + code: "function null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = function () { + var _e = [new global.Error(), 1, -27]; + var null1 = function null1() { + return 'bar'; + }; + null1.__closure = {}; + null1.__workletHash = 2420910284744; + null1.__initData = _worklet_2420910284744_init_data; + null1.__stackDetails = _e; + return null1; +}();" +`; + +exports[`babel plugin for file workletization workletizes FunctionExpression in default export 1`] = ` +"Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; +var _worklet_2420910284744_init_data = { + code: "function null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var _default = exports.default = function () { + var _e = [new global.Error(), 1, -27]; + var null1 = function null1() { + return 'bar'; + }; + null1.__closure = {}; + null1.__workletHash = 2420910284744; + null1.__initData = _worklet_2420910284744_init_data; + null1.__stackDetails = _e; + return null1; +}();" +`; + +exports[`babel plugin for file workletization workletizes FunctionExpression in named export 1`] = ` +"Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.foo = void 0; +var _worklet_2420910284744_init_data = { + code: "function null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = exports.foo = function () { + var _e = [new global.Error(), 1, -27]; + var null1 = function null1() { + return 'bar'; + }; + null1.__closure = {}; + null1.__workletHash = 2420910284744; + null1.__initData = _worklet_2420910284744_init_data; + null1.__stackDetails = _e; + return null1; +}();" +`; + +exports[`babel plugin for file workletization workletizes ObjectMethod 1`] = ` +"var _worklet_17582834634406_init_data = { + code: "function bar_null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = { + bar: function () { + var _e = [new global.Error(), 1, -27]; + var bar_null1 = function bar_null1() { + return 'bar'; + }; + bar_null1.__closure = {}; + bar_null1.__workletHash = 17582834634406; + bar_null1.__initData = _worklet_17582834634406_init_data; + bar_null1.__stackDetails = _e; + return bar_null1; + }() +};" +`; + +exports[`babel plugin for file workletization workletizes ObjectMethod in default export 1`] = ` +"Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; +var _worklet_17582834634406_init_data = { + code: "function bar_null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var _default = exports.default = { + bar: function () { + var _e = [new global.Error(), 1, -27]; + var bar_null1 = function bar_null1() { + return 'bar'; + }; + bar_null1.__closure = {}; + bar_null1.__workletHash = 17582834634406; + bar_null1.__initData = _worklet_17582834634406_init_data; + bar_null1.__stackDetails = _e; + return bar_null1; + }() +};" +`; + +exports[`babel plugin for file workletization workletizes ObjectMethod in named export 1`] = ` +"Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.foo = void 0; +var _worklet_17582834634406_init_data = { + code: "function bar_null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = exports.foo = { + bar: function () { + var _e = [new global.Error(), 1, -27]; + var bar_null1 = function bar_null1() { + return 'bar'; + }; + bar_null1.__closure = {}; + bar_null1.__workletHash = 17582834634406; + bar_null1.__initData = _worklet_17582834634406_init_data; + bar_null1.__stackDetails = _e; + return bar_null1; + }() +};" +`; + +exports[`babel plugin for file workletization workletizes assigned FunctionDeclaration 1`] = ` +"var _worklet_5253890412305_init_data = { + code: "function foo_null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = function () { + var _e = [new global.Error(), 1, -27]; + var foo_null1 = function foo_null1() { + return 'bar'; + }; + foo_null1.__closure = {}; + foo_null1.__workletHash = 5253890412305; + foo_null1.__initData = _worklet_5253890412305_init_data; + foo_null1.__stackDetails = _e; + return foo_null1; +}();" +`; + +exports[`babel plugin for file workletization workletizes multiple functions 1`] = ` +"var _worklet_5253890412305_init_data = { + code: "function foo_null1(){return'bar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = function () { + var _e = [new global.Error(), 1, -27]; + var foo_null1 = function foo_null1() { + return 'bar'; + }; + foo_null1.__closure = {}; + foo_null1.__workletHash = 5253890412305; + foo_null1.__initData = _worklet_5253890412305_init_data; + foo_null1.__stackDetails = _e; + return foo_null1; +}(); +var _worklet_17530869641357_init_data = { + code: "function null2(){return'foobar';}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var bar = function () { + var _e = [new global.Error(), 1, -27]; + var null2 = function null2() { + return 'foobar'; + }; + null2.__closure = {}; + null2.__workletHash = 17530869641357; + null2.__initData = _worklet_17530869641357_init_data; + null2.__stackDetails = _e; + return null2; +}();" +`; + exports[`babel plugin for function hooks workletizes hook wrapped ArrowFunctionExpression automatically 1`] = ` "var _worklet_1392490775014_init_data = { code: "function null1(){return{width:50};}", diff --git a/packages/react-native-reanimated/__tests__/plugin.test.ts b/packages/react-native-reanimated/__tests__/plugin.test.ts index 3b176f1c9928..756ba2c072a5 100644 --- a/packages/react-native-reanimated/__tests__/plugin.test.ts +++ b/packages/react-native-reanimated/__tests__/plugin.test.ts @@ -2066,4 +2066,220 @@ describe('babel plugin', () => { expect(code).toMatchSnapshot(); }); }); + + describe('for file workletization', () => { + it('workletizes FunctionDeclaration', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + + it('workletizes assigned FunctionDeclaration', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + + it('workletizes FunctionDeclaration in named export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toContain('exports.foo = function'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes FunctionDeclaration in default export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toContain('exports.default = function'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes FunctionExpression', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + + it('workletizes FunctionExpression in named export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toContain('exports.foo = function'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes FunctionExpression in default export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toContain('exports.default = function'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ArrowFunctionExpression', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ArrowFunctionExpression in named export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toContain('exports.foo = function'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ArrowFunctionExpression in default export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toContain('exports.default = function'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ObjectMethod', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ObjectMethod in named export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toContain('exports.foo = {'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ObjectMethod in default export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toContain('exports.default = {'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes multiple functions', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(2); + expect(code).toMatchSnapshot(); + }); + + it("doesn't workletize function outside of top level scope", () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).not.toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + }); }); diff --git a/packages/react-native-reanimated/plugin/build/plugin.js b/packages/react-native-reanimated/plugin/build/plugin.js index c3b41e185334..fdb7513fc6ad 100644 --- a/packages/react-native-reanimated/plugin/build/plugin.js +++ b/packages/react-native-reanimated/plugin/build/plugin.js @@ -30,17 +30,26 @@ var require_types = __commonJS({ "lib/types.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.isWorkletizableObjectType = exports2.isWorkletizableFunctionType = exports2.WorkletizableObject = exports2.WorkletizableFunction = void 0; + exports2.isWorkletizableObjectNode = exports2.isWorkletizableObjectPath = exports2.isWorkletizableFunctionNode = exports2.isWorkletizableFunctionPath = exports2.WorkletizableObject = exports2.WorkletizableFunction = void 0; + var types_12 = require("@babel/types"); exports2.WorkletizableFunction = "FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ObjectMethod"; exports2.WorkletizableObject = "ObjectExpression"; - function isWorkletizableFunctionType(path) { + function isWorkletizableFunctionPath(path) { return path.isFunctionDeclaration() || path.isFunctionExpression() || path.isArrowFunctionExpression() || path.isObjectMethod(); } - exports2.isWorkletizableFunctionType = isWorkletizableFunctionType; - function isWorkletizableObjectType(path) { + exports2.isWorkletizableFunctionPath = isWorkletizableFunctionPath; + function isWorkletizableFunctionNode(node) { + return (0, types_12.isFunctionDeclaration)(node) || (0, types_12.isFunctionExpression)(node) || (0, types_12.isArrowFunctionExpression)(node) || (0, types_12.isObjectMethod)(node); + } + exports2.isWorkletizableFunctionNode = isWorkletizableFunctionNode; + function isWorkletizableObjectPath(path) { return path.isObjectExpression(); } - exports2.isWorkletizableObjectType = isWorkletizableObjectType; + exports2.isWorkletizableObjectPath = isWorkletizableObjectPath; + function isWorkletizableObjectNode(node) { + return (0, types_12.isObjectExpression)(node); + } + exports2.isWorkletizableObjectNode = isWorkletizableObjectNode; } }); @@ -878,24 +887,24 @@ var require_referencedWorklets = __commonJS({ return void 0; } const worklet = workletDeclaration.get("init"); - if (acceptWorkletizableFunction && (0, types_12.isWorkletizableFunctionType)(worklet)) { + if (acceptWorkletizableFunction && (0, types_12.isWorkletizableFunctionPath)(worklet)) { return worklet; } - if (acceptObject && (0, types_12.isWorkletizableObjectType)(worklet)) { + if (acceptObject && (0, types_12.isWorkletizableObjectPath)(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")))); + const workletDeclaration = workletBinding.constantViolations.reverse().find((constantViolation) => constantViolation.isAssignmentExpression() && (acceptWorkletizableFunction && (0, types_12.isWorkletizableFunctionPath)(constantViolation.get("right")) || acceptObject && (0, types_12.isWorkletizableObjectPath)(constantViolation.get("right")))); if (!workletDeclaration || !workletDeclaration.isAssignmentExpression()) { return void 0; } const workletDefinition = workletDeclaration.get("right"); - if (acceptWorkletizableFunction && (0, types_12.isWorkletizableFunctionType)(workletDefinition)) { + if (acceptWorkletizableFunction && (0, types_12.isWorkletizableFunctionPath)(workletDefinition)) { return workletDefinition; } - if (acceptObject && (0, types_12.isWorkletizableObjectType)(workletDefinition)) { + if (acceptObject && (0, types_12.isWorkletizableObjectPath)(workletDefinition)) { return workletDefinition; } return void 0; @@ -918,7 +927,7 @@ var require_objectWorklets = __commonJS({ (0, workletSubstitution_12.processWorklet)(property, state); } else if (property.isObjectProperty()) { const value = property.get("value"); - if ((0, types_12.isWorkletizableFunctionType)(value)) { + if ((0, types_12.isWorkletizableFunctionPath)(value)) { (0, workletSubstitution_12.processWorklet)(value, state); } } else { @@ -1009,18 +1018,18 @@ var require_autoworkletization = __commonJS({ if (!maybeWorklet) { return; } - if ((0, types_2.isWorkletizableFunctionType)(maybeWorklet)) { + if ((0, types_2.isWorkletizableFunctionPath)(maybeWorklet)) { (0, workletSubstitution_12.processWorklet)(maybeWorklet, state); - } else if ((0, types_2.isWorkletizableObjectType)(maybeWorklet)) { + } else if ((0, types_2.isWorkletizableObjectPath)(maybeWorklet)) { (0, objectWorklets_1.processWorkletizableObject)(maybeWorklet, state); } }); } function findWorklet(arg, acceptWorkletizableFunction, acceptObject) { - if (acceptWorkletizableFunction && (0, types_2.isWorkletizableFunctionType)(arg)) { + if (acceptWorkletizableFunction && (0, types_2.isWorkletizableFunctionPath)(arg)) { return arg; } - if (acceptObject && (0, types_2.isWorkletizableObjectType)(arg)) { + if (acceptObject && (0, types_2.isWorkletizableObjectPath)(arg)) { return arg; } if (arg.isReferencedIdentifier() && arg.isIdentifier()) { @@ -1132,6 +1141,82 @@ var require_webOptimization = __commonJS({ } }); +// lib/file.js +var require_file = __commonJS({ + "lib/file.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.processIfWorkletFile = void 0; + var types_12 = require("@babel/types"); + var types_2 = require_types(); + function processIfWorkletFile(path, state) { + if (!path.node.directives.some((functionDirective) => functionDirective.value.value === "worklet")) { + return false; + } + processWorkletFile(path, state); + path.node.directives = path.node.directives.filter((functionDirective) => functionDirective.value.value !== "worklet"); + return true; + } + exports2.processIfWorkletFile = processIfWorkletFile; + function processWorkletFile(path, _state) { + path.node.body.forEach((statement) => { + const candidate = getNodeCandidate(statement); + if (candidate === null || candidate === void 0) { + return; + } + processWorkletizableEntity(candidate); + }); + } + function getNodeCandidate(statement) { + if ((0, types_12.isExportNamedDeclaration)(statement) || (0, types_12.isExportDefaultDeclaration)(statement)) { + return statement.declaration; + } else { + return statement; + } + } + function processWorkletizableEntity(node) { + if ((0, types_2.isWorkletizableFunctionNode)(node)) { + if ((0, types_12.isArrowFunctionExpression)(node)) { + replaceImplicitReturnWithBlock(node); + } + appendWorkletDirective(node.body); + } else if ((0, types_2.isWorkletizableObjectNode)(node)) { + processObjectExpression(node); + } else if ((0, types_12.isVariableDeclaration)(node)) { + processVariableDeclaration(node); + } + } + function processVariableDeclaration(variableDeclaration) { + variableDeclaration.declarations.forEach((declaration) => { + const init = declaration.init; + if ((0, types_12.isExpression)(init)) { + processWorkletizableEntity(init); + } + }); + } + function processObjectExpression(object) { + object.properties.forEach((property) => { + if (property.type === "ObjectMethod") { + appendWorkletDirective(property.body); + } else if (property.type === "ObjectProperty") { + const value = property.value; + processWorkletizableEntity(value); + } + }); + } + function replaceImplicitReturnWithBlock(path) { + if (!(0, types_12.isBlockStatement)(path.body)) { + path.body = (0, types_12.blockStatement)([(0, types_12.returnStatement)(path.body)]); + } + } + function appendWorkletDirective(node) { + if (!node.directives.some((functionDirective) => functionDirective.value.value === "worklet")) { + node.directives.push((0, types_12.directive)((0, types_12.directiveLiteral)("worklet"))); + } + } + } +}); + // lib/plugin.js Object.defineProperty(exports, "__esModule", { value: true }); var autoworkletization_1 = require_autoworkletization(); @@ -1141,6 +1226,7 @@ var inlineStylesWarning_1 = require_inlineStylesWarning(); var utils_1 = require_utils(); var globals_1 = require_globals(); var webOptimization_1 = require_webOptimization(); +var file_1 = require_file(); module.exports = function() { function runWithTaggedExceptions(fun) { try { @@ -1176,9 +1262,10 @@ module.exports = function() { } }, Program: { - enter(_path, state) { + enter(path, state) { runWithTaggedExceptions(() => { state.workletNumber = 1; + (0, file_1.processIfWorkletFile)(path, state); }); } }, diff --git a/packages/react-native-reanimated/plugin/src/autoworkletization.ts b/packages/react-native-reanimated/plugin/src/autoworkletization.ts index a9e3eaa141da..d54cefadaee2 100644 --- a/packages/react-native-reanimated/plugin/src/autoworkletization.ts +++ b/packages/react-native-reanimated/plugin/src/autoworkletization.ts @@ -2,8 +2,8 @@ import type { NodePath } from '@babel/core'; import type { CallExpression } from '@babel/types'; import { isSequenceExpression } from '@babel/types'; import { - isWorkletizableFunctionType, - isWorkletizableObjectType, + isWorkletizableFunctionPath, + isWorkletizableObjectPath, } from './types'; import type { WorkletizableFunction, @@ -120,9 +120,9 @@ function processArgs( if (!maybeWorklet) { return; } - if (isWorkletizableFunctionType(maybeWorklet)) { + if (isWorkletizableFunctionPath(maybeWorklet)) { processWorklet(maybeWorklet, state); - } else if (isWorkletizableObjectType(maybeWorklet)) { + } else if (isWorkletizableObjectPath(maybeWorklet)) { processWorkletizableObject(maybeWorklet, state); } }); @@ -133,10 +133,10 @@ function findWorklet( acceptWorkletizableFunction: boolean, acceptObject: boolean ): NodePath | NodePath | undefined { - if (acceptWorkletizableFunction && isWorkletizableFunctionType(arg)) { + if (acceptWorkletizableFunction && isWorkletizableFunctionPath(arg)) { return arg; } - if (acceptObject && isWorkletizableObjectType(arg)) { + if (acceptObject && isWorkletizableObjectPath(arg)) { return arg; } if (arg.isReferencedIdentifier() && arg.isIdentifier()) { diff --git a/packages/react-native-reanimated/plugin/src/file.ts b/packages/react-native-reanimated/plugin/src/file.ts new file mode 100644 index 000000000000..e51da1a2c618 --- /dev/null +++ b/packages/react-native-reanimated/plugin/src/file.ts @@ -0,0 +1,131 @@ +import { + blockStatement, + directive, + directiveLiteral, + isArrowFunctionExpression, + isBlockStatement, + isExportDefaultDeclaration, + isExportNamedDeclaration, + isExpression, + isVariableDeclaration, + returnStatement, +} from '@babel/types'; + +import type { + Program, + BlockStatement, + VariableDeclaration, + ArrowFunctionExpression, + ObjectExpression, + Statement, + Node as BabelNode, +} from '@babel/types'; +import type { NodePath } from '@babel/core'; +import { + isWorkletizableFunctionNode, + isWorkletizableObjectNode, +} from './types'; +import type { ReanimatedPluginPass } from './types'; + +export function processIfWorkletFile( + path: NodePath, + state: ReanimatedPluginPass +): boolean { + if ( + !path.node.directives.some( + (functionDirective) => functionDirective.value.value === 'worklet' + ) + ) { + return false; + } + + processWorkletFile(path, state); + // Remove 'worklet' directive from the file afterwards. + path.node.directives = path.node.directives.filter( + (functionDirective) => functionDirective.value.value !== 'worklet' + ); + return true; +} + +/** + * Adds a worklet directive to each viable top-level entity in the file. + */ +function processWorkletFile( + path: NodePath, + _state: ReanimatedPluginPass +) { + path.node.body.forEach((statement) => { + const candidate = getNodeCandidate(statement); + if (candidate === null || candidate === undefined) { + return; + } + processWorkletizableEntity(candidate); + }); +} + +function getNodeCandidate(statement: Statement) { + if ( + isExportNamedDeclaration(statement) || + isExportDefaultDeclaration(statement) + ) { + return statement.declaration; + } else { + return statement; + } +} + +function processWorkletizableEntity(node: BabelNode) { + if (isWorkletizableFunctionNode(node)) { + if (isArrowFunctionExpression(node)) { + replaceImplicitReturnWithBlock(node); + } + appendWorkletDirective(node.body as BlockStatement); + } else if (isWorkletizableObjectNode(node)) { + processObjectExpression(node); + } else if (isVariableDeclaration(node)) { + processVariableDeclaration(node); + } +} + +function processVariableDeclaration(variableDeclaration: VariableDeclaration) { + variableDeclaration.declarations.forEach((declaration) => { + const init = declaration.init; + if (isExpression(init)) { + processWorkletizableEntity(init); + } + }); +} + +function processObjectExpression(object: ObjectExpression) { + object.properties.forEach((property) => { + if (property.type === 'ObjectMethod') { + appendWorkletDirective(property.body); + } else if (property.type === 'ObjectProperty') { + const value = property.value; + processWorkletizableEntity(value); + } + }); +} + +/** + * Replaces implicit return statements with a block statement i.e.: + * + * `() => 1` becomes `() => { return 1 }` + * + * This is necessary because the worklet directive is only allowed on block statements. + */ +function replaceImplicitReturnWithBlock(path: ArrowFunctionExpression) { + if (!isBlockStatement(path.body)) { + path.body = blockStatement([returnStatement(path.body)]); + } +} + +function appendWorkletDirective(node: BlockStatement) { + if ( + !node.directives.some( + (functionDirective) => functionDirective.value.value === 'worklet' + ) + ) { + node.directives.push(directive(directiveLiteral('worklet'))); + } +} diff --git a/packages/react-native-reanimated/plugin/src/objectWorklets.ts b/packages/react-native-reanimated/plugin/src/objectWorklets.ts index 4bb8302ba23c..0c934dddeb60 100644 --- a/packages/react-native-reanimated/plugin/src/objectWorklets.ts +++ b/packages/react-native-reanimated/plugin/src/objectWorklets.ts @@ -1,5 +1,5 @@ import type { NodePath } from '@babel/core'; -import { isWorkletizableFunctionType } from './types'; +import { isWorkletizableFunctionPath } from './types'; import type { WorkletizableObject, ReanimatedPluginPass } from './types'; import { processWorklet } from './workletSubstitution'; @@ -13,7 +13,7 @@ export function processWorkletizableObject( processWorklet(property, state); } else if (property.isObjectProperty()) { const value = property.get('value'); - if (isWorkletizableFunctionType(value)) { + if (isWorkletizableFunctionPath(value)) { processWorklet(value, state); } } else { diff --git a/packages/react-native-reanimated/plugin/src/plugin.ts b/packages/react-native-reanimated/plugin/src/plugin.ts index 8bc1acece13e..426d08105f55 100644 --- a/packages/react-native-reanimated/plugin/src/plugin.ts +++ b/packages/react-native-reanimated/plugin/src/plugin.ts @@ -1,5 +1,5 @@ import type { PluginItem, NodePath } from '@babel/core'; -import type { CallExpression } from '@babel/types'; +import type { CallExpression, JSXAttribute, Program } from '@babel/types'; import { processIfAutoworkletizableCallback, processCalleesAutoworkletizableCallbacks, @@ -11,6 +11,7 @@ import { processInlineStylesWarning } from './inlineStylesWarning'; import { addCustomGlobals } from './utils'; import { initializeGlobals } from './globals'; import { substituteWebCallExpression } from './webOptimization'; +import { processIfWorkletFile } from './file'; module.exports = function (): PluginItem { function runWithTaggedExceptions(fun: () => void) { @@ -53,15 +54,16 @@ module.exports = function (): PluginItem { }, }, Program: { - enter(_path, state) { + enter(path: NodePath, state: ReanimatedPluginPass) { runWithTaggedExceptions(() => { // Reset worklet number. state.workletNumber = 1; + processIfWorkletFile(path, state); }); }, }, JSXAttribute: { - enter(path, state) { + enter(path: NodePath, state: ReanimatedPluginPass) { runWithTaggedExceptions(() => processInlineStylesWarning(path, state) ); diff --git a/packages/react-native-reanimated/plugin/src/referencedWorklets.ts b/packages/react-native-reanimated/plugin/src/referencedWorklets.ts index e9d4d52ec0b5..2ad72575449b 100644 --- a/packages/react-native-reanimated/plugin/src/referencedWorklets.ts +++ b/packages/react-native-reanimated/plugin/src/referencedWorklets.ts @@ -1,8 +1,8 @@ import type { NodePath } from '@babel/core'; import type { AssignmentExpression, Identifier } from '@babel/types'; import { - isWorkletizableFunctionType, - isWorkletizableObjectType, + isWorkletizableFunctionPath, + isWorkletizableObjectPath, } from './types'; import type { WorkletizableFunction, WorkletizableObject } from './types'; import type { Binding } from '@babel/traverse'; @@ -53,10 +53,10 @@ function findReferencedWorkletFromVariableDeclarator( } const worklet = workletDeclaration.get('init'); - if (acceptWorkletizableFunction && isWorkletizableFunctionType(worklet)) { + if (acceptWorkletizableFunction && isWorkletizableFunctionPath(worklet)) { return worklet; } - if (acceptObject && isWorkletizableObjectType(worklet)) { + if (acceptObject && isWorkletizableObjectPath(worklet)) { return worklet; } return undefined; @@ -73,9 +73,9 @@ function findReferencedWorkletFromAssignmentExpression( (constantViolation) => constantViolation.isAssignmentExpression() && ((acceptWorkletizableFunction && - isWorkletizableFunctionType(constantViolation.get('right'))) || + isWorkletizableFunctionPath(constantViolation.get('right'))) || (acceptObject && - isWorkletizableObjectType(constantViolation.get('right')))) + isWorkletizableObjectPath(constantViolation.get('right')))) ) as NodePath | undefined; if (!workletDeclaration || !workletDeclaration.isAssignmentExpression()) { @@ -86,11 +86,11 @@ function findReferencedWorkletFromAssignmentExpression( if ( acceptWorkletizableFunction && - isWorkletizableFunctionType(workletDefinition) + isWorkletizableFunctionPath(workletDefinition) ) { return workletDefinition; } - if (acceptObject && isWorkletizableObjectType(workletDefinition)) { + if (acceptObject && isWorkletizableObjectPath(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 9403577fdbac..d6e3a99de4fe 100644 --- a/packages/react-native-reanimated/plugin/src/types.ts +++ b/packages/react-native-reanimated/plugin/src/types.ts @@ -1,10 +1,18 @@ import type { BabelFile, NodePath } from '@babel/core'; +import { + isArrowFunctionExpression, + isFunctionDeclaration, + isFunctionExpression, + isObjectMethod, + isObjectExpression, +} from '@babel/types'; import type { FunctionDeclaration, FunctionExpression, ObjectMethod, ArrowFunctionExpression, ObjectExpression, + Node as BabelNode, } from '@babel/types'; export interface ReanimatedPluginOptions { @@ -42,7 +50,7 @@ export type WorkletizableObject = ObjectExpression; export const WorkletizableObject = 'ObjectExpression'; -export function isWorkletizableFunctionType( +export function isWorkletizableFunctionPath( path: NodePath ): path is NodePath { return ( @@ -53,8 +61,25 @@ export function isWorkletizableFunctionType( ); } -export function isWorkletizableObjectType( +export function isWorkletizableFunctionNode( + node: BabelNode | null | undefined +): node is WorkletizableFunction { + return ( + isFunctionDeclaration(node) || + isFunctionExpression(node) || + isArrowFunctionExpression(node) || + isObjectMethod(node) + ); +} + +export function isWorkletizableObjectPath( path: NodePath ): path is NodePath { return path.isObjectExpression(); } + +export function isWorkletizableObjectNode( + node: BabelNode | null | undefined +): node is WorkletizableObject { + return isObjectExpression(node); +}