From 742259191b52e4e50064a23075e7b56609868cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Wed, 17 Jul 2024 16:56:00 +0200 Subject: [PATCH 01/10] feat: support class workletization --- .eslintrc.js | 1 + .../__snapshots__/plugin.test.ts.snap | 216 ++++++++++ .../__tests__/plugin.test.ts | 76 ++++ .../plugin/build/plugin.js | 406 +++++++++++++----- .../plugin/src/class.ts | 235 ++++++++++ .../plugin/src/file.ts | 35 +- .../plugin/src/jestMatchers.ts | 9 +- .../plugin/src/plugin.ts | 18 +- .../plugin/src/workletFactory.ts | 15 +- .../plugin/src/workletStringCode.ts | 33 ++ 10 files changed, 899 insertions(+), 145 deletions(-) create mode 100644 packages/react-native-reanimated/plugin/src/class.ts diff --git a/.eslintrc.js b/.eslintrc.js index 1a03c14df96..36ee0f8d196 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,5 +87,6 @@ module.exports = { 'jest/no-identical-title': 'error', 'jest/prefer-to-have-length': 'warn', 'jest/valid-expect': 'error', + 'react/react-in-jsx-scope': 'off', }, }; 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 f9a32cda80c..25f2957d418 100644 --- a/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap +++ b/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap @@ -593,6 +593,222 @@ var Foo = function () { }();" `; +exports[`babel plugin for classes creates factories 1`] = ` +"var _worklet_4914514148035_init_data = { + code: "function _toPrimitive(t,r){if(\\"object\\"!=typeof t||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var i=e.call(t,r||\\"default\\");if(\\"object\\"!=typeof i)return i;throw new TypeError(\\"@@toPrimitive must return a primitive value.\\");}return(\\"string\\"===r?String:Number)(t);}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var _toPrimitive = function () { + var _e = [new global.Error(), 1, -27]; + var _toPrimitive = function _toPrimitive(t, r) { + if ("object" != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || "default"); + if ("object" != typeof i) return i; + throw new TypeError("@@toPrimitive must return a primitive value."); + } + return ("string" === r ? String : Number)(t); + }; + _toPrimitive.__closure = {}; + _toPrimitive.__workletHash = 4914514148035; + _toPrimitive.__initData = _worklet_4914514148035_init_data; + _toPrimitive.__stackDetails = _e; + return _toPrimitive; +}(); +var _worklet_14183653944217_init_data = { + code: "function _toPropertyKey(t){const{_toPrimitive}=this.__closure;var i=_toPrimitive(t,\\"string\\");return\\"symbol\\"==typeof i?i:i+\\"\\";}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var _toPropertyKey = function () { + var _e = [new global.Error(), -2, -27]; + var _toPropertyKey = function _toPropertyKey(t) { + var i = _toPrimitive(t, "string"); + return "symbol" == typeof i ? i : i + ""; + }; + _toPropertyKey.__closure = { + _toPrimitive: _toPrimitive + }; + _toPropertyKey.__workletHash = 14183653944217; + _toPropertyKey.__initData = _worklet_14183653944217_init_data; + _toPropertyKey.__stackDetails = _e; + return _toPropertyKey; +}(); +var _worklet_17099703587270_init_data = { + code: "function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError(\\"Cannot call a class as a function\\");}}", + location: "/dev/null", + sourceMap: "\\"mock source map\\"", + version: "x.y.z" +}; +var _classCallCheck = function () { + var _e = [new global.Error(), 1, -27]; + var _classCallCheck = function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + }; + _classCallCheck.__closure = {}; + _classCallCheck.__workletHash = 17099703587270; + _classCallCheck.__initData = _worklet_17099703587270_init_data; + _classCallCheck.__stackDetails = _e; + return _classCallCheck; +}(); +var _worklet_7208792264399_init_data = { + code: "function _defineProperties(target,props){const{_toPropertyKey}=this.__closure;for(var i=0;i { expect(code).toMatchSnapshot(); }); }); + + describe('for classes', () => { + it('creates factories', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('var ClazzClassFactory = function ()'); + expect(code).toIncludeInWorkletString('ClazzClassFactory'); + expect(code).toContain('Clazz.ClazzClassFactory = ClazzClassFactory'); + expect(code).toMatchSnapshot(); + }); + + it('injects class factory into worklets', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('ClazzClassFactory'); + expect(code).toMatchSnapshot(); + }); + + it('modifies closures', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('ClazzClassFactory: Clazz.ClazzClassFactory'); + expect(code).toMatchSnapshot(); + }); + + it('keeps "this" binding', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toIncludeInWorkletString('this.member'); + expect(code).toMatchSnapshot(); + }); + + it('appends necessary polyfills', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('createClass'); + expect(code).toMatchSnapshot(); + }); + }); }); diff --git a/packages/react-native-reanimated/plugin/build/plugin.js b/packages/react-native-reanimated/plugin/build/plugin.js index ea05253d534..0cc29a39cc3 100644 --- a/packages/react-native-reanimated/plugin/build/plugin.js +++ b/packages/react-native-reanimated/plugin/build/plugin.js @@ -251,6 +251,22 @@ var require_workletStringCode = __commonJS({ const expression = (0, types_12.isFunctionDeclaration)(draftExpression) ? draftExpression : draftExpression.expression; (0, assert_1.strict)("params" in expression, "'params' property is undefined in 'expression'"); (0, assert_1.strict)((0, types_12.isBlockStatement)(expression.body), "[Reanimated] `expression.body` is not a `BlockStatement`"); + const parsedClasses = /* @__PURE__ */ new Set(); + (0, core_1.traverse)(fun, { + NewExpression(path) { + const constructorName = path.node.callee.name; + if (!closureVariables.some((variable) => variable.name === constructorName) || parsedClasses.has(constructorName)) { + return; + } + const index = closureVariables.findIndex((variable) => variable.name === constructorName); + closureVariables.splice(index, 1); + closureVariables.push((0, types_12.identifier)(constructorName + "ClassFactory")); + expression.body.body.unshift((0, types_12.variableDeclaration)("const", [ + (0, types_12.variableDeclarator)((0, types_12.identifier)(constructorName), (0, types_12.callExpression)((0, types_12.identifier)(constructorName + "ClassFactory"), [])) + ])); + parsedClasses.add(constructorName); + } + }); const workletFunction = (0, types_12.functionExpression)((0, types_12.identifier)(nameWithSource), expression.params, expression.body, expression.generator, expression.async); const code = (0, generator_1.default)(workletFunction).code; (0, assert_1.strict)(inputMap, "[Reanimated] `inputMap` is undefined."); @@ -436,7 +452,7 @@ var require_workletFactory = __commonJS({ (0, types_12.variableDeclaration)("const", [ (0, types_12.variableDeclarator)(functionIdentifier, funExpression) ]), - (0, types_12.expressionStatement)((0, types_12.assignmentExpression)("=", (0, types_12.memberExpression)(functionIdentifier, (0, types_12.identifier)("__closure"), false), (0, types_12.objectExpression)(variables.map((variable) => (0, types_12.objectProperty)((0, types_12.identifier)(variable.name), variable, false, true))))), + (0, types_12.expressionStatement)((0, types_12.assignmentExpression)("=", (0, types_12.memberExpression)(functionIdentifier, (0, types_12.identifier)("__closure"), false), (0, types_12.objectExpression)(variables.map((variable) => variable.name.endsWith("ClassFactory") ? (0, types_12.objectProperty)((0, types_12.identifier)(variable.name), (0, types_12.memberExpression)((0, types_12.identifier)(variable.name.slice(0, variable.name.length - "ClassFactory".length)), (0, types_12.identifier)(variable.name))) : (0, types_12.objectProperty)((0, types_12.identifier)(variable.name), variable, false, true))))), (0, types_12.expressionStatement)((0, types_12.assignmentExpression)("=", (0, types_12.memberExpression)(functionIdentifier, (0, types_12.identifier)("__workletHash"), false), (0, types_12.numericLiteral)(workletHash))) ]; if (shouldIncludeInitData) { @@ -1042,107 +1058,6 @@ var require_autoworkletization = __commonJS({ } }); -// lib/inlineStylesWarning.js -var require_inlineStylesWarning = __commonJS({ - "lib/inlineStylesWarning.js"(exports2) { - "use strict"; - Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.processInlineStylesWarning = void 0; - var types_12 = require("@babel/types"); - var utils_12 = require_utils(); - var assert_1 = require("assert"); - function generateInlineStylesWarning(path) { - return (0, types_12.callExpression)((0, types_12.arrowFunctionExpression)([], (0, types_12.blockStatement)([ - (0, types_12.expressionStatement)((0, types_12.callExpression)((0, types_12.memberExpression)((0, types_12.identifier)("console"), (0, types_12.identifier)("warn")), [ - (0, types_12.callExpression)((0, types_12.memberExpression)((0, types_12.callExpression)((0, types_12.identifier)("require"), [ - (0, types_12.stringLiteral)("react-native-reanimated") - ]), (0, types_12.identifier)("getUseOfValueInStyleWarning")), []) - ])), - (0, types_12.returnStatement)(path.node) - ])), []); - } - function processPropertyValueForInlineStylesWarning(path) { - if (path.isMemberExpression() && (0, types_12.isIdentifier)(path.node.property)) { - if (path.node.property.name === "value") { - path.replaceWith(generateInlineStylesWarning(path)); - } - } - } - function processTransformPropertyForInlineStylesWarning(path) { - if ((0, types_12.isArrayExpression)(path.node)) { - const elements = path.get("elements"); - (0, assert_1.strict)(Array.isArray(elements), "[Reanimated] `elements` should be an array."); - for (const element of elements) { - if (element.isObjectExpression()) { - processStyleObjectForInlineStylesWarning(element); - } - } - } - } - function processStyleObjectForInlineStylesWarning(path) { - const properties = path.get("properties"); - for (const property of properties) { - if (property.isObjectProperty()) { - const value = property.get("value"); - if ((0, types_12.isIdentifier)(property.node.key) && property.node.key.name === "transform") { - processTransformPropertyForInlineStylesWarning(value); - } else { - processPropertyValueForInlineStylesWarning(value); - } - } - } - } - function processInlineStylesWarning(path, state) { - if ((0, utils_12.isRelease)()) { - return; - } - if (state.opts.disableInlineStylesWarning) { - return; - } - if (path.node.name.name !== "style") { - return; - } - if (!(0, types_12.isJSXExpressionContainer)(path.node.value)) { - return; - } - const expression = path.get("value").get("expression"); - (0, assert_1.strict)(!Array.isArray(expression), "[Reanimated] `expression` should not be an array."); - if (expression.isArrayExpression()) { - const elements = expression.get("elements"); - (0, assert_1.strict)(Array.isArray(elements), "[Reanimated] `elements` should be an array."); - for (const element of elements) { - if (element.isObjectExpression()) { - processStyleObjectForInlineStylesWarning(element); - } - } - } else if (expression.isObjectExpression()) { - processStyleObjectForInlineStylesWarning(expression); - } - } - exports2.processInlineStylesWarning = processInlineStylesWarning; - } -}); - -// lib/webOptimization.js -var require_webOptimization = __commonJS({ - "lib/webOptimization.js"(exports2) { - "use strict"; - Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.substituteWebCallExpression = void 0; - var types_12 = require("@babel/types"); - function substituteWebCallExpression(path) { - const callee = path.node.callee; - if ((0, types_12.isIdentifier)(callee)) { - const name = callee.name; - if (name === "isWeb" || name === "shouldBeUseWeb") { - path.replaceWith((0, types_12.booleanLiteral)(true)); - } - } - } - exports2.substituteWebCallExpression = substituteWebCallExpression; - } -}); - // lib/contextObject.js var require_contextObject = __commonJS({ "lib/contextObject.js"(exports2) { @@ -1174,6 +1089,159 @@ var require_contextObject = __commonJS({ } }); +// lib/class.js +var require_class = __commonJS({ + "lib/class.js"(exports2) { + "use strict"; + var __importDefault = exports2 && exports2.__importDefault || function(mod) { + return mod && mod.__esModule ? mod : { "default": mod }; + }; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.processClass = void 0; + var core_1 = require("@babel/core"); + var types_12 = require("@babel/types"); + var generator_1 = __importDefault(require("@babel/generator")); + var types_2 = require_types(); + var traverse_1 = __importDefault(require("@babel/traverse")); + var assert_1 = require("assert"); + function processClass(path, state) { + if (!path.node.id) { + return; + } + const className = path.node.id.name; + const code = (0, generator_1.default)(path.node).code; + const transformedCode = (0, core_1.transformSync)(code, { + plugins: [ + "@babel/plugin-transform-class-properties", + "@babel/plugin-transform-classes", + "@babel/plugin-transform-unicode-regex" + ], + filename: state.file.opts.filename, + ast: true, + babelrc: false, + configFile: false + }); + (0, assert_1.strict)(transformedCode); + (0, assert_1.strict)(transformedCode.ast); + const ast = transformedCode.ast; + let factory; + let hasHandledClass = false; + (0, traverse_1.default)(ast, { + [types_2.WorkletizableFunction]: { + enter: (functionPath) => { + if (functionPath.parentPath.isObjectProperty()) { + return; + } + const workletDirective = (0, types_12.directive)((0, types_12.directiveLiteral)("worklet")); + if (functionPath.parentPath.isCallExpression() && !hasHandledClass) { + const factoryCopy = (0, types_12.cloneNode)(functionPath.node, true); + factoryCopy.id = (0, types_12.identifier)(className + "ClassFactory"); + factoryCopy.body.directives.push(workletDirective); + factory = (0, types_12.variableDeclaration)("const", [ + (0, types_12.variableDeclarator)((0, types_12.identifier)(className + "ClassFactory"), factoryCopy) + ]); + hasHandledClass = true; + return; + } + const bodyPath = functionPath.get("body"); + if (!bodyPath.isBlockStatement()) { + bodyPath.replaceWith((0, types_12.blockStatement)([(0, types_12.returnStatement)(bodyPath.node)])); + } + functionPath.node.body.directives.push(workletDirective); + } + } + }); + const body = ast.program.body; + body.push(factory); + body.push((0, types_12.expressionStatement)((0, types_12.assignmentExpression)("=", (0, types_12.memberExpression)((0, types_12.identifier)(className), (0, types_12.identifier)(className + "ClassFactory")), (0, types_12.identifier)(className + "ClassFactory")))); + sortPolyfills(ast); + const transformedNewCode = (0, core_1.transformSync)((0, generator_1.default)(ast).code, { + ast: true, + filename: state.file.opts.filename + }); + (0, assert_1.strict)(transformedNewCode); + (0, assert_1.strict)(transformedNewCode.ast); + const parent = path.parent; + const index = parent.body.findIndex((node) => node === path.node); + parent.body.splice(index, 1, ...transformedNewCode.ast.program.body); + } + exports2.processClass = processClass; + function sortPolyfills(ast) { + const toSort = getPolyfillsToSort(ast); + const sorted = topoSort(toSort); + const toSortIndices = toSort.map((element) => element.index); + const sortedIndices = sorted.map((element) => element.index); + const statements = ast.program.body; + const oldStatements = [...statements]; + for (let i = 0; i < toSort.length; i++) { + const sourceIndex = sortedIndices[i]; + const targetIndex = toSortIndices[i]; + const source = oldStatements[sourceIndex]; + statements[targetIndex] = source; + } + } + function getPolyfillsToSort(ast) { + const polyfills = []; + (0, traverse_1.default)(ast, { + Program: { + enter: (functionPath) => { + const statements = functionPath.get("body"); + statements.forEach((statement, index) => { + var _a; + const bindingIdentifiers = statement.getBindingIdentifiers(); + if (!statement.isFunctionDeclaration() || !((_a = statement.node.id) === null || _a === void 0 ? void 0 : _a.name)) { + return; + } + const element = { + name: statement.node.id.name, + index, + dependencies: /* @__PURE__ */ new Set() + }; + polyfills.push(element); + statement.traverse({ + Identifier(path) { + if (!path.isReferencedIdentifier() || path.node.name in bindingIdentifiers || statement.scope.hasOwnBinding(path.node.name) || !statement.scope.hasReference(path.node.name)) { + return; + } + element.dependencies.add(path.node.name); + console.log(path.node.name); + } + }); + }); + } + } + }); + return polyfills; + } + function topoSort(toSort) { + const sorted = []; + const stack = /* @__PURE__ */ new Set(); + for (const element of toSort) { + recursiveTopoSort(element, toSort, sorted, stack); + } + return sorted; + } + function recursiveTopoSort(current, toSort, sorted, stack) { + if (stack.has(current.name)) { + throw new Error("Cycle detected. This should never happen."); + } + if (sorted.find((element) => element.name === current.name)) { + return; + } + stack.add(current.name); + for (const dependency of current.dependencies) { + if (!sorted.find((element) => element.name === dependency)) { + const next = toSort.find((element) => element.name === dependency); + (0, assert_1.strict)(next); + recursiveTopoSort(next, toSort, sorted, stack); + } + } + sorted.push(current); + stack.delete(current.name); + } + } +}); + // lib/file.js var require_file = __commonJS({ "lib/file.js"(exports2) { @@ -1182,6 +1250,7 @@ var require_file = __commonJS({ exports2.isImplicitContextObject = exports2.processIfWorkletFile = void 0; var types_12 = require("@babel/types"); var types_2 = require_types(); + var class_1 = require_class(); var contextObject_12 = require_contextObject(); function processIfWorkletFile(path, state) { if (!path.node.directives.some((functionDirective) => functionDirective.value.value === "worklet")) { @@ -1192,13 +1261,11 @@ var require_file = __commonJS({ return true; } exports2.processIfWorkletFile = processIfWorkletFile; - function processWorkletFile(programPath, _state) { + function processWorkletFile(programPath, state) { const statements = programPath.get("body"); statements.forEach((statement) => { const candidatePath = getCandidate(statement); - if (candidatePath.node) { - processWorkletizableEntity(candidatePath); - } + processWorkletizableEntity(candidatePath, state); }); } function getCandidate(statementPath) { @@ -1208,7 +1275,7 @@ var require_file = __commonJS({ return statementPath; } } - function processWorkletizableEntity(nodePath) { + function processWorkletizableEntity(nodePath, state) { if ((0, types_2.isWorkletizableFunctionPath)(nodePath)) { if (nodePath.isArrowFunctionExpression()) { replaceImplicitReturnWithBlock(nodePath.node); @@ -1218,29 +1285,31 @@ var require_file = __commonJS({ if (isImplicitContextObject(nodePath)) { appendWorkletContextObjectMarker(nodePath.node); } else { - processWorkletAggregator(nodePath); + processWorkletAggregator(nodePath, state); } } else if (nodePath.isVariableDeclaration()) { - processVariableDeclaration(nodePath); + processVariableDeclaration(nodePath, state); + } else if (nodePath.isClassDeclaration()) { + (0, class_1.processClass)(nodePath, state); } } - function processVariableDeclaration(variableDeclarationPath) { + function processVariableDeclaration(variableDeclarationPath, state) { const declarations = variableDeclarationPath.get("declarations"); declarations.forEach((declaration) => { const initPath = declaration.get("init"); if (initPath.isExpression()) { - processWorkletizableEntity(initPath); + processWorkletizableEntity(initPath, state); } }); } - function processWorkletAggregator(objectPath) { + function processWorkletAggregator(objectPath, state) { 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); + processWorkletizableEntity(valuePath, state); } }); } @@ -1283,17 +1352,118 @@ var require_file = __commonJS({ } }); +// lib/inlineStylesWarning.js +var require_inlineStylesWarning = __commonJS({ + "lib/inlineStylesWarning.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.processInlineStylesWarning = void 0; + var types_12 = require("@babel/types"); + var utils_12 = require_utils(); + var assert_1 = require("assert"); + function generateInlineStylesWarning(path) { + return (0, types_12.callExpression)((0, types_12.arrowFunctionExpression)([], (0, types_12.blockStatement)([ + (0, types_12.expressionStatement)((0, types_12.callExpression)((0, types_12.memberExpression)((0, types_12.identifier)("console"), (0, types_12.identifier)("warn")), [ + (0, types_12.callExpression)((0, types_12.memberExpression)((0, types_12.callExpression)((0, types_12.identifier)("require"), [ + (0, types_12.stringLiteral)("react-native-reanimated") + ]), (0, types_12.identifier)("getUseOfValueInStyleWarning")), []) + ])), + (0, types_12.returnStatement)(path.node) + ])), []); + } + function processPropertyValueForInlineStylesWarning(path) { + if (path.isMemberExpression() && (0, types_12.isIdentifier)(path.node.property)) { + if (path.node.property.name === "value") { + path.replaceWith(generateInlineStylesWarning(path)); + } + } + } + function processTransformPropertyForInlineStylesWarning(path) { + if ((0, types_12.isArrayExpression)(path.node)) { + const elements = path.get("elements"); + (0, assert_1.strict)(Array.isArray(elements), "[Reanimated] `elements` should be an array."); + for (const element of elements) { + if (element.isObjectExpression()) { + processStyleObjectForInlineStylesWarning(element); + } + } + } + } + function processStyleObjectForInlineStylesWarning(path) { + const properties = path.get("properties"); + for (const property of properties) { + if (property.isObjectProperty()) { + const value = property.get("value"); + if ((0, types_12.isIdentifier)(property.node.key) && property.node.key.name === "transform") { + processTransformPropertyForInlineStylesWarning(value); + } else { + processPropertyValueForInlineStylesWarning(value); + } + } + } + } + function processInlineStylesWarning(path, state) { + if ((0, utils_12.isRelease)()) { + return; + } + if (state.opts.disableInlineStylesWarning) { + return; + } + if (path.node.name.name !== "style") { + return; + } + if (!(0, types_12.isJSXExpressionContainer)(path.node.value)) { + return; + } + const expression = path.get("value").get("expression"); + (0, assert_1.strict)(!Array.isArray(expression), "[Reanimated] `expression` should not be an array."); + if (expression.isArrayExpression()) { + const elements = expression.get("elements"); + (0, assert_1.strict)(Array.isArray(elements), "[Reanimated] `elements` should be an array."); + for (const element of elements) { + if (element.isObjectExpression()) { + processStyleObjectForInlineStylesWarning(element); + } + } + } else if (expression.isObjectExpression()) { + processStyleObjectForInlineStylesWarning(expression); + } + } + exports2.processInlineStylesWarning = processInlineStylesWarning; + } +}); + +// lib/webOptimization.js +var require_webOptimization = __commonJS({ + "lib/webOptimization.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.substituteWebCallExpression = void 0; + var types_12 = require("@babel/types"); + function substituteWebCallExpression(path) { + const callee = path.node.callee; + if ((0, types_12.isIdentifier)(callee)) { + const name = callee.name; + if (name === "isWeb" || name === "shouldBeUseWeb") { + path.replaceWith((0, types_12.booleanLiteral)(true)); + } + } + } + exports2.substituteWebCallExpression = substituteWebCallExpression; + } +}); + // lib/plugin.js Object.defineProperty(exports, "__esModule", { value: true }); var autoworkletization_1 = require_autoworkletization(); -var types_1 = require_types(); -var workletSubstitution_1 = require_workletSubstitution(); +var contextObject_1 = require_contextObject(); +var file_1 = require_file(); +var globals_1 = require_globals(); var inlineStylesWarning_1 = require_inlineStylesWarning(); +var types_1 = require_types(); 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(); +var workletSubstitution_1 = require_workletSubstitution(); module.exports = function() { function runWithTaggedExceptions(fun) { try { diff --git a/packages/react-native-reanimated/plugin/src/class.ts b/packages/react-native-reanimated/plugin/src/class.ts new file mode 100644 index 00000000000..b8cd114eab2 --- /dev/null +++ b/packages/react-native-reanimated/plugin/src/class.ts @@ -0,0 +1,235 @@ +import { transformSync } from '@babel/core'; +import type { NodePath } from '@babel/core'; +import { + assignmentExpression, + blockStatement, + cloneNode, + directive, + directiveLiteral, + expressionStatement, + identifier, + memberExpression, + returnStatement, + variableDeclaration, + variableDeclarator, +} from '@babel/types'; +import type { + BlockStatement, + ClassDeclaration, + Expression, + FunctionExpression, + Program, + File as BabelFile, + VariableDeclaration, + Identifier, +} from '@babel/types'; +import type { ReanimatedPluginPass } from './types'; +import generate from '@babel/generator'; +import { WorkletizableFunction } from './types'; +import traverse from '@babel/traverse'; +import { strict as assert } from 'assert'; + +export function processClass( + path: NodePath, + state: ReanimatedPluginPass +) { + if (!path.node.id) { + return; + } + const className = path.node.id.name; + const code = generate(path.node).code; + + const transformedCode = transformSync(code, { + plugins: [ + '@babel/plugin-transform-class-properties', + '@babel/plugin-transform-classes', + '@babel/plugin-transform-unicode-regex', + ], + filename: state.file.opts.filename, + ast: true, + babelrc: false, + configFile: false, + }); + + assert(transformedCode); + assert(transformedCode.ast); + + const ast = transformedCode.ast; + + let factory: VariableDeclaration; + + let hasHandledClass = false; + + traverse(ast, { + [WorkletizableFunction]: { + // @ts-expect-error TS has some trouble inferring here. + enter: (functionPath: NodePath) => { + if (functionPath.parentPath.isObjectProperty()) { + return; + } + + const workletDirective = directive(directiveLiteral('worklet')); + + if (functionPath.parentPath.isCallExpression() && !hasHandledClass) { + const factoryCopy = cloneNode( + functionPath.node, + true + ) as FunctionExpression; + factoryCopy.id = identifier(className + 'ClassFactory'); + factoryCopy.body.directives.push(workletDirective); + factory = variableDeclaration('const', [ + variableDeclarator( + identifier(className + 'ClassFactory'), + factoryCopy + ), + ]); + hasHandledClass = true; + + return; + } + + const bodyPath = functionPath.get('body'); + if (!bodyPath.isBlockStatement()) { + bodyPath.replaceWith( + blockStatement([returnStatement(bodyPath.node as Expression)]) + ); + } + + (functionPath.node.body as BlockStatement).directives.push( + workletDirective + ); + }, + }, + }); + + const body = ast.program.body; + body.push(factory!); + + body.push( + expressionStatement( + assignmentExpression( + '=', + memberExpression( + identifier(className), + identifier(className + 'ClassFactory') + ), + identifier(className + 'ClassFactory') + ) + ) + ); + + sortPolyfills(ast); + + const transformedNewCode = transformSync(generate(ast).code, { + ast: true, + filename: state.file.opts.filename, + }); + + assert(transformedNewCode); + assert(transformedNewCode.ast); + + const parent = path.parent as Program; + + const index = parent.body.findIndex((node) => node === path.node); + + parent.body.splice(index, 1, ...transformedNewCode.ast.program.body); +} + +function sortPolyfills(ast: BabelFile) { + const toSort = getPolyfillsToSort(ast); + + const sorted = topoSort(toSort); + + const toSortIndices = toSort.map((element) => element.index); + const sortedIndices = sorted.map((element) => element.index); + const statements = ast.program.body; + const oldStatements = [...statements]; + + for (let i = 0; i < toSort.length; i++) { + const sourceIndex = sortedIndices[i]; + const targetIndex = toSortIndices[i]; + const source = oldStatements[sourceIndex]; + statements[targetIndex] = source; + } +} + +function getPolyfillsToSort(ast: BabelFile): SortElement[] { + const polyfills: SortElement[] = []; + + traverse(ast, { + Program: { + enter: (functionPath: NodePath) => { + const statements = functionPath.get('body'); + statements.forEach((statement, index) => { + const bindingIdentifiers = statement.getBindingIdentifiers(); + if (!statement.isFunctionDeclaration() || !statement.node.id?.name) { + return; + } + + const element: SortElement = { + name: statement.node.id.name, + index, + dependencies: new Set(), + }; + polyfills.push(element); + statement.traverse({ + Identifier(path: NodePath) { + if ( + !path.isReferencedIdentifier() || + path.node.name in bindingIdentifiers || + statement.scope.hasOwnBinding(path.node.name) || + !statement.scope.hasReference(path.node.name) + ) { + return; + } + element.dependencies.add(path.node.name); + console.log(path.node.name); + }, + }); + }); + }, + }, + }); + + return polyfills; +} + +function topoSort(toSort: SortElement[]): SortElement[] { + const sorted: SortElement[] = []; + const stack: Set = new Set(); + for (const element of toSort) { + recursiveTopoSort(element, toSort, sorted, stack); + } + return sorted; +} + +function recursiveTopoSort( + current: SortElement, + toSort: SortElement[], + sorted: SortElement[], + stack: Set +) { + if (stack.has(current.name)) { + throw new Error('Cycle detected. This should never happen.'); + } + if (sorted.find((element) => element.name === current.name)) { + return; + } + stack.add(current.name); + for (const dependency of current.dependencies) { + if (!sorted.find((element) => element.name === dependency)) { + const next = toSort.find((element) => element.name === dependency); + assert(next); + + recursiveTopoSort(next, toSort, sorted, stack); + } + } + sorted.push(current); + stack.delete(current.name); +} + +type SortElement = { + name: string; + index: number; + dependencies: Set; +}; diff --git a/packages/react-native-reanimated/plugin/src/file.ts b/packages/react-native-reanimated/plugin/src/file.ts index dc27d9e15c9..2d1b00f0439 100644 --- a/packages/react-native-reanimated/plugin/src/file.ts +++ b/packages/react-native-reanimated/plugin/src/file.ts @@ -28,6 +28,7 @@ import { isWorkletizableObjectPath, } from './types'; import type { ReanimatedPluginPass } from './types'; +import { processClass } from './class'; import { contextObjectMarker } from './contextObject'; export function processIfWorkletFile( @@ -55,16 +56,15 @@ export function processIfWorkletFile( */ function processWorkletFile( programPath: NodePath, - _state: ReanimatedPluginPass + state: ReanimatedPluginPass ) { const statements = programPath.get('body'); statements.forEach((statement) => { const candidatePath = getCandidate(statement); - if (candidatePath.node) { - processWorkletizableEntity( - candidatePath as NodePath> - ); - } + processWorkletizableEntity( + candidatePath as NodePath>, + state + ); }); } @@ -81,7 +81,10 @@ function getCandidate(statementPath: NodePath) { } } -function processWorkletizableEntity(nodePath: NodePath) { +function processWorkletizableEntity( + nodePath: NodePath, + state: ReanimatedPluginPass +) { if (isWorkletizableFunctionPath(nodePath)) { if (nodePath.isArrowFunctionExpression()) { replaceImplicitReturnWithBlock(nodePath.node); @@ -91,33 +94,39 @@ function processWorkletizableEntity(nodePath: NodePath) { if (isImplicitContextObject(nodePath)) { appendWorkletContextObjectMarker(nodePath.node); } else { - processWorkletAggregator(nodePath); + processWorkletAggregator(nodePath, state); } } else if (nodePath.isVariableDeclaration()) { - processVariableDeclaration(nodePath); + processVariableDeclaration(nodePath, state); + } else if (nodePath.isClassDeclaration()) { + processClass(nodePath, state); } } function processVariableDeclaration( - variableDeclarationPath: NodePath + variableDeclarationPath: NodePath, + state: ReanimatedPluginPass ) { const declarations = variableDeclarationPath.get('declarations'); declarations.forEach((declaration) => { const initPath = declaration.get('init'); if (initPath.isExpression()) { - processWorkletizableEntity(initPath); + processWorkletizableEntity(initPath, state); } }); } -function processWorkletAggregator(objectPath: NodePath) { +function processWorkletAggregator( + objectPath: NodePath, + state: ReanimatedPluginPass +) { 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); + processWorkletizableEntity(valuePath, state); } }); } diff --git a/packages/react-native-reanimated/plugin/src/jestMatchers.ts b/packages/react-native-reanimated/plugin/src/jestMatchers.ts index 34b8dfbab1c..45b9b539f0c 100644 --- a/packages/react-native-reanimated/plugin/src/jestMatchers.ts +++ b/packages/react-native-reanimated/plugin/src/jestMatchers.ts @@ -70,11 +70,12 @@ expect.extend({ toIncludeInWorkletString(received: string, expected: string) { // Regular expression pattern to match the code field - const pattern = /code: "((?:[^"\\]|\\.)*)"/s; - const match = received.match(pattern); + // @ts-ignore This regex works well in Jest. + const pattern = /code: "((?:[^"\\]|\\.)*)"/gs; + const matches = received.match(pattern); // If a match was found and the match group 1 (content within quotes) includes the expected string - if (match && match[1].includes(expected)) { + if (matches && matches.some((match) => match.includes(expected))) { // return true; return { message: () => `Reanimated: found ${expected} in worklet string`, @@ -86,7 +87,7 @@ expect.extend({ // return false; return { message: () => - `Reanimated: expected to find ${expected} in worklet string, but it's not present`, + `Reanimated: expected to find ${expected} in worklet string, but it's not present.`, pass: false, }; }, diff --git a/packages/react-native-reanimated/plugin/src/plugin.ts b/packages/react-native-reanimated/plugin/src/plugin.ts index 3f7b41c25d7..c0f6da97103 100644 --- a/packages/react-native-reanimated/plugin/src/plugin.ts +++ b/packages/react-native-reanimated/plugin/src/plugin.ts @@ -1,23 +1,23 @@ -import type { PluginItem, NodePath } from '@babel/core'; +import type { NodePath, PluginItem } from '@babel/core'; import type { CallExpression, JSXAttribute, - Program, ObjectExpression, + Program, } from '@babel/types'; import { - processIfAutoworkletizableCallback, processCalleesAutoworkletizableCallbacks, + processIfAutoworkletizableCallback, } from './autoworkletization'; -import { WorkletizableFunction } from './types'; -import type { ReanimatedPluginPass } from './types'; -import { processIfWithWorkletDirective } from './workletSubstitution'; +import { processIfWorkletContextObject } from './contextObject'; +import { processIfWorkletFile } from './file'; +import { initializeGlobals } from './globals'; import { processInlineStylesWarning } from './inlineStylesWarning'; +import type { ReanimatedPluginPass } from './types'; +import { WorkletizableFunction } from './types'; import { addCustomGlobals } from './utils'; -import { initializeGlobals } from './globals'; import { substituteWebCallExpression } from './webOptimization'; -import { processIfWorkletFile } from './file'; -import { processIfWorkletContextObject } from './contextObject'; +import { processIfWithWorkletDirective } from './workletSubstitution'; module.exports = function (): PluginItem { function runWithTaggedExceptions(fun: () => void) { diff --git a/packages/react-native-reanimated/plugin/src/workletFactory.ts b/packages/react-native-reanimated/plugin/src/workletFactory.ts index 522ccb1c0bb..6af3c24f722 100644 --- a/packages/react-native-reanimated/plugin/src/workletFactory.ts +++ b/packages/react-native-reanimated/plugin/src/workletFactory.ts @@ -226,7 +226,20 @@ export function makeWorkletFactory( memberExpression(functionIdentifier, identifier('__closure'), false), objectExpression( variables.map((variable) => - objectProperty(identifier(variable.name), variable, false, true) + variable.name.endsWith('ClassFactory') + ? objectProperty( + identifier(variable.name), + memberExpression( + identifier( + variable.name.slice( + 0, + variable.name.length - 'ClassFactory'.length + ) + ), + identifier(variable.name) + ) + ) + : objectProperty(identifier(variable.name), variable, false, true) ) ) ) diff --git a/packages/react-native-reanimated/plugin/src/workletStringCode.ts b/packages/react-native-reanimated/plugin/src/workletStringCode.ts index 36649e9e7c1..73f7c384339 100644 --- a/packages/react-native-reanimated/plugin/src/workletStringCode.ts +++ b/packages/react-native-reanimated/plugin/src/workletStringCode.ts @@ -9,6 +9,7 @@ import type { VariableDeclaration, } from '@babel/types'; import { + callExpression, functionExpression, identifier, isArrowFunctionExpression, @@ -63,6 +64,38 @@ export function buildWorkletString( '[Reanimated] `expression.body` is not a `BlockStatement`' ); + const parsedClasses = new Set(); + + traverse(fun, { + NewExpression(path) { + // @ts-ignore fix me later + const constructorName = path.node.callee.name; + if ( + !closureVariables.some( + (variable) => variable.name === constructorName + ) || + parsedClasses.has(constructorName) + ) { + return; + } + const index = closureVariables.findIndex( + (variable) => variable.name === constructorName + ); + closureVariables.splice(index, 1); + closureVariables.push(identifier(constructorName + 'ClassFactory')); + // @ts-ignore fix me later + expression.body.body.unshift( + variableDeclaration('const', [ + variableDeclarator( + identifier(constructorName), + callExpression(identifier(constructorName + 'ClassFactory'), []) + ), + ]) + ); + parsedClasses.add(constructorName); + }, + }); + const workletFunction = functionExpression( identifier(nameWithSource), expression.params, From 26cea02f2fb5eec39e33b1d59dde156b12f0de74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Thu, 18 Jul 2024 12:55:00 +0200 Subject: [PATCH 02/10] feat: add class marker --- .../RuntimeTests/RuntimeTestsExample.tsx | 1 + .../tests/plugin/fileWorkletization.test.tsx | 21 +- .../tests/plugin/fileWorkletization.ts | 10 + .../tests/plugin/workletClasses.test.tsx | 126 + .../__snapshots__/plugin.test.ts.snap | 4119 +++++++++++------ .../__tests__/plugin.test.ts | 101 +- packages/react-native-reanimated/package.json | 3 + .../plugin/build/plugin.js | 370 +- .../plugin/package.json | 2 + .../plugin/src/class.ts | 115 +- .../plugin/src/file.ts | 26 +- .../plugin/src/plugin.ts | 9 + yarn.lock | 238 +- 13 files changed, 3516 insertions(+), 1625 deletions(-) create mode 100644 apps/common-app/src/examples/RuntimeTests/tests/plugin/workletClasses.test.tsx diff --git a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx index 314777f2a9a..995c8c9deaf 100644 --- a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx +++ b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx @@ -73,6 +73,7 @@ export default function RuntimeTestsExample() { importTest: () => { require('./tests/plugin/fileWorkletization.test'); require('./tests/plugin/contextObjects.test'); + require('./tests/plugin/workletClasses.test'); }, }, { 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 56436a39e26..c91f26dcf0d 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,7 +10,7 @@ import { test, expect, } from '../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi'; -import { getThree, implicitContextObject } from './fileWorkletization'; +import { ImplicitWorkletClass, getThree, implicitContextObject } from './fileWorkletization'; const SHARED_VALUE_REF = 'SHARED_VALUE_REF'; @@ -52,4 +52,23 @@ describe('Test file workletization', () => { const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); expect(sharedValue.onUI).toBe(5); }); + + test('WorkletClass are workletized', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + + useEffect(() => { + runOnUI(() => { + output.value = new ImplicitWorkletClass().getSeven(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(7); + }); }); 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 01f1219a8cf..a65545a8f98 100644 --- a/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.ts +++ b/apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.ts @@ -22,3 +22,13 @@ export const implicitContextObject = { return this.getFour() + 1; }, }; + +export class ImplicitWorkletClass { + getSix() { + return 6; + } + + getSeven() { + return this.getSix() + 1; + } +} diff --git a/apps/common-app/src/examples/RuntimeTests/tests/plugin/workletClasses.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/plugin/workletClasses.test.tsx new file mode 100644 index 00000000000..f9eadac5b47 --- /dev/null +++ b/apps/common-app/src/examples/RuntimeTests/tests/plugin/workletClasses.test.tsx @@ -0,0 +1,126 @@ +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'; + +class Clazz { + __workletClass = true; + value = 0; + getOne() { + return 1; + } + + getTwo() { + return this.getOne() + 1; + } + + getIncremented() { + return ++this.value; + } +} + +describe('Test worklet classes', () => { + test('class works on React runtime', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + const clazz = new Clazz(); + + output.value = clazz.getTwo() + clazz.getIncremented() + clazz.getIncremented(); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onJS).toBe(5); + }); + + test('constructor works on Worklet runtime', async () => { + const ExampleComponent = () => { + useEffect(() => { + runOnUI(() => { + const clazz = new Clazz(); + clazz.getOne(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + // TODO: assert no crash here + }); + + test('class methods work on Worklet runtime', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + + useEffect(() => { + runOnUI(() => { + const clazz = new Clazz(); + output.value = clazz.getOne(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(1); + }); + + test('class instance methods preserve binding', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + + useEffect(() => { + runOnUI(() => { + const clazz = new Clazz(); + output.value = clazz.getTwo(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(2); + }); + + test('class instances preserve state', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + + useEffect(() => { + runOnUI(() => { + const clazz = new Clazz(); + output.value = clazz.getIncremented() + clazz.getIncremented(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(3); + }); + + // TODO: Add a test that throws when class is sent from React to Worklet runtime. +}); 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 25f2957d418..3473536d62e 100644 --- a/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap +++ b/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap @@ -593,222 +593,6 @@ var Foo = function () { }();" `; -exports[`babel plugin for classes creates factories 1`] = ` -"var _worklet_4914514148035_init_data = { - code: "function _toPrimitive(t,r){if(\\"object\\"!=typeof t||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var i=e.call(t,r||\\"default\\");if(\\"object\\"!=typeof i)return i;throw new TypeError(\\"@@toPrimitive must return a primitive value.\\");}return(\\"string\\"===r?String:Number)(t);}", - location: "/dev/null", - sourceMap: "\\"mock source map\\"", - version: "x.y.z" -}; -var _toPrimitive = function () { - var _e = [new global.Error(), 1, -27]; - var _toPrimitive = function _toPrimitive(t, r) { - if ("object" != typeof t || !t) return t; - var e = t[Symbol.toPrimitive]; - if (void 0 !== e) { - var i = e.call(t, r || "default"); - if ("object" != typeof i) return i; - throw new TypeError("@@toPrimitive must return a primitive value."); - } - return ("string" === r ? String : Number)(t); - }; - _toPrimitive.__closure = {}; - _toPrimitive.__workletHash = 4914514148035; - _toPrimitive.__initData = _worklet_4914514148035_init_data; - _toPrimitive.__stackDetails = _e; - return _toPrimitive; -}(); -var _worklet_14183653944217_init_data = { - code: "function _toPropertyKey(t){const{_toPrimitive}=this.__closure;var i=_toPrimitive(t,\\"string\\");return\\"symbol\\"==typeof i?i:i+\\"\\";}", - location: "/dev/null", - sourceMap: "\\"mock source map\\"", - version: "x.y.z" -}; -var _toPropertyKey = function () { - var _e = [new global.Error(), -2, -27]; - var _toPropertyKey = function _toPropertyKey(t) { - var i = _toPrimitive(t, "string"); - return "symbol" == typeof i ? i : i + ""; - }; - _toPropertyKey.__closure = { - _toPrimitive: _toPrimitive - }; - _toPropertyKey.__workletHash = 14183653944217; - _toPropertyKey.__initData = _worklet_14183653944217_init_data; - _toPropertyKey.__stackDetails = _e; - return _toPropertyKey; -}(); -var _worklet_17099703587270_init_data = { - code: "function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError(\\"Cannot call a class as a function\\");}}", - location: "/dev/null", - sourceMap: "\\"mock source map\\"", - version: "x.y.z" -}; -var _classCallCheck = function () { - var _e = [new global.Error(), 1, -27]; - var _classCallCheck = function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - }; - _classCallCheck.__closure = {}; - _classCallCheck.__workletHash = 17099703587270; - _classCallCheck.__initData = _worklet_17099703587270_init_data; - _classCallCheck.__stackDetails = _e; - return _classCallCheck; -}(); -var _worklet_7208792264399_init_data = { - code: "function _defineProperties(target,props){const{_toPropertyKey}=this.__closure;for(var i=0;i { expect(code).toMatchSnapshot(); }); + it('workletizes ClassDeclaration', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('var ClazzClassFactory = function ()'); + expect(code).toIncludeInWorkletString('ClazzClassFactory'); + expect(code).toContain('Clazz.ClazzClassFactory = ClazzClassFactory'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ClassDeclaration in named export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('var Clazz = exports.Clazz = function ()'); + expect(code).toContain('var ClazzClassFactory = function ()'); + expect(code).toIncludeInWorkletString('ClazzClassFactory'); + expect(code).toContain('Clazz.ClazzClassFactory = ClazzClassFactory'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes ClassDeclaration in default export', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('var Clazz = exports.default = function ()'); + expect(code).toContain('var ClazzClassFactory = function ()'); + expect(code).toIncludeInWorkletString('ClazzClassFactory'); + expect(code).toContain('Clazz.ClazzClassFactory = ClazzClassFactory'); + expect(code).toMatchSnapshot(); + }); + it('workletizes multiple functions', () => { const input = html``; + + const { code } = runPlugin(input); + expect(code).not.toMatch(/__workletClass:\s*/g); + expect(code).toMatchSnapshot(); + }); + it('creates factories', () => { const input = html``; const { code } = runPlugin(input); - expect(code).toContain('var ClazzClassFactory = function ()'); - expect(code).toIncludeInWorkletString('ClazzClassFactory'); - expect(code).toContain('Clazz.ClazzClassFactory = ClazzClassFactory'); + expect(code).toContain('ClazzClassFactory'); + expect(code).toHaveWorkletData(7); + expect(code).toMatchSnapshot(); + }); + + it('workletizes regardless of marker value', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('ClazzClassFactory'); + expect(code).toHaveWorkletData(7); expect(code).toMatchSnapshot(); }); @@ -2450,10 +2533,10 @@ describe('babel plugin', () => { expect(code).toMatchSnapshot(); }); - it('keeps "this" binding', () => { + it('keeps this binding', () => { const input = html``; const { code } = runPlugin(input); - expect(code).toContain('var ClazzClassFactory = function ()'); - expect(code).toIncludeInWorkletString('ClazzClassFactory'); - expect(code).toContain('Clazz.ClazzClassFactory = ClazzClassFactory'); + expect(code).toContain('var Clazz__classFactory = function ()'); + expect(code).toIncludeInWorkletString('Clazz__classFactory'); + expect(code).toContain('Clazz.Clazz__classFactory = Clazz__classFactory'); expect(code).toMatchSnapshot(); }); @@ -2337,9 +2337,9 @@ describe('babel plugin', () => { const { code } = runPlugin(input); expect(code).toContain('var Clazz = exports.Clazz = function ()'); - expect(code).toContain('var ClazzClassFactory = function ()'); - expect(code).toIncludeInWorkletString('ClazzClassFactory'); - expect(code).toContain('Clazz.ClazzClassFactory = ClazzClassFactory'); + expect(code).toContain('var Clazz__classFactory = function ()'); + expect(code).toIncludeInWorkletString('Clazz__classFactory'); + expect(code).toContain('Clazz.Clazz__classFactory = Clazz__classFactory'); expect(code).toMatchSnapshot(); }); @@ -2355,9 +2355,9 @@ describe('babel plugin', () => { const { code } = runPlugin(input); expect(code).toContain('var Clazz = exports.default = function ()'); - expect(code).toContain('var ClazzClassFactory = function ()'); - expect(code).toIncludeInWorkletString('ClazzClassFactory'); - expect(code).toContain('Clazz.ClazzClassFactory = ClazzClassFactory'); + expect(code).toContain('var Clazz__classFactory = function ()'); + expect(code).toIncludeInWorkletString('Clazz__classFactory'); + expect(code).toContain('Clazz.Clazz__classFactory = Clazz__classFactory'); expect(code).toMatchSnapshot(); }); @@ -2486,8 +2486,8 @@ describe('babel plugin', () => { `; const { code } = runPlugin(input); - expect(code).toContain('ClazzClassFactory'); - expect(code).toHaveWorkletData(7); + expect(code).toContain('Clazz__classFactory'); + expect(code).toIncludeInWorkletString('Clazz__classFactory'); expect(code).toMatchSnapshot(); }); @@ -2502,8 +2502,8 @@ describe('babel plugin', () => { `; const { code } = runPlugin(input); - expect(code).toContain('ClazzClassFactory'); - expect(code).toHaveWorkletData(7); + expect(code).toContain('Clazz__classFactory'); + expect(code).toIncludeInWorkletString('Clazz__classFactory'); expect(code).toMatchSnapshot(); }); @@ -2516,7 +2516,7 @@ describe('babel plugin', () => { `; const { code } = runPlugin(input); - expect(code).toContain('ClazzClassFactory'); + expect(code).toContain('Clazz__classFactory'); expect(code).toMatchSnapshot(); }); @@ -2529,7 +2529,7 @@ describe('babel plugin', () => { `; const { code } = runPlugin(input); - expect(code).toContain('ClazzClassFactory: Clazz.ClazzClassFactory'); + expect(code).toContain('Clazz__classFactory: Clazz.Clazz__classFactory'); expect(code).toMatchSnapshot(); }); @@ -2546,11 +2546,10 @@ describe('babel plugin', () => { const { code } = runPlugin(input); expect(code).toIncludeInWorkletString('this.member'); - expect(code).toHaveWorkletData(8); expect(code).toMatchSnapshot(); }); - it('appends necessary polyfills', () => { + it('appends polyfills', () => { const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(6); + expect(code).toMatchSnapshot(); + }); }); }); diff --git a/packages/react-native-reanimated/plugin/build/plugin.js b/packages/react-native-reanimated/plugin/build/plugin.js index 6ab6484500a..34bd7955650 100644 --- a/packages/react-native-reanimated/plugin/build/plugin.js +++ b/packages/react-native-reanimated/plugin/build/plugin.js @@ -30,7 +30,7 @@ var require_types = __commonJS({ "lib/types.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.isWorkletizableObjectNode = exports2.isWorkletizableObjectPath = exports2.isWorkletizableFunctionNode = exports2.isWorkletizableFunctionPath = exports2.WorkletizableObject = exports2.WorkletizableFunction = void 0; + exports2.workletClassFactorySuffix = 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"; @@ -50,6 +50,7 @@ var require_types = __commonJS({ return (0, types_12.isObjectExpression)(node); } exports2.isWorkletizableObjectNode = isWorkletizableObjectNode; + exports2.workletClassFactorySuffix = "__classFactory"; } }); @@ -176,7 +177,8 @@ var require_utils = __commonJS({ "lib/utils.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.addCustomGlobals = exports2.isRelease = void 0; + exports2.replaceWithFactoryCall = exports2.addCustomGlobals = exports2.isRelease = void 0; + var types_12 = require("@babel/types"); var globals_12 = require_globals(); function isRelease() { var _a, _b; @@ -192,6 +194,20 @@ var require_utils = __commonJS({ } } exports2.addCustomGlobals = addCustomGlobals; + function replaceWithFactoryCall(toReplace, name, factoryCall) { + if (!name || !needsDeclaration(toReplace)) { + toReplace.replaceWith(factoryCall); + } else { + const replacement = (0, types_12.variableDeclaration)("const", [ + (0, types_12.variableDeclarator)((0, types_12.identifier)(name), factoryCall) + ]); + toReplace.replaceWith(replacement); + } + } + exports2.replaceWithFactoryCall = replaceWithFactoryCall; + function needsDeclaration(nodePath) { + return (0, types_12.isScopable)(nodePath.parent) || (0, types_12.isExportNamedDeclaration)(nodePath.parent); + } } }); @@ -242,6 +258,7 @@ var require_workletStringCode = __commonJS({ var assert_1 = require("assert"); var convertSourceMap = __importStar(require("convert-source-map")); var fs = __importStar(require("fs")); + var types_2 = require_types(); var utils_12 = require_utils(); var MOCK_SOURCE_MAP = "mock source map"; function buildWorkletString(fun, state, closureVariables, nameWithSource, inputMap) { @@ -263,10 +280,11 @@ var require_workletStringCode = __commonJS({ } const index = closureVariables.findIndex((variable) => variable.name === constructorName); closureVariables.splice(index, 1); - closureVariables.push((0, types_12.identifier)(constructorName + "ClassFactory")); + const workletClassFactoryName = constructorName + types_2.workletClassFactorySuffix; + closureVariables.push((0, types_12.identifier)(workletClassFactoryName)); (0, types_12.assertBlockStatement)(expression.body); expression.body.body.unshift((0, types_12.variableDeclaration)("const", [ - (0, types_12.variableDeclarator)((0, types_12.identifier)(constructorName), (0, types_12.callExpression)((0, types_12.identifier)(constructorName + "ClassFactory"), [])) + (0, types_12.variableDeclarator)((0, types_12.identifier)(constructorName), (0, types_12.callExpression)((0, types_12.identifier)(workletClassFactoryName), [])) ])); parsedClasses.add(constructorName); } @@ -369,9 +387,10 @@ var require_workletFactory = __commonJS({ var types_12 = require("@babel/types"); var assert_1 = require("assert"); var path_1 = require("path"); - var workletStringCode_1 = require_workletStringCode(); var globals_12 = require_globals(); + var types_2 = require_types(); var utils_12 = require_utils(); + var workletStringCode_1 = require_workletStringCode(); var REAL_VERSION = require("../../package.json").version; var MOCK_VERSION = "x.y.z"; var workletStringTransformPresets = [ @@ -456,7 +475,7 @@ var require_workletFactory = __commonJS({ (0, types_12.variableDeclaration)("const", [ (0, types_12.variableDeclarator)(functionIdentifier, funExpression) ]), - (0, types_12.expressionStatement)((0, types_12.assignmentExpression)("=", (0, types_12.memberExpression)(functionIdentifier, (0, types_12.identifier)("__closure"), false), (0, types_12.objectExpression)(variables.map((variable) => variable.name.endsWith("ClassFactory") ? (0, types_12.objectProperty)((0, types_12.identifier)(variable.name), (0, types_12.memberExpression)((0, types_12.identifier)(variable.name.slice(0, variable.name.length - "ClassFactory".length)), (0, types_12.identifier)(variable.name))) : (0, types_12.objectProperty)((0, types_12.identifier)(variable.name), variable, false, true))))), + (0, types_12.expressionStatement)((0, types_12.assignmentExpression)("=", (0, types_12.memberExpression)(functionIdentifier, (0, types_12.identifier)("__closure"), false), (0, types_12.objectExpression)(variables.map((variable) => variable.name.endsWith(types_2.workletClassFactorySuffix) ? (0, types_12.objectProperty)((0, types_12.identifier)(variable.name), (0, types_12.memberExpression)((0, types_12.identifier)(variable.name.slice(0, variable.name.length - types_2.workletClassFactorySuffix.length)), (0, types_12.identifier)(variable.name))) : (0, types_12.objectProperty)((0, types_12.identifier)(variable.name), variable, false, true))))), (0, types_12.expressionStatement)((0, types_12.assignmentExpression)("=", (0, types_12.memberExpression)(functionIdentifier, (0, types_12.identifier)("__workletHash"), false), (0, types_12.numericLiteral)(workletHash))) ]; if (shouldIncludeInitData) { @@ -610,10 +629,11 @@ var require_workletSubstitution = __commonJS({ "lib/workletSubstitution.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.maybeSubstituteFunctionDeclarationWithVariableDeclaration = exports2.substituteObjectMethodWithObjectProperty = exports2.processWorklet = exports2.processIfWithWorkletDirective = void 0; + exports2.substituteObjectMethodWithObjectProperty = exports2.processWorklet = exports2.processIfWithWorkletDirective = void 0; var types_12 = require("@babel/types"); - var workletFactoryCall_1 = require_workletFactoryCall(); var types_2 = require_types(); + var utils_12 = require_utils(); + var workletFactoryCall_1 = require_workletFactoryCall(); function processIfWithWorkletDirective(path, state) { if (!(0, types_12.isBlockStatement)(path.node.body)) { return false; @@ -634,19 +654,19 @@ var require_workletSubstitution = __commonJS({ }, state); } const workletFactoryCall = (0, workletFactoryCall_1.makeWorkletFactoryCall)(path, state); - substituteWithWorkletFactoryCall(path, workletFactoryCall); + substituteWorkletWithWorkletFactoryCall(path, workletFactoryCall); } exports2.processWorklet = processWorklet; function hasWorkletDirective(directives) { return directives.some((directive) => (0, types_12.isDirectiveLiteral)(directive.value) && directive.value.value === "worklet"); } - function substituteWithWorkletFactoryCall(path, workletFactoryCall) { + function substituteWorkletWithWorkletFactoryCall(path, workletFactoryCall) { + var _a; if (path.isObjectMethod()) { substituteObjectMethodWithObjectProperty(path, workletFactoryCall); - } else if (path.isFunctionDeclaration()) { - maybeSubstituteFunctionDeclarationWithVariableDeclaration(path, workletFactoryCall); } else { - path.replaceWith(workletFactoryCall); + const name = "id" in path.node ? (_a = path.node.id) === null || _a === void 0 ? void 0 : _a.name : void 0; + (0, utils_12.replaceWithFactoryCall)(path, name, workletFactoryCall); } } function substituteObjectMethodWithObjectProperty(path, workletFactoryCall) { @@ -654,14 +674,6 @@ var require_workletSubstitution = __commonJS({ path.replaceWith(replacement); } exports2.substituteObjectMethodWithObjectProperty = substituteObjectMethodWithObjectProperty; - function maybeSubstituteFunctionDeclarationWithVariableDeclaration(path, workletFactoryCall) { - const needDeclaration = (0, types_12.isScopable)(path.parent) || (0, types_12.isExportNamedDeclaration)(path.parent); - const replacement = "id" in path.node && path.node.id && needDeclaration ? (0, types_12.variableDeclaration)("const", [ - (0, types_12.variableDeclarator)(path.node.id, workletFactoryCall) - ]) : workletFactoryCall; - path.replaceWith(replacement); - } - exports2.maybeSubstituteFunctionDeclarationWithVariableDeclaration = maybeSubstituteFunctionDeclarationWithVariableDeclaration; } }); @@ -1316,11 +1328,12 @@ var require_class = __commonJS({ Object.defineProperty(exports2, "__esModule", { value: true }); exports2.processIfWorkletClass = void 0; var core_1 = require("@babel/core"); - var types_12 = require("@babel/types"); var generator_1 = __importDefault(require("@babel/generator")); - var types_2 = require_types(); var traverse_1 = __importDefault(require("@babel/traverse")); + var types_12 = require("@babel/types"); var assert_1 = require("assert"); + var types_2 = require_types(); + var utils_12 = require_utils(); var classWorkletMarker = "__workletClass"; function processIfWorkletClass(classPath, state) { if (!classPath.node.id) { @@ -1329,10 +1342,6 @@ var require_class = __commonJS({ if (!hasWorkletClassMarker(classPath.node.body)) { return false; } - const parentPath = classPath.parentPath; - const className = classPath.node.id.name; - classPath.skip(); - classPath = makeStatement(classPath, parentPath, className); removeWorkletClassMarker(classPath.node.body); processClass(classPath, state); return true; @@ -1341,8 +1350,18 @@ var require_class = __commonJS({ function processClass(classPath, state) { (0, assert_1.strict)(classPath.node.id); const className = classPath.node.id.name; - const code = (0, generator_1.default)(classPath.node).code; - const transformedCode = (0, core_1.transformSync)(code, { + const polyfilledClassAst = getPolyfilledAst(classPath.node, state); + appendWorkletDirectiveToPolyfills(polyfilledClassAst.program.body); + replaceClassDeclarationWithFactoryAndCall(polyfilledClassAst.program.body, className); + sortPolyfills(polyfilledClassAst); + polyfilledClassAst.program.body.push((0, types_12.returnStatement)((0, types_12.identifier)(className))); + const factoryFactory = (0, types_12.functionExpression)(null, [], (0, types_12.blockStatement)([...polyfilledClassAst.program.body])); + const factoryCall = (0, types_12.callExpression)(factoryFactory, []); + (0, utils_12.replaceWithFactoryCall)(classPath, className, factoryCall); + } + function getPolyfilledAst(classNode, state) { + const classCode = (0, generator_1.default)(classNode).code; + const classWithPolyfills = (0, core_1.transformSync)(classCode, { plugins: [ "@babel/plugin-transform-class-properties", "@babel/plugin-transform-classes", @@ -1353,76 +1372,38 @@ var require_class = __commonJS({ babelrc: false, configFile: false }); - (0, assert_1.strict)(transformedCode); - (0, assert_1.strict)(transformedCode.ast); - const ast = transformedCode.ast; - let factory; - let hasHandledClass = false; - (0, traverse_1.default)(ast, { - [types_2.WorkletizableFunction]: { - enter: (functionPath) => { - if (functionPath.parentPath.isObjectProperty()) { - return; - } - const workletDirective = (0, types_12.directive)((0, types_12.directiveLiteral)("worklet")); - if (functionPath.parentPath.isCallExpression() && !hasHandledClass) { - const factoryCopy = (0, types_12.cloneNode)(functionPath.node, true); - factoryCopy.id = (0, types_12.identifier)(className + "ClassFactory"); - factoryCopy.body.directives.push(workletDirective); - factory = (0, types_12.variableDeclaration)("const", [ - (0, types_12.variableDeclarator)((0, types_12.identifier)(className + "ClassFactory"), factoryCopy) - ]); - hasHandledClass = true; - return; - } - const bodyPath = functionPath.get("body"); - if (!bodyPath.isBlockStatement()) { - bodyPath.replaceWith((0, types_12.blockStatement)([(0, types_12.returnStatement)(bodyPath.node)])); - } - functionPath.node.body.directives.push(workletDirective); - } + (0, assert_1.strict)(classWithPolyfills && classWithPolyfills.ast); + return classWithPolyfills.ast; + } + function appendWorkletDirectiveToPolyfills(statements) { + statements.forEach((statement) => { + if ((0, types_12.isFunctionDeclaration)(statement)) { + const workletDirective = (0, types_12.directive)((0, types_12.directiveLiteral)("worklet")); + statement.body.directives.push(workletDirective); } }); - const body = ast.program.body; - body.push(factory); - body.push((0, types_12.expressionStatement)((0, types_12.assignmentExpression)("=", (0, types_12.memberExpression)((0, types_12.identifier)(className), (0, types_12.identifier)(className + "ClassFactory")), (0, types_12.identifier)(className + "ClassFactory")))); - sortPolyfills(ast); - const transformedNewCode = (0, core_1.transformSync)((0, generator_1.default)(ast).code, { - ast: true, - filename: state.file.opts.filename - }); - (0, assert_1.strict)(transformedNewCode); - (0, assert_1.strict)(transformedNewCode.ast); - const parent = classPath.parent; - const index = parent.body.findIndex((node) => node === classPath.node); - parent.body.splice(index, 1, ...transformedNewCode.ast.program.body); - } - function makeStatement(classPath, parentPath, className) { - if (parentPath.isExportDefaultDeclaration()) { - return splitDefaultExportClassDeclaration(parentPath, className); - } else if (parentPath.isExportNamedDeclaration()) { - return splitNamedExportClassDeclaration(parentPath, className); - } else { - return classPath; - } } - function splitDefaultExportClassDeclaration(exportPath, name) { - const identifierExport = (0, types_12.exportDefaultDeclaration)((0, types_12.identifier)(name)); - const newClassPath = exportPath.replaceWithMultiple([ - exportPath.node.declaration, - identifierExport - ])[0]; - return newClassPath; - } - function splitNamedExportClassDeclaration(exportPath, name) { - const identifierExport = (0, types_12.exportNamedDeclaration)(null, [ - (0, types_12.exportSpecifier)((0, types_12.identifier)(name), (0, types_12.identifier)(name)) + function replaceClassDeclarationWithFactoryAndCall(statements, className) { + const classFactoryName = className + types_2.workletClassFactorySuffix; + const classDeclarationIndex = getPolyfilledClassDeclarationIndex(statements, className); + const classDeclarationToReplace = statements[classDeclarationIndex]; + const classDeclarationInit = classDeclarationToReplace.declarations[0].init; + const classFactoryDeclaration = (0, types_12.functionDeclaration)((0, types_12.identifier)(classFactoryName), [], (0, types_12.blockStatement)([ + (0, types_12.variableDeclaration)("const", [ + (0, types_12.variableDeclarator)((0, types_12.identifier)(className), classDeclarationInit) + ]), + (0, types_12.expressionStatement)((0, types_12.assignmentExpression)("=", (0, types_12.memberExpression)((0, types_12.identifier)(className), (0, types_12.identifier)(classFactoryName)), (0, types_12.identifier)(classFactoryName))), + (0, types_12.returnStatement)((0, types_12.identifier)(className)) + ], [(0, types_12.directive)((0, types_12.directiveLiteral)("worklet"))])); + const newClassDeclaration = (0, types_12.variableDeclaration)("const", [ + (0, types_12.variableDeclarator)((0, types_12.identifier)(className), (0, types_12.callExpression)((0, types_12.identifier)(classFactoryName), [])) ]); - const newClassPath = exportPath.replaceWithMultiple([ - exportPath.node.declaration, - identifierExport - ])[0]; - return newClassPath; + statements.splice(classDeclarationIndex, 1, classFactoryDeclaration, newClassDeclaration); + } + function getPolyfilledClassDeclarationIndex(statements, className) { + const index = statements.findIndex((statement) => (0, types_12.isVariableDeclaration)(statement) && statement.declarations.some((declaration) => (0, types_12.isIdentifier)(declaration.id) && declaration.id.name === className)); + (0, assert_1.strict)(index >= 0); + return index; } function hasWorkletClassMarker(classBody) { return classBody.body.some((statement) => (0, types_12.isClassProperty)(statement) && (0, types_12.isIdentifier)(statement.key) && statement.key.name === classWorkletMarker); diff --git a/packages/react-native-reanimated/plugin/src/class.ts b/packages/react-native-reanimated/plugin/src/class.ts index 14bb5606fe3..3ea940dcea2 100644 --- a/packages/react-native-reanimated/plugin/src/class.ts +++ b/packages/react-native-reanimated/plugin/src/class.ts @@ -1,41 +1,40 @@ -import { transformSync } from '@babel/core'; import type { NodePath } from '@babel/core'; +import { transformSync } from '@babel/core'; +import generate from '@babel/generator'; +import traverse from '@babel/traverse'; +import type { + File as BabelFile, + CallExpression, + ClassBody, + ClassDeclaration, + Identifier, + Program, + Statement, + VariableDeclaration, +} from '@babel/types'; import { assignmentExpression, blockStatement, - cloneNode, + callExpression, directive, directiveLiteral, - exportDefaultDeclaration, - exportNamedDeclaration, - exportSpecifier, expressionStatement, + functionDeclaration, + functionExpression, identifier, isClassProperty, + isFunctionDeclaration, isIdentifier, + isVariableDeclaration, memberExpression, returnStatement, variableDeclaration, variableDeclarator, } from '@babel/types'; -import type { - BlockStatement, - ClassDeclaration, - Expression, - FunctionExpression, - Program, - File as BabelFile, - VariableDeclaration, - Identifier, - ClassBody, - ExportDefaultDeclaration, - ExportNamedDeclaration, -} from '@babel/types'; -import type { ReanimatedPluginPass } from './types'; -import generate from '@babel/generator'; -import { WorkletizableFunction } from './types'; -import traverse from '@babel/traverse'; import { strict as assert } from 'assert'; +import type { ReanimatedPluginPass } from './types'; +import { workletClassFactorySuffix } from './types'; +import { replaceWithFactoryCall } from './utils'; const classWorkletMarker = '__workletClass'; @@ -52,13 +51,6 @@ export function processIfWorkletClass( return false; } - const parentPath = classPath.parentPath; - const className = classPath.node.id.name; - - classPath.skip(); - - classPath = makeStatement(classPath, parentPath, className); - removeWorkletClassMarker(classPath.node.body); processClass(classPath, state); @@ -72,146 +64,182 @@ function processClass( ) { assert(classPath.node.id); const className = classPath.node.id.name; - const code = generate(classPath.node).code; - const transformedCode = transformSync(code, { - plugins: [ - '@babel/plugin-transform-class-properties', - '@babel/plugin-transform-classes', - '@babel/plugin-transform-unicode-regex', - ], - filename: state.file.opts.filename, - ast: true, - babelrc: false, - configFile: false, - }); + const polyfilledClassAst = getPolyfilledAst(classPath.node, state); - assert(transformedCode); - assert(transformedCode.ast); + appendWorkletDirectiveToPolyfills(polyfilledClassAst.program.body); - const ast = transformedCode.ast; + replaceClassDeclarationWithFactoryAndCall( + polyfilledClassAst.program.body, + className + ); - let factory: VariableDeclaration; + sortPolyfills(polyfilledClassAst); - let hasHandledClass = false; + polyfilledClassAst.program.body.push(returnStatement(identifier(className))); - traverse(ast, { - [WorkletizableFunction]: { - // @ts-expect-error TS has some trouble inferring here. - enter: (functionPath: NodePath) => { - if (functionPath.parentPath.isObjectProperty()) { - return; - } - - const workletDirective = directive(directiveLiteral('worklet')); - - if (functionPath.parentPath.isCallExpression() && !hasHandledClass) { - const factoryCopy = cloneNode( - functionPath.node, - true - ) as FunctionExpression; - factoryCopy.id = identifier(className + 'ClassFactory'); - factoryCopy.body.directives.push(workletDirective); - factory = variableDeclaration('const', [ - variableDeclarator( - identifier(className + 'ClassFactory'), - factoryCopy - ), - ]); - hasHandledClass = true; - - return; - } - - const bodyPath = functionPath.get('body'); - if (!bodyPath.isBlockStatement()) { - bodyPath.replaceWith( - blockStatement([returnStatement(bodyPath.node as Expression)]) - ); - } - - (functionPath.node.body as BlockStatement).directives.push( - workletDirective - ); - }, - }, - }); + const factoryFactory = functionExpression( + null, + [], + blockStatement([...polyfilledClassAst.program.body]) + ); - const body = ast.program.body; - body.push(factory!); + const factoryCall = callExpression(factoryFactory, []); - body.push( - expressionStatement( - assignmentExpression( - '=', - memberExpression( - identifier(className), - identifier(className + 'ClassFactory') - ), - identifier(className + 'ClassFactory') - ) - ) - ); + replaceWithFactoryCall(classPath, className, factoryCall); + // const body = classWithPolyfills.ast.program.body; + // body.push(factory!); - sortPolyfills(ast); + // body.push( + // expressionStatement( + // assignmentExpression( + // '=', + // memberExpression( + // identifier(className), + // identifier(className + classFactorySuffix) + // ), + // identifier(className + classFactorySuffix) + // ) + // ) + // ); - const transformedNewCode = transformSync(generate(ast).code, { - ast: true, - filename: state.file.opts.filename, - }); + // sortPolyfills(classWithPolyfills.ast); + + // const transformedNewCode = transformSync( + // generate(classWithPolyfills.ast).code, + // { + // ast: true, + // filename: state.file.opts.filename, + // } + // ); + + // assert(transformedNewCode); + // assert(transformedNewCode.ast); - assert(transformedNewCode); - assert(transformedNewCode.ast); + // // const needsDeclaration = needsDeclaration(classPath.parent); - const parent = classPath.parent as Program; + // const parent = classPath.parent as Program; - const index = parent.body.findIndex((node) => node === classPath.node); + // const index = parent.body.findIndex((node) => node === classPath.node); - parent.body.splice(index, 1, ...transformedNewCode.ast.program.body); + // parent.body.splice(index, 1, ...transformedNewCode.ast.program.body); + // #endregion } -function makeStatement( - classPath: NodePath, - parentPath: NodePath, - className: string +function getPolyfilledAst( + classNode: ClassDeclaration, + state: ReanimatedPluginPass ) { - if (parentPath.isExportDefaultDeclaration()) { - return splitDefaultExportClassDeclaration(parentPath, className); - } else if (parentPath.isExportNamedDeclaration()) { - return splitNamedExportClassDeclaration(parentPath, className); - } else { - return classPath; - } -} + const classCode = generate(classNode).code; + + const classWithPolyfills = transformSync(classCode, { + plugins: [ + '@babel/plugin-transform-class-properties', + '@babel/plugin-transform-classes', + '@babel/plugin-transform-unicode-regex', + ], + filename: state.file.opts.filename, + ast: true, + babelrc: false, + configFile: false, + }); -function splitDefaultExportClassDeclaration( - exportPath: NodePath, - name: string -): NodePath { - const identifierExport = exportDefaultDeclaration(identifier(name)); + assert(classWithPolyfills && classWithPolyfills.ast); - const newClassPath = exportPath.replaceWithMultiple([ - exportPath.node.declaration, - identifierExport, - ])[0] as NodePath; + return classWithPolyfills.ast; +} - return newClassPath; +function appendWorkletDirectiveToPolyfills(statements: Statement[]) { + statements.forEach((statement) => { + if (isFunctionDeclaration(statement)) { + const workletDirective = directive(directiveLiteral('worklet')); + statement.body.directives.push(workletDirective); + } + }); } -function splitNamedExportClassDeclaration( - exportPath: NodePath, - name: string +/** + * Replaces + * ```ts + * const Clazz = ...; + * ``` + * with + * ```ts + * const Clazz__classFactory = ...; + * const Clazz = Clazz__classFactory(); + * ``` + */ +function replaceClassDeclarationWithFactoryAndCall( + statements: Statement[], + className: string ) { - const identifierExport = exportNamedDeclaration(null, [ - exportSpecifier(identifier(name), identifier(name)), + const classFactoryName = className + workletClassFactorySuffix; + + const classDeclarationIndex = getPolyfilledClassDeclarationIndex( + statements, + className + ); + + const classDeclarationToReplace = statements[ + classDeclarationIndex + ] as VariableDeclaration; + + const classDeclarationInit = classDeclarationToReplace.declarations[0] + .init as CallExpression; + + const classFactoryDeclaration = functionDeclaration( + identifier(classFactoryName), + [], + blockStatement( + [ + variableDeclaration('const', [ + variableDeclarator(identifier(className), classDeclarationInit), + ]), + expressionStatement( + assignmentExpression( + '=', + memberExpression( + identifier(className), + identifier(classFactoryName) + ), + identifier(classFactoryName) + ) + ), + returnStatement(identifier(className)), + ], + [directive(directiveLiteral('worklet'))] + ) + ); + + const newClassDeclaration = variableDeclaration('const', [ + variableDeclarator( + identifier(className), + callExpression(identifier(classFactoryName), []) + ), ]); - const newClassPath = exportPath.replaceWithMultiple([ - exportPath.node.declaration!, - identifierExport, - ])[0] as NodePath; + statements.splice( + classDeclarationIndex, + 1, + classFactoryDeclaration, + newClassDeclaration + ); +} - return newClassPath; +function getPolyfilledClassDeclarationIndex( + statements: Statement[], + className: string +) { + const index = statements.findIndex( + (statement) => + isVariableDeclaration(statement) && + statement.declarations.some( + (declaration) => + isIdentifier(declaration.id) && declaration.id.name === className + ) + ); + assert(index >= 0); + return index; } function hasWorkletClassMarker(classBody: ClassBody) { @@ -250,8 +278,8 @@ function sortPolyfills(ast: BabelFile) { } } -function getPolyfillsToSort(ast: BabelFile): SortElement[] { - const polyfills: SortElement[] = []; +function getPolyfillsToSort(ast: BabelFile): Polyfill[] { + const polyfills: Polyfill[] = []; traverse(ast, { Program: { @@ -263,7 +291,7 @@ function getPolyfillsToSort(ast: BabelFile): SortElement[] { return; } - const element: SortElement = { + const element: Polyfill = { name: statement.node.id.name, index, dependencies: new Set(), @@ -290,8 +318,8 @@ function getPolyfillsToSort(ast: BabelFile): SortElement[] { return polyfills; } -function topoSort(toSort: SortElement[]): SortElement[] { - const sorted: SortElement[] = []; +function topoSort(toSort: Polyfill[]): Polyfill[] { + const sorted: Polyfill[] = []; const stack: Set = new Set(); for (const element of toSort) { recursiveTopoSort(element, toSort, sorted, stack); @@ -300,9 +328,9 @@ function topoSort(toSort: SortElement[]): SortElement[] { } function recursiveTopoSort( - current: SortElement, - toSort: SortElement[], - sorted: SortElement[], + current: Polyfill, + toSort: Polyfill[], + sorted: Polyfill[], stack: Set ) { if (stack.has(current.name)) { @@ -324,7 +352,7 @@ function recursiveTopoSort( stack.delete(current.name); } -type SortElement = { +type Polyfill = { name: string; index: number; dependencies: Set; diff --git a/packages/react-native-reanimated/plugin/src/types.ts b/packages/react-native-reanimated/plugin/src/types.ts index d6e3a99de4f..d7b96e041cd 100644 --- a/packages/react-native-reanimated/plugin/src/types.ts +++ b/packages/react-native-reanimated/plugin/src/types.ts @@ -1,18 +1,18 @@ import type { BabelFile, NodePath } from '@babel/core'; +import type { + ArrowFunctionExpression, + Node as BabelNode, + FunctionDeclaration, + FunctionExpression, + ObjectExpression, + ObjectMethod, +} from '@babel/types'; import { isArrowFunctionExpression, isFunctionDeclaration, isFunctionExpression, - isObjectMethod, isObjectExpression, -} from '@babel/types'; -import type { - FunctionDeclaration, - FunctionExpression, - ObjectMethod, - ArrowFunctionExpression, - ObjectExpression, - Node as BabelNode, + isObjectMethod, } from '@babel/types'; export interface ReanimatedPluginOptions { @@ -83,3 +83,5 @@ export function isWorkletizableObjectNode( ): node is WorkletizableObject { return isObjectExpression(node); } + +export const workletClassFactorySuffix = '__classFactory'; diff --git a/packages/react-native-reanimated/plugin/src/utils.ts b/packages/react-native-reanimated/plugin/src/utils.ts index 45899184d7d..95948b80f57 100644 --- a/packages/react-native-reanimated/plugin/src/utils.ts +++ b/packages/react-native-reanimated/plugin/src/utils.ts @@ -1,3 +1,12 @@ +import type { NodePath } from '@babel/traverse'; +import type { CallExpression } from '@babel/types'; +import { + identifier, + isExportNamedDeclaration, + isScopable, + variableDeclaration, + variableDeclarator, +} from '@babel/types'; import { globals } from './globals'; import type { ReanimatedPluginPass } from './types'; @@ -22,3 +31,55 @@ export function addCustomGlobals(this: ReanimatedPluginPass) { }); } } + +/** + * This function replaces the node with a factory call while making + * sure that it's a legal operation. + * If the node cannot be simply replaced with a factory call, it will + * be replaced with a variable declaration. + * + * For example: + * ```ts + * const foo = function() { + * 'worklet'; + * return 1; + * }; + * ``` + * becomes + * ```ts + * const foo = factoryCall(); + * ``` + * But: + * ``` + * export function foo() { + * 'worklet'; + * return 1; + * }; + * ``` + * + * becomes + * + * ```ts + * const foo = factoryCall(); + * ``` + */ +export function replaceWithFactoryCall( + toReplace: NodePath, + name: string | undefined, + factoryCall: CallExpression +) { + if (!name || !needsDeclaration(toReplace)) { + toReplace.replaceWith(factoryCall); + } else { + const replacement = variableDeclaration('const', [ + variableDeclarator(identifier(name), factoryCall), + ]); + toReplace.replaceWith(replacement); + } +} + +function needsDeclaration(nodePath: NodePath): boolean { + return ( + isScopable(nodePath.parent) || isExportNamedDeclaration(nodePath.parent) + ); +} diff --git a/packages/react-native-reanimated/plugin/src/workletFactory.ts b/packages/react-native-reanimated/plugin/src/workletFactory.ts index 6af3c24f722..931e3ecc301 100644 --- a/packages/react-native-reanimated/plugin/src/workletFactory.ts +++ b/packages/react-native-reanimated/plugin/src/workletFactory.ts @@ -39,10 +39,11 @@ import { } from '@babel/types'; import { strict as assert } from 'assert'; import { basename, relative } from 'path'; -import { buildWorkletString } from './workletStringCode'; import { globals } from './globals'; import type { ReanimatedPluginPass, WorkletizableFunction } from './types'; +import { workletClassFactorySuffix } from './types'; import { isRelease } from './utils'; +import { buildWorkletString } from './workletStringCode'; const REAL_VERSION = require('../../package.json').version; const MOCK_VERSION = 'x.y.z'; @@ -226,14 +227,14 @@ export function makeWorkletFactory( memberExpression(functionIdentifier, identifier('__closure'), false), objectExpression( variables.map((variable) => - variable.name.endsWith('ClassFactory') + variable.name.endsWith(workletClassFactorySuffix) ? objectProperty( identifier(variable.name), memberExpression( identifier( variable.name.slice( 0, - variable.name.length - 'ClassFactory'.length + variable.name.length - workletClassFactorySuffix.length ) ), identifier(variable.name) diff --git a/packages/react-native-reanimated/plugin/src/workletStringCode.ts b/packages/react-native-reanimated/plugin/src/workletStringCode.ts index 6cd47c06a50..5d58e683f60 100644 --- a/packages/react-native-reanimated/plugin/src/workletStringCode.ts +++ b/packages/react-native-reanimated/plugin/src/workletStringCode.ts @@ -31,8 +31,9 @@ import { import { strict as assert } from 'assert'; import * as convertSourceMap from 'convert-source-map'; import * as fs from 'fs'; -import { isRelease } from './utils'; import type { ReanimatedPluginPass, WorkletizableFunction } from './types'; +import { workletClassFactorySuffix } from './types'; +import { isRelease } from './utils'; const MOCK_SOURCE_MAP = 'mock source map'; @@ -86,14 +87,16 @@ export function buildWorkletString( (variable) => variable.name === constructorName ); closureVariables.splice(index, 1); - closureVariables.push(identifier(constructorName + 'ClassFactory')); + const workletClassFactoryName = + constructorName + workletClassFactorySuffix; + closureVariables.push(identifier(workletClassFactoryName)); assertBlockStatement(expression.body); expression.body.body.unshift( variableDeclaration('const', [ variableDeclarator( identifier(constructorName), - callExpression(identifier(constructorName + 'ClassFactory'), []) + callExpression(identifier(workletClassFactoryName), []) ), ]) ); diff --git a/packages/react-native-reanimated/plugin/src/workletSubstitution.ts b/packages/react-native-reanimated/plugin/src/workletSubstitution.ts index 0d30e09fe5c..42db4ca07f1 100644 --- a/packages/react-native-reanimated/plugin/src/workletSubstitution.ts +++ b/packages/react-native-reanimated/plugin/src/workletSubstitution.ts @@ -1,22 +1,14 @@ import type { NodePath } from '@babel/core'; +import type { CallExpression, Directive, ObjectMethod } from '@babel/types'; import { isBlockStatement, isDirectiveLiteral, objectProperty, - variableDeclaration, - variableDeclarator, - isScopable, - isExportNamedDeclaration, } from '@babel/types'; -import type { - Directive, - ObjectMethod, - CallExpression, - FunctionDeclaration, -} from '@babel/types'; -import { makeWorkletFactoryCall } from './workletFactoryCall'; import type { ReanimatedPluginPass } from './types'; import { WorkletizableFunction } from './types'; +import { replaceWithFactoryCall } from './utils'; +import { makeWorkletFactoryCall } from './workletFactoryCall'; /** * @@ -69,7 +61,7 @@ export function processWorklet( const workletFactoryCall = makeWorkletFactoryCall(path, state); - substituteWithWorkletFactoryCall(path, workletFactoryCall); + substituteWorkletWithWorkletFactoryCall(path, workletFactoryCall); } function hasWorkletDirective(directives: Directive[]): boolean { @@ -79,19 +71,15 @@ function hasWorkletDirective(directives: Directive[]): boolean { ); } -function substituteWithWorkletFactoryCall( +function substituteWorkletWithWorkletFactoryCall( path: NodePath, workletFactoryCall: CallExpression ): void { if (path.isObjectMethod()) { substituteObjectMethodWithObjectProperty(path, workletFactoryCall); - } else if (path.isFunctionDeclaration()) { - maybeSubstituteFunctionDeclarationWithVariableDeclaration( - path, - workletFactoryCall - ); } else { - path.replaceWith(workletFactoryCall); + const name = 'id' in path.node ? path.node.id?.name : undefined; + replaceWithFactoryCall(path, name, workletFactoryCall); } } @@ -103,26 +91,25 @@ export function substituteObjectMethodWithObjectProperty( path.replaceWith(replacement); } -export function maybeSubstituteFunctionDeclarationWithVariableDeclaration( - path: NodePath, - workletFactoryCall: CallExpression -): void { - // We check if function needs to be assigned to variable declaration. - // This is needed if function definition directly in a scope. Some other ways - // where function definition can be used is for example with variable declaration: - // - // const bar = function foo() {'worklet' ...}; - // - // In such a case we don't need to define variable for the function. - const needDeclaration = - isScopable(path.parent) || isExportNamedDeclaration(path.parent); +// export function maybeSubstituteFunctionDeclarationWithVariableDeclaration( +// path: NodePath, +// workletFactoryCall: CallExpression +// ): void { +// // We check if function needs to be assigned to variable declaration. +// // This is needed if function definition directly in a scope. Some other ways +// // where function definition can be used is for example with variable declaration: +// // +// // const bar = function foo() {'worklet' ...}; +// // +// // In such a case we don't need to define variable for the function. +// const needDeclaration = needsDeclaration(path.parent); - const replacement = - 'id' in path.node && path.node.id && needDeclaration - ? variableDeclaration('const', [ - variableDeclarator(path.node.id, workletFactoryCall), - ]) - : workletFactoryCall; +// const replacement = +// 'id' in path.node && path.node.id && needDeclaration +// ? variableDeclaration('const', [ +// variableDeclarator(path.node.id, workletFactoryCall), +// ]) +// : workletFactoryCall; - path.replaceWith(replacement); -} +// path.replaceWith(replacement); +// } From 80446cf6a4cc2cc5e695e02b95c824847f8abb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Fri, 19 Jul 2024 13:34:29 +0200 Subject: [PATCH 06/10] chore: remove comment --- .../plugin/src/class.ts | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/packages/react-native-reanimated/plugin/src/class.ts b/packages/react-native-reanimated/plugin/src/class.ts index 3ea940dcea2..8f0ae02d00f 100644 --- a/packages/react-native-reanimated/plugin/src/class.ts +++ b/packages/react-native-reanimated/plugin/src/class.ts @@ -87,43 +87,6 @@ function processClass( const factoryCall = callExpression(factoryFactory, []); replaceWithFactoryCall(classPath, className, factoryCall); - // const body = classWithPolyfills.ast.program.body; - // body.push(factory!); - - // body.push( - // expressionStatement( - // assignmentExpression( - // '=', - // memberExpression( - // identifier(className), - // identifier(className + classFactorySuffix) - // ), - // identifier(className + classFactorySuffix) - // ) - // ) - // ); - - // sortPolyfills(classWithPolyfills.ast); - - // const transformedNewCode = transformSync( - // generate(classWithPolyfills.ast).code, - // { - // ast: true, - // filename: state.file.opts.filename, - // } - // ); - - // assert(transformedNewCode); - // assert(transformedNewCode.ast); - - // // const needsDeclaration = needsDeclaration(classPath.parent); - - // const parent = classPath.parent as Program; - - // const index = parent.body.findIndex((node) => node === classPath.node); - - // parent.body.splice(index, 1, ...transformedNewCode.ast.program.body); - // #endregion } function getPolyfilledAst( From 8a411b523eb23b15acc572e3ad65853ebb78c469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Fri, 19 Jul 2024 13:38:31 +0200 Subject: [PATCH 07/10] chore: remove comment --- .../plugin/src/workletSubstitution.ts | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/packages/react-native-reanimated/plugin/src/workletSubstitution.ts b/packages/react-native-reanimated/plugin/src/workletSubstitution.ts index 42db4ca07f1..b688f65242c 100644 --- a/packages/react-native-reanimated/plugin/src/workletSubstitution.ts +++ b/packages/react-native-reanimated/plugin/src/workletSubstitution.ts @@ -90,26 +90,3 @@ export function substituteObjectMethodWithObjectProperty( const replacement = objectProperty(path.node.key, workletFactoryCall); path.replaceWith(replacement); } - -// export function maybeSubstituteFunctionDeclarationWithVariableDeclaration( -// path: NodePath, -// workletFactoryCall: CallExpression -// ): void { -// // We check if function needs to be assigned to variable declaration. -// // This is needed if function definition directly in a scope. Some other ways -// // where function definition can be used is for example with variable declaration: -// // -// // const bar = function foo() {'worklet' ...}; -// // -// // In such a case we don't need to define variable for the function. -// const needDeclaration = needsDeclaration(path.parent); - -// const replacement = -// 'id' in path.node && path.node.id && needDeclaration -// ? variableDeclaration('const', [ -// variableDeclarator(path.node.id, workletFactoryCall), -// ]) -// : workletFactoryCall; - -// path.replaceWith(replacement); -// } From d55f5822842d5e9fc750ce9d8c246869fdffaa7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Mon, 22 Jul 2024 10:38:56 +0200 Subject: [PATCH 08/10] chore: review changes --- .../plugin/build/plugin.js | 30 ++++++++-------- .../plugin/src/class.ts | 34 ++++++++++++++----- .../plugin/src/file.ts | 32 ++++++----------- .../plugin/src/jestMatchers.ts | 6 ++-- 4 files changed, 56 insertions(+), 46 deletions(-) diff --git a/packages/react-native-reanimated/plugin/build/plugin.js b/packages/react-native-reanimated/plugin/build/plugin.js index 34bd7955650..72f5134a6fc 100644 --- a/packages/react-native-reanimated/plugin/build/plugin.js +++ b/packages/react-native-reanimated/plugin/build/plugin.js @@ -1114,20 +1114,20 @@ var require_file = __commonJS({ var types_12 = require("@babel/types"); var types_2 = require_types(); var contextObject_12 = require_contextObject(); - function processIfWorkletFile(path, state) { + function processIfWorkletFile(path, _state) { if (!path.node.directives.some((functionDirective) => functionDirective.value.value === "worklet")) { return false; } path.node.directives = path.node.directives.filter((functionDirective) => functionDirective.value.value !== "worklet"); - processWorkletFile(path, state); + processWorkletFile(path); return true; } exports2.processIfWorkletFile = processIfWorkletFile; - function processWorkletFile(programPath, state) { + function processWorkletFile(programPath) { const statements = programPath.get("body"); statements.forEach((statement) => { const candidatePath = getCandidate(statement); - processWorkletizableEntity(candidatePath, state); + processWorkletizableEntity(candidatePath); }); } function getCandidate(statementPath) { @@ -1137,7 +1137,7 @@ var require_file = __commonJS({ return statementPath; } } - function processWorkletizableEntity(nodePath, state) { + function processWorkletizableEntity(nodePath) { if ((0, types_2.isWorkletizableFunctionPath)(nodePath)) { if (nodePath.isArrowFunctionExpression()) { replaceImplicitReturnWithBlock(nodePath.node); @@ -1147,31 +1147,31 @@ var require_file = __commonJS({ if (isImplicitContextObject(nodePath)) { appendWorkletContextObjectMarker(nodePath.node); } else { - processWorkletAggregator(nodePath, state); + processWorkletAggregator(nodePath); } } else if (nodePath.isVariableDeclaration()) { - processVariableDeclaration(nodePath, state); + processVariableDeclaration(nodePath); } else if (nodePath.isClassDeclaration()) { appendWorkletClassMarker(nodePath.node.body); } } - function processVariableDeclaration(variableDeclarationPath, state) { + function processVariableDeclaration(variableDeclarationPath) { const declarations = variableDeclarationPath.get("declarations"); declarations.forEach((declaration) => { const initPath = declaration.get("init"); if (initPath.isExpression()) { - processWorkletizableEntity(initPath, state); + processWorkletizableEntity(initPath); } }); } - function processWorkletAggregator(objectPath, state) { + 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, state); + processWorkletizableEntity(valuePath); } }); } @@ -1445,10 +1445,9 @@ var require_class = __commonJS({ polyfills.push(element); statement.traverse({ Identifier(path) { - if (!path.isReferencedIdentifier() || path.node.name in bindingIdentifiers || statement.scope.hasOwnBinding(path.node.name) || !statement.scope.hasReference(path.node.name)) { - return; + if (isOutsideDependency(path, bindingIdentifiers, statement)) { + element.dependencies.add(path.node.name); } - element.dependencies.add(path.node.name); } }); }); @@ -1483,6 +1482,9 @@ var require_class = __commonJS({ sorted.push(current); stack.delete(current.name); } + function isOutsideDependency(identifierPath, bindingIdentifiers, functionPath) { + return identifierPath.isReferencedIdentifier() && !(identifierPath.node.name in bindingIdentifiers) && !functionPath.scope.hasOwnBinding(identifierPath.node.name) && functionPath.scope.hasReference(identifierPath.node.name); + } } }); diff --git a/packages/react-native-reanimated/plugin/src/class.ts b/packages/react-native-reanimated/plugin/src/class.ts index 8f0ae02d00f..16b6151a026 100644 --- a/packages/react-native-reanimated/plugin/src/class.ts +++ b/packages/react-native-reanimated/plugin/src/class.ts @@ -7,6 +7,7 @@ import type { CallExpression, ClassBody, ClassDeclaration, + FunctionDeclaration, Identifier, Program, Statement, @@ -262,15 +263,9 @@ function getPolyfillsToSort(ast: BabelFile): Polyfill[] { polyfills.push(element); statement.traverse({ Identifier(path: NodePath) { - if ( - !path.isReferencedIdentifier() || - path.node.name in bindingIdentifiers || - statement.scope.hasOwnBinding(path.node.name) || - !statement.scope.hasReference(path.node.name) - ) { - return; + if (isOutsideDependency(path, bindingIdentifiers, statement)) { + element.dependencies.add(path.node.name); } - element.dependencies.add(path.node.name); }, }); }); @@ -315,6 +310,29 @@ function recursiveTopoSort( stack.delete(current.name); } +/** + * Checks if an identifier is a reference to an outside dependency. + * The condition was made by trial and error. + */ +function isOutsideDependency( + identifierPath: NodePath, + bindingIdentifiers: Record, + functionPath: NodePath +): boolean { + return ( + // We don't care about identifiers that were just declared. + identifierPath.isReferencedIdentifier() && + // We don't care about identifiers that are bound in the scope. + !(identifierPath.node.name in bindingIdentifiers) && + // This I don't exactly understand, but the function identifier itself isn't in `bindingIdentifiers`, + // but it return true on `hasOwnBinding`. + !functionPath.scope.hasOwnBinding(identifierPath.node.name) && + // `hasReference` returns true for global identifiers, like `Object`, + // we don't want to include those. + functionPath.scope.hasReference(identifierPath.node.name) + ); +} + type Polyfill = { name: string; index: number; diff --git a/packages/react-native-reanimated/plugin/src/file.ts b/packages/react-native-reanimated/plugin/src/file.ts index 6131f4d8262..33f20de7dd2 100644 --- a/packages/react-native-reanimated/plugin/src/file.ts +++ b/packages/react-native-reanimated/plugin/src/file.ts @@ -33,7 +33,7 @@ import { contextObjectMarker } from './contextObject'; export function processIfWorkletFile( path: NodePath, - state: ReanimatedPluginPass + _state: ReanimatedPluginPass ): boolean { if ( !path.node.directives.some( @@ -47,7 +47,7 @@ export function processIfWorkletFile( path.node.directives = path.node.directives.filter( (functionDirective) => functionDirective.value.value !== 'worklet' ); - processWorkletFile(path, state); + processWorkletFile(path); return true; } @@ -55,14 +55,11 @@ export function processIfWorkletFile( /** * Adds a worklet directive to each viable top-level entity in the file. */ -function processWorkletFile( - programPath: NodePath, - state: ReanimatedPluginPass -) { +function processWorkletFile(programPath: NodePath) { const statements = programPath.get('body'); statements.forEach((statement) => { const candidatePath = getCandidate(statement); - processWorkletizableEntity(candidatePath, state); + processWorkletizableEntity(candidatePath); }); } @@ -77,10 +74,7 @@ function getCandidate(statementPath: NodePath) { } } -function processWorkletizableEntity( - nodePath: NodePath, - state: ReanimatedPluginPass -) { +function processWorkletizableEntity(nodePath: NodePath) { if (isWorkletizableFunctionPath(nodePath)) { if (nodePath.isArrowFunctionExpression()) { replaceImplicitReturnWithBlock(nodePath.node); @@ -90,39 +84,35 @@ function processWorkletizableEntity( if (isImplicitContextObject(nodePath)) { appendWorkletContextObjectMarker(nodePath.node); } else { - processWorkletAggregator(nodePath, state); + processWorkletAggregator(nodePath); } } else if (nodePath.isVariableDeclaration()) { - processVariableDeclaration(nodePath, state); + processVariableDeclaration(nodePath); } else if (nodePath.isClassDeclaration()) { appendWorkletClassMarker(nodePath.node.body); } } function processVariableDeclaration( - variableDeclarationPath: NodePath, - state: ReanimatedPluginPass + variableDeclarationPath: NodePath ) { const declarations = variableDeclarationPath.get('declarations'); declarations.forEach((declaration) => { const initPath = declaration.get('init'); if (initPath.isExpression()) { - processWorkletizableEntity(initPath, state); + processWorkletizableEntity(initPath); } }); } -function processWorkletAggregator( - objectPath: NodePath, - state: ReanimatedPluginPass -) { +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, state); + processWorkletizableEntity(valuePath); } }); } diff --git a/packages/react-native-reanimated/plugin/src/jestMatchers.ts b/packages/react-native-reanimated/plugin/src/jestMatchers.ts index 45b9b539f0c..c70dc06de6b 100644 --- a/packages/react-native-reanimated/plugin/src/jestMatchers.ts +++ b/packages/react-native-reanimated/plugin/src/jestMatchers.ts @@ -69,12 +69,12 @@ expect.extend({ }, toIncludeInWorkletString(received: string, expected: string) { - // Regular expression pattern to match the code field + // Regular expression pattern to find the `code` field in `initData`. // @ts-ignore This regex works well in Jest. const pattern = /code: "((?:[^"\\]|\\.)*)"/gs; const matches = received.match(pattern); - // If a match was found and the match group 1 (content within quotes) includes the expected string + // If a match was found and some of matches (`initData`'s `code`) include the expected string. if (matches && matches.some((match) => match.includes(expected))) { // return true; return { @@ -83,7 +83,7 @@ expect.extend({ }; } - // If no match was found or the expected string is not a substring of the code field + // If no match was found or the expected string is not a substring of the code field. // return false; return { message: () => From ac6a901f893dbb12c2d6fcf79f71a44fe8ceacef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Mon, 22 Jul 2024 14:02:24 +0200 Subject: [PATCH 09/10] chore: add comment --- packages/react-native-reanimated/plugin/src/class.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-reanimated/plugin/src/class.ts b/packages/react-native-reanimated/plugin/src/class.ts index 16b6151a026..ad7a552c1c9 100644 --- a/packages/react-native-reanimated/plugin/src/class.ts +++ b/packages/react-native-reanimated/plugin/src/class.ts @@ -251,6 +251,7 @@ function getPolyfillsToSort(ast: BabelFile): Polyfill[] { const statements = functionPath.get('body'); statements.forEach((statement, index) => { const bindingIdentifiers = statement.getBindingIdentifiers(); + // Polyfills are prepended as FunctionDeclarations. if (!statement.isFunctionDeclaration() || !statement.node.id?.name) { return; } From c4daa922a96f778b13523a48e14f51f21fccb600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Mon, 22 Jul 2024 14:13:12 +0200 Subject: [PATCH 10/10] chore: fix missed rename --- .../RuntimeTests/tests/plugin/workletClasses.test.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/common-app/src/examples/RuntimeTests/tests/plugin/workletClasses.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/plugin/workletClasses.test.tsx index 3e3834b49ab..bd4d0244ecb 100644 --- a/apps/common-app/src/examples/RuntimeTests/tests/plugin/workletClasses.test.tsx +++ b/apps/common-app/src/examples/RuntimeTests/tests/plugin/workletClasses.test.tsx @@ -1,15 +1,7 @@ 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 { render, wait, describe, getRegisteredValue, registerValue, test, expect } from '../../ReJest/RuntimeTestsApi'; const SHARED_VALUE_REF = 'SHARED_VALUE_REF';