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,