From ce914e6acd4fac37ece09ab8042be5a0fd3d87e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Wed, 17 Jul 2024 14:19:51 +0200 Subject: [PATCH 1/3] feat: add context objects --- .../RuntimeTests/RuntimeTestsExample.tsx | 1 + .../tests/plugin/contextObjects.test.tsx | 178 ++++++++++++++++++ .../__snapshots__/plugin.test.ts.snap | 122 ++++++++++++ .../__tests__/plugin.test.ts | 66 +++++++ .../plugin/build/plugin.js | 48 +++++ .../plugin/src/contextObject.ts | 72 +++++++ .../plugin/src/globals.ts | 2 + .../plugin/src/plugin.ts | 15 +- .../react-native-reanimated/src/shareables.ts | 16 +- 9 files changed, 516 insertions(+), 4 deletions(-) create mode 100644 apps/common-app/src/examples/RuntimeTests/tests/plugin/contextObjects.test.tsx create mode 100644 packages/react-native-reanimated/plugin/src/contextObject.ts diff --git a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx index 9441ed22e19..d40620e0b57 100644 --- a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx +++ b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx @@ -72,6 +72,7 @@ export default function RuntimeTestsExample() { testSuiteName: 'babelPlugin', 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 new file mode 100644 index 00000000000..be42d72bcbf --- /dev/null +++ b/apps/common-app/src/examples/RuntimeTests/tests/plugin/contextObjects.test.tsx @@ -0,0 +1,178 @@ +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'; + +const SHARED_VALUE_REF = 'SHARED_VALUE_REF'; + +describe('Test context objects', () => { + test('methods are workletized', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + const contextObject = { + foo() { + return 1; + }, + __workletObject: true, + }; + + useEffect(() => { + runOnUI(() => { + output.value = contextObject.foo(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(1); + }); + + test('properties are workletized', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + const contextObject = { + foo: () => 1, + __workletObject: true, + }; + + useEffect(() => { + runOnUI(() => { + output.value = contextObject.foo(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(1); + }); + + test('methods preserve implicit context', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + const contextObject = { + foo() { + return 1; + }, + bar() { + return this.foo() + 1; + }, + __workletObject: true, + }; + + useEffect(() => { + runOnUI(() => { + output.value = contextObject.bar(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(2); + }); + + test('methods preserve explicit context', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + const contextObject = { + foo() { + return 1; + }, + bar() { + return this.foo.call(contextObject) + 1; + }, + __workletObject: true, + }; + + useEffect(() => { + runOnUI(() => { + output.value = contextObject.bar(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(2); + }); + + test('methods change the state of the object', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + const contextObject = { + foo: 1, + bar() { + this.foo += 1; + }, + __workletObject: true, + }; + + useEffect(() => { + runOnUI(() => { + contextObject.bar(); + output.value = contextObject.foo; + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(2); + }); + + test("the object doesn't persist in memory", async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + const contextObject = { + foo: 1, + bar() { + this.foo += 1; + }, + __workletObject: true, + }; + + useEffect(() => { + runOnUI(() => { + contextObject.bar(); + output.value = contextObject.foo; + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(2); + await render(); + await wait(100); + const sharedValue2 = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue2.onUI).toBe(2); + }); +}); 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 70193b42bd7..ddc16a51ecf 100644 --- a/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap +++ b/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap @@ -708,6 +708,128 @@ 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';}};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = { + bar: function bar() { + return 'bar'; + }, + __workletObjectFactory: function () { + var _e = [new global.Error(), 1, -27]; + var __workletObjectFactory_null1 = function __workletObjectFactory_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; + }() +};" +`; + +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();}};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = { + bar: function bar() { + return 'bar'; + }, + foobar: function foobar() { + return this.bar(); + }, + __workletObjectFactory: function () { + var _e = [new global.Error(), 1, -27]; + var __workletObjectFactory_null1 = function __workletObjectFactory_null1() { + return { + bar: function bar() { + return 'bar'; + }, + foobar: function foobar() { + return this.bar(); + } + }; + }; + __workletObjectFactory_null1.__closure = {}; + __workletObjectFactory_null1.__workletHash = 13432710970622; + __workletObjectFactory_null1.__initData = _worklet_13432710970622_init_data; + __workletObjectFactory_null1.__stackDetails = _e; + return __workletObjectFactory_null1; + }() +};" +`; + +exports[`babel plugin for context objects removes marker 1`] = ` +"var _worklet_14630842371699_init_data = { + code: "function __workletObjectFactory_null1(){return{bar:function(){return'bar';}};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = { + bar: function bar() { + return 'bar'; + }, + __workletObjectFactory: function () { + var _e = [new global.Error(), 1, -27]; + var __workletObjectFactory_null1 = function __workletObjectFactory_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; + }() +};" +`; + +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';}};}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var foo = { + bar: function bar() { + return 'bar'; + }, + __workletObjectFactory: function () { + var _e = [new global.Error(), 1, -27]; + var __workletObjectFactory_null1 = function __workletObjectFactory_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; + }() +};" +`; + exports[`babel plugin for debugging does inject location for worklets in dev builds 1`] = ` "var _worklet_8623346549410_init_data = { code: "function null1(){const x=1;}", diff --git a/packages/react-native-reanimated/__tests__/plugin.test.ts b/packages/react-native-reanimated/__tests__/plugin.test.ts index 756ba2c072a..e310aa02c35 100644 --- a/packages/react-native-reanimated/__tests__/plugin.test.ts +++ b/packages/react-native-reanimated/__tests__/plugin.test.ts @@ -2282,4 +2282,70 @@ describe('babel plugin', () => { expect(code).toMatchSnapshot(); }); }); + + describe('for context objects', () => { + it('removes marker', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).not.toMatch(/__workletObject:\s*/g); + expect(code).toMatchSnapshot(); + }); + + it('creates factories', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('__workletObjectFactory'); + expect(code).toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + + it('workletizes regardless of marker value', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(); + expect(code).toMatchSnapshot(); + }); + + it('preserves bindings', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toIncludeInWorkletString('this.bar()'); + expect(code).toMatchSnapshot(); + }); + }); }); diff --git a/packages/react-native-reanimated/plugin/build/plugin.js b/packages/react-native-reanimated/plugin/build/plugin.js index fdb7513fc6a..525e9d7e93c 100644 --- a/packages/react-native-reanimated/plugin/build/plugin.js +++ b/packages/react-native-reanimated/plugin/build/plugin.js @@ -131,6 +131,8 @@ var require_globals = __commonJS({ "null", "this", "global", + "window", + "globalThis", "console", "performance", "queueMicrotask", @@ -1217,6 +1219,44 @@ var require_file = __commonJS({ } }); +// 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(); + } + } + }); + if (isWorkletContextObject) { + processWorkletContextObject(path, state); + } + return isWorkletContextObject; + } + exports2.processIfWorkletContextObject = processIfWorkletContextObject; + function processWorkletContextObject(path, _state) { + path.traverse({ + ObjectProperty(subPath) { + if ((0, types_12.isIdentifier)(subPath.node.key) && subPath.node.key.name === contextObjectMarker) { + subPath.remove(); + } + } + }); + 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)); + } + } +}); + // lib/plugin.js Object.defineProperty(exports, "__esModule", { value: true }); var autoworkletization_1 = require_autoworkletization(); @@ -1227,6 +1267,7 @@ var utils_1 = require_utils(); var globals_1 = require_globals(); var webOptimization_1 = require_webOptimization(); var file_1 = require_file(); +var contextObject_1 = require_contextObject(); module.exports = function() { function runWithTaggedExceptions(fun) { try { @@ -1261,6 +1302,13 @@ module.exports = function() { }); } }, + ObjectExpression: { + enter(path, state) { + runWithTaggedExceptions(() => { + (0, contextObject_1.processIfWorkletContextObject)(path, state); + }); + } + }, Program: { enter(path, state) { runWithTaggedExceptions(() => { diff --git a/packages/react-native-reanimated/plugin/src/contextObject.ts b/packages/react-native-reanimated/plugin/src/contextObject.ts new file mode 100644 index 00000000000..2ec29a5dcb8 --- /dev/null +++ b/packages/react-native-reanimated/plugin/src/contextObject.ts @@ -0,0 +1,72 @@ +import type { NodePath } from '@babel/core'; +import { + blockStatement, + cloneNode, + directive, + directiveLiteral, + functionExpression, + identifier, + isIdentifier, + objectProperty, + returnStatement, +} from '@babel/types'; +import type { ObjectExpression } from '@babel/types'; +import type { ReanimatedPluginPass } from './types'; + +const contextObjectMarker = '__workletObject'; + +export function processIfWorkletContextObject( + path: NodePath, + 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); + } + + return isWorkletContextObject; +} + +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(); + } + }, + }); + + // A simple factory function that returns the context object. + const workletObjectFactory = functionExpression( + null, + [], + blockStatement( + [returnStatement(cloneNode(path.node))], + [directive(directiveLiteral('worklet'))] + ) + ); + + path.node.properties.push( + objectProperty(identifier('__workletObjectFactory'), workletObjectFactory) + ); +} diff --git a/packages/react-native-reanimated/plugin/src/globals.ts b/packages/react-native-reanimated/plugin/src/globals.ts index f097078ea59..dc790c8a812 100644 --- a/packages/react-native-reanimated/plugin/src/globals.ts +++ b/packages/react-native-reanimated/plugin/src/globals.ts @@ -102,6 +102,8 @@ const notCapturedIdentifiers = [ 'null', 'this', 'global', + 'window', + 'globalThis', 'console', 'performance', 'queueMicrotask', diff --git a/packages/react-native-reanimated/plugin/src/plugin.ts b/packages/react-native-reanimated/plugin/src/plugin.ts index 426d08105f5..3f7b41c25d7 100644 --- a/packages/react-native-reanimated/plugin/src/plugin.ts +++ b/packages/react-native-reanimated/plugin/src/plugin.ts @@ -1,5 +1,10 @@ import type { PluginItem, NodePath } from '@babel/core'; -import type { CallExpression, JSXAttribute, Program } from '@babel/types'; +import type { + CallExpression, + JSXAttribute, + Program, + ObjectExpression, +} from '@babel/types'; import { processIfAutoworkletizableCallback, processCalleesAutoworkletizableCallbacks, @@ -12,6 +17,7 @@ import { addCustomGlobals } from './utils'; import { initializeGlobals } from './globals'; import { substituteWebCallExpression } from './webOptimization'; import { processIfWorkletFile } from './file'; +import { processIfWorkletContextObject } from './contextObject'; module.exports = function (): PluginItem { function runWithTaggedExceptions(fun: () => void) { @@ -53,6 +59,13 @@ module.exports = function (): PluginItem { }); }, }, + ObjectExpression: { + enter(path: NodePath, state: ReanimatedPluginPass) { + runWithTaggedExceptions(() => { + processIfWorkletContextObject(path, state); + }); + }, + }, Program: { enter(path: NodePath, state: ReanimatedPluginPass) { runWithTaggedExceptions(() => { diff --git a/packages/react-native-reanimated/src/shareables.ts b/packages/react-native-reanimated/src/shareables.ts index 87b812e047d..fc45985a6ab 100644 --- a/packages/react-native-reanimated/src/shareables.ts +++ b/packages/react-native-reanimated/src/shareables.ts @@ -17,7 +17,7 @@ import { // for web/chrome debugger/jest environments this file provides a stub implementation // where no shareable references are used. Instead, the objects themselves are used // instead of shareable references, because of the fact that we don't have to deal with -// runnning the code on separate VMs. +// running the code on separate VMs. const SHOULD_BE_USE_WEB = shouldBeUseWeb(); const MAGIC_KEY = 'REANIMATED_MAGIC_KEY'; @@ -109,7 +109,7 @@ export function makeShareableCloneRecursive( } if (depth >= DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD) { // if we reach certain recursion depth we suspect that we are dealing with a cyclic object. - // this type of objects are not supported and cannot be trasferred as shareable, so we + // this type of objects are not supported and cannot be transferred as shareable, so we // implement a simple detection mechanism that remembers the value at a given depth and // tests whether we try reenter this method later on with the same value. If that happens // we throw an appropriate error. @@ -149,6 +149,16 @@ 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; + const handle = makeShareableCloneRecursive({ + __init: () => { + 'worklet'; + return workletObjectFactory(); + }, + }); + shareableMappingCache.set(value, handle); + return handle as ShareableRef; } else if (isPlainJSObject(value) || isTypeFunction) { toAdapt = {}; if (isWorkletFunction(value)) { @@ -173,7 +183,7 @@ Offending code was: \`${getWorkletCode(value)}\``); } // to save on transferring static __initData field of worklet structure // we request shareable value to persist its UI counterpart. This means - // that the __initData field that contains long strings represeting the + // that the __initData field that contains long strings representing the // worklet code, source map, and location, will always be // serialized/deserialized once. toAdapt.__initData = makeShareableCloneRecursive( 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 2/3] 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); From 077bdd2665f57f20832d9c259654cdeaa626b5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Wed, 17 Jul 2024 16:46:33 +0200 Subject: [PATCH 3/3] chore: make test names more verbose --- .../RuntimeTests/tests/plugin/contextObjects.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 68af3cb1752..1bc7edff4ac 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 @@ -14,7 +14,7 @@ import { const SHARED_VALUE_REF = 'SHARED_VALUE_REF'; describe('Test context objects', () => { - test('methods are workletized', async () => { + test('non-context methods are workletized', async () => { const ExampleComponent = () => { const output = useSharedValue(null); registerValue(SHARED_VALUE_REF, output); @@ -39,7 +39,7 @@ describe('Test context objects', () => { expect(sharedValue.onUI).toBe(1); }); - test('properties are workletized', async () => { + test('non-context properties are workletized', async () => { const ExampleComponent = () => { const output = useSharedValue(null); registerValue(SHARED_VALUE_REF, output);