From 4c2e509b3ca1c09525babbe01acf44b543468074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Wed, 17 Jul 2024 15:58:06 +0200 Subject: [PATCH] feat: allow implicit detection of context objects --- .../RuntimeTests/RuntimeTestsExample.tsx | 2 +- .../tests/plugin/contextObjects.test.tsx | 12 +- .../tests/plugin/fileWorkletization.test.tsx | 46 +++-- .../tests/plugin/fileWorkletization.ts | 9 + .../__snapshots__/plugin.test.ts.snap | 185 ++++++++++++++---- .../__tests__/plugin.test.ts | 69 ++++++- .../plugin/build/plugin.js | 152 ++++++++------ .../plugin/src/contextObject.ts | 71 ++++--- .../plugin/src/file.ts | 137 +++++++++---- .../react-native-reanimated/src/shareables.ts | 9 +- 10 files changed, 487 insertions(+), 205 deletions(-) diff --git a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx index d40620e0b57..314777f2a9a 100644 --- a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx +++ b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx @@ -69,7 +69,7 @@ export default function RuntimeTestsExample() { }, }, { - testSuiteName: 'babelPlugin', + testSuiteName: 'babel plugin', importTest: () => { require('./tests/plugin/fileWorkletization.test'); require('./tests/plugin/contextObjects.test'); diff --git a/apps/common-app/src/examples/RuntimeTests/tests/plugin/contextObjects.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/plugin/contextObjects.test.tsx index be42d72bcbf..68af3cb1752 100644 --- a/apps/common-app/src/examples/RuntimeTests/tests/plugin/contextObjects.test.tsx +++ b/apps/common-app/src/examples/RuntimeTests/tests/plugin/contextObjects.test.tsx @@ -22,7 +22,7 @@ describe('Test context objects', () => { foo() { return 1; }, - __workletObject: true, + __workletContextObject: true, }; useEffect(() => { @@ -45,7 +45,7 @@ describe('Test context objects', () => { registerValue(SHARED_VALUE_REF, output); const contextObject = { foo: () => 1, - __workletObject: true, + __workletContextObject: true, }; useEffect(() => { @@ -73,7 +73,7 @@ describe('Test context objects', () => { bar() { return this.foo() + 1; }, - __workletObject: true, + __workletContextObject: true, }; useEffect(() => { @@ -101,7 +101,7 @@ describe('Test context objects', () => { bar() { return this.foo.call(contextObject) + 1; }, - __workletObject: true, + __workletContextObject: true, }; useEffect(() => { @@ -127,7 +127,7 @@ describe('Test context objects', () => { bar() { this.foo += 1; }, - __workletObject: true, + __workletContextObject: true, }; useEffect(() => { @@ -154,7 +154,7 @@ describe('Test context objects', () => { bar() { this.foo += 1; }, - __workletObject: true, + __workletContextObject: true, }; useEffect(() => { 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 index a410df2ca3e..56436a39e26 100644 --- a/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.test.tsx +++ b/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.test.tsx @@ -10,28 +10,46 @@ import { test, expect, } from '../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi'; -import { getThree } from './fileWorkletization'; +import { getThree, implicitContextObject } from './fileWorkletization'; const SHARED_VALUE_REF = 'SHARED_VALUE_REF'; -describe('Test workletization', () => { - const ExampleComponent = () => { - const output = useSharedValue(null); - registerValue(SHARED_VALUE_REF, output); +describe('Test file workletization', () => { + test('Functions and methods are workletized', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); - useEffect(() => { - runOnUI(() => { - output.value = getThree(); - })(); - }); + useEffect(() => { + runOnUI(() => { + output.value = getThree(); + })(); + }); - return ; - }; - - test('Test file workletization', async () => { + return ; + }; await render(); await wait(100); const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); expect(sharedValue.onUI).toBe(3); }); + + test('WorkletContextObjects are workletized', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + + useEffect(() => { + runOnUI(() => { + output.value = implicitContextObject.getFive(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(5); + }); }); diff --git a/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.ts b/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.ts index 552d0d44590..01f1219a8cf 100644 --- a/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.ts +++ b/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.ts @@ -13,3 +13,12 @@ const getterContainer = { export const getThree = () => { return getOne() + getterContainer.getTwo(); }; + +export const implicitContextObject = { + getFour() { + return 4; + }, + getFive() { + return this.getFour() + 1; + }, +}; 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 ddc16a51ecf..f9a32cda80c 100644 --- a/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap +++ b/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap @@ -709,8 +709,8 @@ var f = function () { `; exports[`babel plugin for context objects creates factories 1`] = ` -"var _worklet_14630842371699_init_data = { - code: "function __workletObjectFactory_null1(){return{bar:function(){return'bar';}};}", +"var _worklet_9226058452652_init_data = { + code: "function __workletContextObjectFactory_null1(){return{bar:function(){return'bar';}};}", location: "/dev/null", sourceMap: "\\"mock source map\\"", version: "x.y.z" @@ -719,27 +719,27 @@ var foo = { bar: function bar() { return 'bar'; }, - __workletObjectFactory: function () { + __workletContextObjectFactory: function () { var _e = [new global.Error(), 1, -27]; - var __workletObjectFactory_null1 = function __workletObjectFactory_null1() { + var __workletContextObjectFactory_null1 = function __workletContextObjectFactory_null1() { return { bar: function bar() { return 'bar'; } }; }; - __workletObjectFactory_null1.__closure = {}; - __workletObjectFactory_null1.__workletHash = 14630842371699; - __workletObjectFactory_null1.__initData = _worklet_14630842371699_init_data; - __workletObjectFactory_null1.__stackDetails = _e; - return __workletObjectFactory_null1; + __workletContextObjectFactory_null1.__closure = {}; + __workletContextObjectFactory_null1.__workletHash = 9226058452652; + __workletContextObjectFactory_null1.__initData = _worklet_9226058452652_init_data; + __workletContextObjectFactory_null1.__stackDetails = _e; + return __workletContextObjectFactory_null1; }() };" `; exports[`babel plugin for context objects preserves bindings 1`] = ` -"var _worklet_13432710970622_init_data = { - code: "function __workletObjectFactory_null1(){return{bar:function(){return'bar';},foobar:function(){return this.bar();}};}", +"var _worklet_4592588545601_init_data = { + code: "function __workletContextObjectFactory_null1(){return{bar:function(){return'bar';},foobar:function(){return this.bar();}};}", location: "/dev/null", sourceMap: "\\"mock source map\\"", version: "x.y.z" @@ -751,9 +751,9 @@ var foo = { foobar: function foobar() { return this.bar(); }, - __workletObjectFactory: function () { + __workletContextObjectFactory: function () { var _e = [new global.Error(), 1, -27]; - var __workletObjectFactory_null1 = function __workletObjectFactory_null1() { + var __workletContextObjectFactory_null1 = function __workletContextObjectFactory_null1() { return { bar: function bar() { return 'bar'; @@ -763,18 +763,18 @@ var foo = { } }; }; - __workletObjectFactory_null1.__closure = {}; - __workletObjectFactory_null1.__workletHash = 13432710970622; - __workletObjectFactory_null1.__initData = _worklet_13432710970622_init_data; - __workletObjectFactory_null1.__stackDetails = _e; - return __workletObjectFactory_null1; + __workletContextObjectFactory_null1.__closure = {}; + __workletContextObjectFactory_null1.__workletHash = 4592588545601; + __workletContextObjectFactory_null1.__initData = _worklet_4592588545601_init_data; + __workletContextObjectFactory_null1.__stackDetails = _e; + return __workletContextObjectFactory_null1; }() };" `; exports[`babel plugin for context objects removes marker 1`] = ` -"var _worklet_14630842371699_init_data = { - code: "function __workletObjectFactory_null1(){return{bar:function(){return'bar';}};}", +"var _worklet_9226058452652_init_data = { + code: "function __workletContextObjectFactory_null1(){return{bar:function(){return'bar';}};}", location: "/dev/null", sourceMap: "\\"mock source map\\"", version: "x.y.z" @@ -783,27 +783,27 @@ var foo = { bar: function bar() { return 'bar'; }, - __workletObjectFactory: function () { + __workletContextObjectFactory: function () { var _e = [new global.Error(), 1, -27]; - var __workletObjectFactory_null1 = function __workletObjectFactory_null1() { + var __workletContextObjectFactory_null1 = function __workletContextObjectFactory_null1() { return { bar: function bar() { return 'bar'; } }; }; - __workletObjectFactory_null1.__closure = {}; - __workletObjectFactory_null1.__workletHash = 14630842371699; - __workletObjectFactory_null1.__initData = _worklet_14630842371699_init_data; - __workletObjectFactory_null1.__stackDetails = _e; - return __workletObjectFactory_null1; + __workletContextObjectFactory_null1.__closure = {}; + __workletContextObjectFactory_null1.__workletHash = 9226058452652; + __workletContextObjectFactory_null1.__initData = _worklet_9226058452652_init_data; + __workletContextObjectFactory_null1.__stackDetails = _e; + return __workletContextObjectFactory_null1; }() };" `; exports[`babel plugin for context objects workletizes regardless of marker value 1`] = ` -"var _worklet_14630842371699_init_data = { - code: "function __workletObjectFactory_null1(){return{bar:function(){return'bar';}};}", +"var _worklet_9226058452652_init_data = { + code: "function __workletContextObjectFactory_null1(){return{bar:function(){return'bar';}};}", location: "/dev/null", sourceMap: "\\"mock source map\\"", version: "x.y.z" @@ -812,20 +812,20 @@ var foo = { bar: function bar() { return 'bar'; }, - __workletObjectFactory: function () { + __workletContextObjectFactory: function () { var _e = [new global.Error(), 1, -27]; - var __workletObjectFactory_null1 = function __workletObjectFactory_null1() { + var __workletContextObjectFactory_null1 = function __workletContextObjectFactory_null1() { return { bar: function bar() { return 'bar'; } }; }; - __workletObjectFactory_null1.__closure = {}; - __workletObjectFactory_null1.__workletHash = 14630842371699; - __workletObjectFactory_null1.__initData = _worklet_14630842371699_init_data; - __workletObjectFactory_null1.__stackDetails = _e; - return __workletObjectFactory_null1; + __workletContextObjectFactory_null1.__closure = {}; + __workletContextObjectFactory_null1.__workletHash = 9226058452652; + __workletContextObjectFactory_null1.__initData = _worklet_9226058452652_init_data; + __workletContextObjectFactory_null1.__stackDetails = _e; + return __workletContextObjectFactory_null1; }() };" `; @@ -1288,6 +1288,119 @@ var foo = function () { }();" `; +exports[`babel plugin for file workletization workletizes implicit WorkletContextObject 1`] = ` +"var _worklet_4592588545601_init_data = { + code: "function __workletContextObjectFactory_null1(){return{bar:function(){return'bar';},foobar:function(){return this.bar();}};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = { + bar: function bar() { + return 'bar'; + }, + foobar: function foobar() { + return this.bar(); + }, + __workletContextObjectFactory: function () { + var _e = [new global.Error(), 1, -27]; + var __workletContextObjectFactory_null1 = function __workletContextObjectFactory_null1() { + return { + bar: function bar() { + return 'bar'; + }, + foobar: function foobar() { + return this.bar(); + } + }; + }; + __workletContextObjectFactory_null1.__closure = {}; + __workletContextObjectFactory_null1.__workletHash = 4592588545601; + __workletContextObjectFactory_null1.__initData = _worklet_4592588545601_init_data; + __workletContextObjectFactory_null1.__stackDetails = _e; + return __workletContextObjectFactory_null1; + }() +};" +`; + +exports[`babel plugin for file workletization workletizes implicit WorkletContextObject in default export 1`] = ` +"Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; +var _worklet_4592588545601_init_data = { + code: "function __workletContextObjectFactory_null1(){return{bar:function(){return'bar';},foobar:function(){return this.bar();}};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var _default = exports.default = { + bar: function bar() { + return 'bar'; + }, + foobar: function foobar() { + return this.bar(); + }, + __workletContextObjectFactory: function () { + var _e = [new global.Error(), 1, -27]; + var __workletContextObjectFactory_null1 = function __workletContextObjectFactory_null1() { + return { + bar: function bar() { + return 'bar'; + }, + foobar: function foobar() { + return this.bar(); + } + }; + }; + __workletContextObjectFactory_null1.__closure = {}; + __workletContextObjectFactory_null1.__workletHash = 4592588545601; + __workletContextObjectFactory_null1.__initData = _worklet_4592588545601_init_data; + __workletContextObjectFactory_null1.__stackDetails = _e; + return __workletContextObjectFactory_null1; + }() +};" +`; + +exports[`babel plugin for file workletization workletizes implicit WorkletContextObject in named export 1`] = ` +"Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.foo = void 0; +var _worklet_4592588545601_init_data = { + code: "function __workletContextObjectFactory_null1(){return{bar:function(){return'bar';},foobar:function(){return this.bar();}};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = exports.foo = { + bar: function bar() { + return 'bar'; + }, + foobar: function foobar() { + return this.bar(); + }, + __workletContextObjectFactory: function () { + var _e = [new global.Error(), 1, -27]; + var __workletContextObjectFactory_null1 = function __workletContextObjectFactory_null1() { + return { + bar: function bar() { + return 'bar'; + }, + foobar: function foobar() { + return this.bar(); + } + }; + }; + __workletContextObjectFactory_null1.__closure = {}; + __workletContextObjectFactory_null1.__workletHash = 4592588545601; + __workletContextObjectFactory_null1.__initData = _worklet_4592588545601_init_data; + __workletContextObjectFactory_null1.__stackDetails = _e; + return __workletContextObjectFactory_null1; + }() +};" +`; + exports[`babel plugin for file workletization workletizes multiple functions 1`] = ` "var _worklet_5253890412305_init_data = { code: "function foo_null1(){return'bar';}", diff --git a/packages/react-native-reanimated/__tests__/plugin.test.ts b/packages/react-native-reanimated/__tests__/plugin.test.ts index e310aa02c35..48183e88c89 100644 --- a/packages/react-native-reanimated/__tests__/plugin.test.ts +++ b/packages/react-native-reanimated/__tests__/plugin.test.ts @@ -2251,6 +2251,63 @@ describe('babel plugin', () => { expect(code).toMatchSnapshot(); }); + it('workletizes implicit WorkletContextObject', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('__workletContextObjectFactory'); + expect(code).toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + + it('workletizes implicit WorkletContextObject in named export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('__workletContextObjectFactory'); + expect(code).toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + + it('workletizes implicit WorkletContextObject in default export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('__workletContextObjectFactory'); + expect(code).toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + it('workletizes multiple functions', () => { const input = html``; const { code } = runPlugin(input); - expect(code).not.toMatch(/__workletObject:\s*/g); + expect(code).not.toMatch(/__workletContextObject:\s*/g); expect(code).toMatchSnapshot(); }); @@ -2305,12 +2362,12 @@ describe('babel plugin', () => { bar() { return 'bar'; }, - __workletObject: true, + __workletContextObject: true, }; `; const { code } = runPlugin(input); - expect(code).toContain('__workletObjectFactory'); + expect(code).toContain('__workletContextObjectFactory'); expect(code).toHaveWorkletData(); expect(code).toMatchSnapshot(); }); @@ -2321,7 +2378,7 @@ describe('babel plugin', () => { bar() { return 'bar'; }, - __workletObject: new RegExp('foo'), + __workletContextObject: new RegExp('foo'), }; `; @@ -2339,7 +2396,7 @@ describe('babel plugin', () => { foobar() { return this.bar(); }, - __workletObject: true, + __workletContextObject: true, }; `; diff --git a/packages/react-native-reanimated/plugin/build/plugin.js b/packages/react-native-reanimated/plugin/build/plugin.js index 525e9d7e93c..ea05253d534 100644 --- a/packages/react-native-reanimated/plugin/build/plugin.js +++ b/packages/react-native-reanimated/plugin/build/plugin.js @@ -1143,14 +1143,46 @@ var require_webOptimization = __commonJS({ } }); +// lib/contextObject.js +var require_contextObject = __commonJS({ + "lib/contextObject.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.isContextObject = exports2.processIfWorkletContextObject = exports2.contextObjectMarker = void 0; + var types_12 = require("@babel/types"); + exports2.contextObjectMarker = "__workletContextObject"; + function processIfWorkletContextObject(path, _state) { + if (!isContextObject(path.node)) { + return false; + } + removeContextObjectMarker(path.node); + processWorkletContextObject(path.node); + return true; + } + exports2.processIfWorkletContextObject = processIfWorkletContextObject; + function isContextObject(objectExpression) { + return objectExpression.properties.some((property) => (0, types_12.isObjectProperty)(property) && (0, types_12.isIdentifier)(property.key) && property.key.name === exports2.contextObjectMarker); + } + exports2.isContextObject = isContextObject; + function processWorkletContextObject(objectExpression) { + const workletObjectFactory = (0, types_12.functionExpression)(null, [], (0, types_12.blockStatement)([(0, types_12.returnStatement)((0, types_12.cloneNode)(objectExpression))], [(0, types_12.directive)((0, types_12.directiveLiteral)("worklet"))])); + objectExpression.properties.push((0, types_12.objectProperty)((0, types_12.identifier)(`${exports2.contextObjectMarker}Factory`), workletObjectFactory)); + } + function removeContextObjectMarker(objectExpression) { + objectExpression.properties = objectExpression.properties.filter((property) => !((0, types_12.isObjectProperty)(property) && (0, types_12.isIdentifier)(property.key) && property.key.name === exports2.contextObjectMarker)); + } + } +}); + // lib/file.js var require_file = __commonJS({ "lib/file.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.processIfWorkletFile = void 0; + exports2.isImplicitContextObject = exports2.processIfWorkletFile = void 0; var types_12 = require("@babel/types"); var types_2 = require_types(); + var contextObject_12 = require_contextObject(); function processIfWorkletFile(path, state) { if (!path.node.directives.some((functionDirective) => functionDirective.value.value === "worklet")) { return false; @@ -1160,49 +1192,55 @@ var require_file = __commonJS({ 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; + function processWorkletFile(programPath, _state) { + const statements = programPath.get("body"); + statements.forEach((statement) => { + const candidatePath = getCandidate(statement); + if (candidatePath.node) { + processWorkletizableEntity(candidatePath); } - processWorkletizableEntity(candidate); }); } - function getNodeCandidate(statement) { - if ((0, types_12.isExportNamedDeclaration)(statement) || (0, types_12.isExportDefaultDeclaration)(statement)) { - return statement.declaration; + function getCandidate(statementPath) { + if (statementPath.isExportNamedDeclaration() || statementPath.isExportDefaultDeclaration()) { + return statementPath.get("declaration"); } else { - return statement; + return statementPath; } } - function processWorkletizableEntity(node) { - if ((0, types_2.isWorkletizableFunctionNode)(node)) { - if ((0, types_12.isArrowFunctionExpression)(node)) { - replaceImplicitReturnWithBlock(node); + function processWorkletizableEntity(nodePath) { + if ((0, types_2.isWorkletizableFunctionPath)(nodePath)) { + if (nodePath.isArrowFunctionExpression()) { + replaceImplicitReturnWithBlock(nodePath.node); + } + appendWorkletDirective(nodePath.node.body); + } else if ((0, types_2.isWorkletizableObjectPath)(nodePath)) { + if (isImplicitContextObject(nodePath)) { + appendWorkletContextObjectMarker(nodePath.node); + } else { + processWorkletAggregator(nodePath); } - appendWorkletDirective(node.body); - } else if ((0, types_2.isWorkletizableObjectNode)(node)) { - processObjectExpression(node); - } else if ((0, types_12.isVariableDeclaration)(node)) { - processVariableDeclaration(node); + } else if (nodePath.isVariableDeclaration()) { + processVariableDeclaration(nodePath); } } - function processVariableDeclaration(variableDeclaration) { - variableDeclaration.declarations.forEach((declaration) => { - const init = declaration.init; - if ((0, types_12.isExpression)(init)) { - processWorkletizableEntity(init); + function processVariableDeclaration(variableDeclarationPath) { + const declarations = variableDeclarationPath.get("declarations"); + declarations.forEach((declaration) => { + const initPath = declaration.get("init"); + if (initPath.isExpression()) { + processWorkletizableEntity(initPath); } }); } - 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 processWorkletAggregator(objectPath) { + const properties = objectPath.get("properties"); + properties.forEach((property) => { + if (property.isObjectMethod()) { + appendWorkletDirective(property.node.body); + } else if (property.isObjectProperty()) { + const valuePath = property.get("value"); + processWorkletizableEntity(valuePath); } }); } @@ -1216,43 +1254,31 @@ var require_file = __commonJS({ node.directives.push((0, types_12.directive)((0, types_12.directiveLiteral)("worklet"))); } } - } -}); - -// lib/contextObject.js -var require_contextObject = __commonJS({ - "lib/contextObject.js"(exports2) { - "use strict"; - Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.processIfWorkletContextObject = void 0; - var types_12 = require("@babel/types"); - var contextObjectMarker = "__workletObject"; - function processIfWorkletContextObject(path, state) { - let isWorkletContextObject = false; - path.traverse({ - ObjectProperty(subPath) { - if ((0, types_12.isIdentifier)(subPath.node.key) && subPath.node.key.name === contextObjectMarker) { - isWorkletContextObject = true; - subPath.stop(); - } + function appendWorkletContextObjectMarker(objectExpression) { + if (objectExpression.properties.some((value) => (0, types_12.isObjectProperty)(value) && (0, types_12.isIdentifier)(value.key) && value.key.name === contextObject_12.contextObjectMarker)) { + return; + } + objectExpression.properties.push((0, types_12.objectProperty)((0, types_12.identifier)(`${contextObject_12.contextObjectMarker}`), (0, types_12.booleanLiteral)(true))); + } + function isImplicitContextObject(path) { + const propertyPaths = path.get("properties"); + return propertyPaths.some((propertyPath) => { + if (!propertyPath.isObjectMethod()) { + return false; } + return hasThisExpression(propertyPath); }); - if (isWorkletContextObject) { - processWorkletContextObject(path, state); - } - return isWorkletContextObject; } - exports2.processIfWorkletContextObject = processIfWorkletContextObject; - function processWorkletContextObject(path, _state) { + exports2.isImplicitContextObject = isImplicitContextObject; + function hasThisExpression(path) { + let result = false; path.traverse({ - ObjectProperty(subPath) { - if ((0, types_12.isIdentifier)(subPath.node.key) && subPath.node.key.name === contextObjectMarker) { - subPath.remove(); - } + ThisExpression(thisPath) { + result = true; + thisPath.stop(); } }); - const workletObjectFactory = (0, types_12.functionExpression)(null, [], (0, types_12.blockStatement)([(0, types_12.returnStatement)((0, types_12.cloneNode)(path.node))], [(0, types_12.directive)((0, types_12.directiveLiteral)("worklet"))])); - path.node.properties.push((0, types_12.objectProperty)((0, types_12.identifier)("__workletObjectFactory"), workletObjectFactory)); + return result; } } }); diff --git a/packages/react-native-reanimated/plugin/src/contextObject.ts b/packages/react-native-reanimated/plugin/src/contextObject.ts index 2ec29a5dcb8..7f78e781d10 100644 --- a/packages/react-native-reanimated/plugin/src/contextObject.ts +++ b/packages/react-native-reanimated/plugin/src/contextObject.ts @@ -7,66 +7,63 @@ import { functionExpression, identifier, isIdentifier, + isObjectProperty, objectProperty, returnStatement, } from '@babel/types'; import type { ObjectExpression } from '@babel/types'; import type { ReanimatedPluginPass } from './types'; -const contextObjectMarker = '__workletObject'; +export const contextObjectMarker = '__workletContextObject'; export function processIfWorkletContextObject( path: NodePath, - state: ReanimatedPluginPass + _state: ReanimatedPluginPass ): boolean { - let isWorkletContextObject = false; - - path.traverse({ - ObjectProperty(subPath) { - if ( - isIdentifier(subPath.node.key) && - subPath.node.key.name === contextObjectMarker - ) { - isWorkletContextObject = true; - subPath.stop(); - } - }, - }); - - if (isWorkletContextObject) { - processWorkletContextObject(path, state); + if (!isContextObject(path.node)) { + return false; } - return isWorkletContextObject; + removeContextObjectMarker(path.node); + processWorkletContextObject(path.node); + return true; } -function processWorkletContextObject( - path: NodePath, - _state: ReanimatedPluginPass -): void { - path.traverse({ - ObjectProperty(subPath) { - if ( - isIdentifier(subPath.node.key) && - subPath.node.key.name === contextObjectMarker - ) { - // We need to remove the marker so that we won't process it again. - subPath.remove(); - } - }, - }); +export function isContextObject(objectExpression: ObjectExpression): boolean { + return objectExpression.properties.some( + (property) => + isObjectProperty(property) && + isIdentifier(property.key) && + property.key.name === contextObjectMarker + ); +} +function processWorkletContextObject(objectExpression: ObjectExpression): void { // A simple factory function that returns the context object. const workletObjectFactory = functionExpression( null, [], blockStatement( - [returnStatement(cloneNode(path.node))], + [returnStatement(cloneNode(objectExpression))], [directive(directiveLiteral('worklet'))] ) ); - path.node.properties.push( - objectProperty(identifier('__workletObjectFactory'), workletObjectFactory) + objectExpression.properties.push( + objectProperty( + identifier(`${contextObjectMarker}Factory`), + workletObjectFactory + ) + ); +} + +function removeContextObjectMarker(objectExpression: ObjectExpression): void { + objectExpression.properties = objectExpression.properties.filter( + (property) => + !( + isObjectProperty(property) && + isIdentifier(property.key) && + property.key.name === contextObjectMarker + ) ); } diff --git a/packages/react-native-reanimated/plugin/src/file.ts b/packages/react-native-reanimated/plugin/src/file.ts index e51da1a2c61..dc27d9e15c9 100644 --- a/packages/react-native-reanimated/plugin/src/file.ts +++ b/packages/react-native-reanimated/plugin/src/file.ts @@ -1,13 +1,13 @@ import { blockStatement, + booleanLiteral, directive, directiveLiteral, - isArrowFunctionExpression, + identifier, isBlockStatement, - isExportDefaultDeclaration, - isExportNamedDeclaration, - isExpression, - isVariableDeclaration, + isIdentifier, + isObjectProperty, + objectProperty, returnStatement, } from '@babel/types'; @@ -19,13 +19,16 @@ import type { ObjectExpression, Statement, Node as BabelNode, + ThisExpression, + ObjectMethod, } from '@babel/types'; import type { NodePath } from '@babel/core'; import { - isWorkletizableFunctionNode, - isWorkletizableObjectNode, + isWorkletizableFunctionPath, + isWorkletizableObjectPath, } from './types'; import type { ReanimatedPluginPass } from './types'; +import { contextObjectMarker } from './contextObject'; export function processIfWorkletFile( path: NodePath, @@ -51,58 +54,70 @@ export function processIfWorkletFile( * Adds a worklet directive to each viable top-level entity in the file. */ function processWorkletFile( - path: NodePath, + programPath: NodePath, _state: ReanimatedPluginPass ) { - path.node.body.forEach((statement) => { - const candidate = getNodeCandidate(statement); - if (candidate === null || candidate === undefined) { - return; + const statements = programPath.get('body'); + statements.forEach((statement) => { + const candidatePath = getCandidate(statement); + if (candidatePath.node) { + processWorkletizableEntity( + candidatePath as NodePath> + ); } - processWorkletizableEntity(candidate); }); } -function getNodeCandidate(statement: Statement) { +function getCandidate(statementPath: NodePath) { if ( - isExportNamedDeclaration(statement) || - isExportDefaultDeclaration(statement) + statementPath.isExportNamedDeclaration() || + statementPath.isExportDefaultDeclaration() ) { - return statement.declaration; + return statementPath.get('declaration') as NodePath< + typeof statementPath.node.declaration + >; } else { - return statement; + return statementPath; } } -function processWorkletizableEntity(node: BabelNode) { - if (isWorkletizableFunctionNode(node)) { - if (isArrowFunctionExpression(node)) { - replaceImplicitReturnWithBlock(node); +function processWorkletizableEntity(nodePath: NodePath) { + if (isWorkletizableFunctionPath(nodePath)) { + if (nodePath.isArrowFunctionExpression()) { + replaceImplicitReturnWithBlock(nodePath.node); + } + appendWorkletDirective(nodePath.node.body as BlockStatement); + } else if (isWorkletizableObjectPath(nodePath)) { + if (isImplicitContextObject(nodePath)) { + appendWorkletContextObjectMarker(nodePath.node); + } else { + processWorkletAggregator(nodePath); } - appendWorkletDirective(node.body as BlockStatement); - } else if (isWorkletizableObjectNode(node)) { - processObjectExpression(node); - } else if (isVariableDeclaration(node)) { - processVariableDeclaration(node); + } else if (nodePath.isVariableDeclaration()) { + processVariableDeclaration(nodePath); } } -function processVariableDeclaration(variableDeclaration: VariableDeclaration) { - variableDeclaration.declarations.forEach((declaration) => { - const init = declaration.init; - if (isExpression(init)) { - processWorkletizableEntity(init); +function processVariableDeclaration( + variableDeclarationPath: NodePath +) { + const declarations = variableDeclarationPath.get('declarations'); + declarations.forEach((declaration) => { + const initPath = declaration.get('init'); + if (initPath.isExpression()) { + processWorkletizableEntity(initPath); } }); } -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); +function processWorkletAggregator(objectPath: NodePath) { + const properties = objectPath.get('properties'); + properties.forEach((property) => { + if (property.isObjectMethod()) { + appendWorkletDirective(property.node.body); + } else if (property.isObjectProperty()) { + const valuePath = property.get('value'); + processWorkletizableEntity(valuePath); } }); } @@ -129,3 +144,47 @@ function appendWorkletDirective(node: BlockStatement) { node.directives.push(directive(directiveLiteral('worklet'))); } } + +function appendWorkletContextObjectMarker(objectExpression: ObjectExpression) { + if ( + objectExpression.properties.some( + (value) => + isObjectProperty(value) && + isIdentifier(value.key) && + value.key.name === contextObjectMarker + ) + ) { + return; + } + + objectExpression.properties.push( + objectProperty(identifier(`${contextObjectMarker}`), booleanLiteral(true)) + ); +} + +export function isImplicitContextObject( + path: NodePath +): boolean { + const propertyPaths = path.get('properties'); + + return propertyPaths.some((propertyPath) => { + if (!propertyPath.isObjectMethod()) { + return false; + } + + return hasThisExpression(propertyPath); + }); +} + +function hasThisExpression(path: NodePath): boolean { + let result = false; + + path.traverse({ + ThisExpression(thisPath: NodePath) { + result = true; + thisPath.stop(); + }, + }); + + return result; +} diff --git a/packages/react-native-reanimated/src/shareables.ts b/packages/react-native-reanimated/src/shareables.ts index fc45985a6ab..235f35c9b69 100644 --- a/packages/react-native-reanimated/src/shareables.ts +++ b/packages/react-native-reanimated/src/shareables.ts @@ -149,12 +149,15 @@ export function makeShareableCloneRecursive( // then recreate new host object wrapping the same instance on the UI thread. // there is no point of iterating over keys as we do for regular objects. toAdapt = value; - } else if (isPlainJSObject(value) && value.__workletObjectFactory) { - const workletObjectFactory = value.__workletObjectFactory; + } else if ( + isPlainJSObject(value) && + value.__workletContextObjectFactory + ) { + const workletContextObjectFactory = value.__workletContextObjectFactory; const handle = makeShareableCloneRecursive({ __init: () => { 'worklet'; - return workletObjectFactory(); + return workletContextObjectFactory(); }, }); shareableMappingCache.set(value, handle);