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/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx index fd9bf573d6d..1983abd054f 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 4c3c87163b4..22eccfb5910 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 @@ -2,7 +2,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 '../../ReJest/RuntimeTestsApi'; -import { getThree, implicitContextObject } from './fileWorkletization'; +import { ImplicitWorkletClass, getThree, implicitContextObject } from './fileWorkletization'; const SHARED_VALUE_REF = 'SHARED_VALUE_REF'; @@ -44,4 +44,23 @@ describe('Test file workletization', () => { const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); expect(sharedValue.onUI).toBe(5); }); + + test('WorkletClasses 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..bd4d0244ecb --- /dev/null +++ b/apps/common-app/src/examples/RuntimeTests/tests/plugin/workletClasses.test.tsx @@ -0,0 +1,138 @@ +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 '../../ReJest/RuntimeTestsApi'; + +const SHARED_VALUE_REF = 'SHARED_VALUE_REF'; + +class WorkletClass { + __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 WorkletClass(); + + 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 WorkletClass(); + 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 WorkletClass(); + 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 WorkletClass(); + 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 WorkletClass(); + output.value = clazz.getIncremented() + clazz.getIncremented(); + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(3); + }); + + test('instanceof operator works on Worklet runtime', async () => { + const ExampleComponent = () => { + const output = useSharedValue(null); + registerValue(SHARED_VALUE_REF, output); + + useEffect(() => { + runOnUI(() => { + const clazz = new WorkletClass(); + output.value = clazz instanceof WorkletClass; + })(); + }); + + return ; + }; + await render(); + await wait(100); + const sharedValue = await getRegisteredValue(SHARED_VALUE_REF); + expect(sharedValue.onUI).toBe(true); + }); + + // 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 f9a32cda80c..8f0c6def7cb 100644 --- a/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap +++ b/packages/react-native-reanimated/__tests__/__snapshots__/plugin.test.ts.snap @@ -1058,6 +1058,467 @@ var foo = exports.foo = function () { }();" `; +exports[`babel plugin for file workletization workletizes ClassDeclaration 1`] = ` +"var _worklet_4985973100755_init_data = { + code: "function _classCallCheck_null1(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 _worklet_12814759901173_init_data = { + code: "function _toPrimitive_null2(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 _worklet_10645339112046_init_data = { + code: "function _toPropertyKey_null3(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 _worklet_712232292287_init_data = { + code: "function _defineProperties_null4(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 Clazz__classFactory = function ()'); + expect(code).toIncludeInWorkletString('Clazz__classFactory'); + expect(code).toContain('Clazz.Clazz__classFactory = Clazz__classFactory'); + 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 Clazz__classFactory = function ()'); + expect(code).toIncludeInWorkletString('Clazz__classFactory'); + expect(code).toContain('Clazz.Clazz__classFactory = Clazz__classFactory'); + 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 Clazz__classFactory = function ()'); + expect(code).toIncludeInWorkletString('Clazz__classFactory'); + expect(code).toContain('Clazz.Clazz__classFactory = Clazz__classFactory'); + 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('Clazz__classFactory'); + expect(code).toIncludeInWorkletString('Clazz__classFactory'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes regardless of marker value', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('Clazz__classFactory'); + expect(code).toIncludeInWorkletString('Clazz__classFactory'); + expect(code).toMatchSnapshot(); + }); + + it('injects class factory into worklets', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('Clazz__classFactory'); + expect(code).toMatchSnapshot(); + }); + + it('modifies closures', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('Clazz__classFactory: Clazz.Clazz__classFactory'); + expect(code).toMatchSnapshot(); + }); + + it('keeps this binding', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toIncludeInWorkletString('this.member'); + expect(code).toMatchSnapshot(); + }); + + it('appends polyfills', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toContain('createClass'); + expect(code).toMatchSnapshot(); + }); + + it('workletizes polyfills', () => { + const input = html``; + + const { code } = runPlugin(input); + expect(code).toHaveWorkletData(6); + expect(code).toMatchSnapshot(); + }); + }); }); diff --git a/packages/react-native-reanimated/package.json b/packages/react-native-reanimated/package.json index 11843f49b90..f13fe4b3258 100644 --- a/packages/react-native-reanimated/package.json +++ b/packages/react-native-reanimated/package.json @@ -84,10 +84,13 @@ "homepage": "https://docs.swmansion.com/react-native-reanimated", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-class-properties": "^7.0.0-0", + "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", + "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4" diff --git a/packages/react-native-reanimated/plugin/build/plugin.js b/packages/react-native-reanimated/plugin/build/plugin.js index ea05253d534..72f5134a6fc 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) { @@ -251,6 +268,27 @@ 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) { + if (!(0, types_12.isIdentifier)(path.node.callee)) { + return; + } + 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); + 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)(workletClassFactoryName), [])) + ])); + 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."); @@ -349,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 = [ @@ -436,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) => (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) { @@ -590,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; @@ -614,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) { @@ -634,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; } }); @@ -1042,107 +1074,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) { @@ -1183,22 +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; } - processWorkletFile(path, state); path.node.directives = path.node.directives.filter((functionDirective) => functionDirective.value.value !== "worklet"); + 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); - if (candidatePath.node) { - processWorkletizableEntity(candidatePath); - } + processWorkletizableEntity(candidatePath); }); } function getCandidate(statementPath) { @@ -1222,6 +1151,8 @@ var require_file = __commonJS({ } } else if (nodePath.isVariableDeclaration()) { processVariableDeclaration(nodePath); + } else if (nodePath.isClassDeclaration()) { + appendWorkletClassMarker(nodePath.node.body); } } function processVariableDeclaration(variableDeclarationPath) { @@ -1280,20 +1211,295 @@ var require_file = __commonJS({ }); return result; } + function appendWorkletClassMarker(classBody) { + classBody.body.push((0, types_12.classProperty)((0, types_12.identifier)("__workletClass"), (0, types_12.booleanLiteral)(true))); + } + } +}); + +// 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/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.processIfWorkletClass = void 0; + var core_1 = require("@babel/core"); + var generator_1 = __importDefault(require("@babel/generator")); + 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) { + return false; + } + if (!hasWorkletClassMarker(classPath.node.body)) { + return false; + } + removeWorkletClassMarker(classPath.node.body); + processClass(classPath, state); + return true; + } + exports2.processIfWorkletClass = processIfWorkletClass; + function processClass(classPath, state) { + (0, assert_1.strict)(classPath.node.id); + const className = classPath.node.id.name; + 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", + "@babel/plugin-transform-unicode-regex" + ], + filename: state.file.opts.filename, + ast: true, + babelrc: false, + configFile: false + }); + (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); + } + }); + } + 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), [])) + ]); + 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); + } + function removeWorkletClassMarker(classBody) { + classBody.body = classBody.body.filter((statement) => !(0, types_12.isClassProperty)(statement) || !(0, types_12.isIdentifier)(statement.key) || statement.key.name !== classWorkletMarker); + } + 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 (isOutsideDependency(path, bindingIdentifiers, statement)) { + element.dependencies.add(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); + } + 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); + } } }); // 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(); +var class_1 = require_class(); module.exports = function() { function runWithTaggedExceptions(fun) { try { @@ -1335,6 +1541,13 @@ module.exports = function() { }); } }, + ClassDeclaration: { + enter(path, state) { + runWithTaggedExceptions(() => { + (0, class_1.processIfWorkletClass)(path, state); + }); + } + }, Program: { enter(path, state) { runWithTaggedExceptions(() => { diff --git a/packages/react-native-reanimated/plugin/package.json b/packages/react-native-reanimated/plugin/package.json index c1993fd4f2b..c19b4ede325 100644 --- a/packages/react-native-reanimated/plugin/package.json +++ b/packages/react-native-reanimated/plugin/package.json @@ -2,6 +2,8 @@ "devDependencies": { "@babel/cli": "^7.20.0", "@babel/core": "^7.20.0", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.8", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", 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..ad7a552c1c9 --- /dev/null +++ b/packages/react-native-reanimated/plugin/src/class.ts @@ -0,0 +1,341 @@ +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, + FunctionDeclaration, + Identifier, + Program, + Statement, + VariableDeclaration, +} from '@babel/types'; +import { + assignmentExpression, + blockStatement, + callExpression, + directive, + directiveLiteral, + expressionStatement, + functionDeclaration, + functionExpression, + identifier, + isClassProperty, + isFunctionDeclaration, + isIdentifier, + isVariableDeclaration, + memberExpression, + returnStatement, + variableDeclaration, + variableDeclarator, +} from '@babel/types'; +import { strict as assert } from 'assert'; +import type { ReanimatedPluginPass } from './types'; +import { workletClassFactorySuffix } from './types'; +import { replaceWithFactoryCall } from './utils'; + +const classWorkletMarker = '__workletClass'; + +export function processIfWorkletClass( + classPath: NodePath, + state: ReanimatedPluginPass +): boolean { + if (!classPath.node.id) { + // We don't support unnamed classes yet. + return false; + } + + if (!hasWorkletClassMarker(classPath.node.body)) { + return false; + } + + removeWorkletClassMarker(classPath.node.body); + + processClass(classPath, state); + + return true; +} + +function processClass( + classPath: NodePath, + state: ReanimatedPluginPass +) { + assert(classPath.node.id); + const className = classPath.node.id.name; + + const polyfilledClassAst = getPolyfilledAst(classPath.node, state); + + appendWorkletDirectiveToPolyfills(polyfilledClassAst.program.body); + + replaceClassDeclarationWithFactoryAndCall( + polyfilledClassAst.program.body, + className + ); + + sortPolyfills(polyfilledClassAst); + + polyfilledClassAst.program.body.push(returnStatement(identifier(className))); + + const factoryFactory = functionExpression( + null, + [], + blockStatement([...polyfilledClassAst.program.body]) + ); + + const factoryCall = callExpression(factoryFactory, []); + + replaceWithFactoryCall(classPath, className, factoryCall); +} + +function getPolyfilledAst( + classNode: ClassDeclaration, + state: ReanimatedPluginPass +) { + 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, + }); + + assert(classWithPolyfills && classWithPolyfills.ast); + + return classWithPolyfills.ast; +} + +function appendWorkletDirectiveToPolyfills(statements: Statement[]) { + statements.forEach((statement) => { + if (isFunctionDeclaration(statement)) { + const workletDirective = directive(directiveLiteral('worklet')); + statement.body.directives.push(workletDirective); + } + }); +} + +/** + * Replaces + * ```ts + * const Clazz = ...; + * ``` + * with + * ```ts + * const Clazz__classFactory = ...; + * const Clazz = Clazz__classFactory(); + * ``` + */ +function replaceClassDeclarationWithFactoryAndCall( + statements: Statement[], + className: string +) { + 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), []) + ), + ]); + + statements.splice( + classDeclarationIndex, + 1, + classFactoryDeclaration, + newClassDeclaration + ); +} + +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) { + return classBody.body.some( + (statement) => + isClassProperty(statement) && + isIdentifier(statement.key) && + statement.key.name === classWorkletMarker + ); +} + +function removeWorkletClassMarker(classBody: ClassBody) { + classBody.body = classBody.body.filter( + (statement) => + !isClassProperty(statement) || + !isIdentifier(statement.key) || + statement.key.name !== classWorkletMarker + ); +} + +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): Polyfill[] { + const polyfills: Polyfill[] = []; + + traverse(ast, { + Program: { + enter: (functionPath: NodePath) => { + 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; + } + + const element: Polyfill = { + name: statement.node.id.name, + index, + dependencies: new Set(), + }; + polyfills.push(element); + statement.traverse({ + Identifier(path: NodePath) { + if (isOutsideDependency(path, bindingIdentifiers, statement)) { + element.dependencies.add(path.node.name); + } + }, + }); + }); + }, + }, + }); + + return polyfills; +} + +function topoSort(toSort: Polyfill[]): Polyfill[] { + const sorted: Polyfill[] = []; + const stack: Set = new Set(); + for (const element of toSort) { + recursiveTopoSort(element, toSort, sorted, stack); + } + return sorted; +} + +function recursiveTopoSort( + current: Polyfill, + toSort: Polyfill[], + sorted: Polyfill[], + 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); +} + +/** + * 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; + dependencies: Set; +}; diff --git a/packages/react-native-reanimated/plugin/src/file.ts b/packages/react-native-reanimated/plugin/src/file.ts index dc27d9e15c9..33f20de7dd2 100644 --- a/packages/react-native-reanimated/plugin/src/file.ts +++ b/packages/react-native-reanimated/plugin/src/file.ts @@ -1,6 +1,7 @@ import { blockStatement, booleanLiteral, + classProperty, directive, directiveLiteral, identifier, @@ -18,9 +19,9 @@ import type { ArrowFunctionExpression, ObjectExpression, Statement, - Node as BabelNode, ThisExpression, ObjectMethod, + ClassBody, } from '@babel/types'; import type { NodePath } from '@babel/core'; import { @@ -32,7 +33,7 @@ import { contextObjectMarker } from './contextObject'; export function processIfWorkletFile( path: NodePath, - state: ReanimatedPluginPass + _state: ReanimatedPluginPass ): boolean { if ( !path.node.directives.some( @@ -42,29 +43,23 @@ export function processIfWorkletFile( return false; } - processWorkletFile(path, state); - // Remove 'worklet' directive from the file afterwards. + // Remove 'worklet' directive from the file. path.node.directives = path.node.directives.filter( (functionDirective) => functionDirective.value.value !== 'worklet' ); + processWorkletFile(path); + return true; } /** * 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); - if (candidatePath.node) { - processWorkletizableEntity( - candidatePath as NodePath> - ); - } + processWorkletizableEntity(candidatePath); }); } @@ -73,15 +68,13 @@ function getCandidate(statementPath: NodePath) { statementPath.isExportNamedDeclaration() || statementPath.isExportDefaultDeclaration() ) { - return statementPath.get('declaration') as NodePath< - typeof statementPath.node.declaration - >; + return statementPath.get('declaration') as NodePath; } else { return statementPath; } } -function processWorkletizableEntity(nodePath: NodePath) { +function processWorkletizableEntity(nodePath: NodePath) { if (isWorkletizableFunctionPath(nodePath)) { if (nodePath.isArrowFunctionExpression()) { replaceImplicitReturnWithBlock(nodePath.node); @@ -95,6 +88,8 @@ function processWorkletizableEntity(nodePath: NodePath) { } } else if (nodePath.isVariableDeclaration()) { processVariableDeclaration(nodePath); + } else if (nodePath.isClassDeclaration()) { + appendWorkletClassMarker(nodePath.node.body); } } @@ -188,3 +183,9 @@ function hasThisExpression(path: NodePath): boolean { return result; } + +function appendWorkletClassMarker(classBody: ClassBody) { + classBody.body.push( + classProperty(identifier('__workletClass'), booleanLiteral(true)) + ); +} diff --git a/packages/react-native-reanimated/plugin/src/jestMatchers.ts b/packages/react-native-reanimated/plugin/src/jestMatchers.ts index 34b8dfbab1c..c70dc06de6b 100644 --- a/packages/react-native-reanimated/plugin/src/jestMatchers.ts +++ b/packages/react-native-reanimated/plugin/src/jestMatchers.ts @@ -69,12 +69,13 @@ expect.extend({ }, toIncludeInWorkletString(received: string, expected: string) { - // Regular expression pattern to match the code field - const pattern = /code: "((?:[^"\\]|\\.)*)"/s; - const match = received.match(pattern); + // 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 (match && match[1].includes(expected)) { + // 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 { message: () => `Reanimated: found ${expected} in worklet string`, @@ -82,11 +83,11 @@ 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: () => - `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..ce6b056c792 100644 --- a/packages/react-native-reanimated/plugin/src/plugin.ts +++ b/packages/react-native-reanimated/plugin/src/plugin.ts @@ -1,23 +1,25 @@ -import type { PluginItem, NodePath } from '@babel/core'; +import type { NodePath, PluginItem } from '@babel/core'; import type { CallExpression, + ClassDeclaration, 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'; +import { processIfWorkletClass } from './class'; module.exports = function (): PluginItem { function runWithTaggedExceptions(fun: () => void) { @@ -66,6 +68,13 @@ module.exports = function (): PluginItem { }); }, }, + ClassDeclaration: { + enter(path: NodePath, state: ReanimatedPluginPass) { + runWithTaggedExceptions(() => { + processIfWorkletClass(path, state); + }); + }, + }, Program: { enter(path: NodePath, state: ReanimatedPluginPass) { runWithTaggedExceptions(() => { 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 522ccb1c0bb..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,7 +227,20 @@ export function makeWorkletFactory( memberExpression(functionIdentifier, identifier('__closure'), false), objectExpression( variables.map((variable) => - objectProperty(identifier(variable.name), variable, false, true) + variable.name.endsWith(workletClassFactorySuffix) + ? objectProperty( + identifier(variable.name), + memberExpression( + identifier( + variable.name.slice( + 0, + variable.name.length - workletClassFactorySuffix.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..5d58e683f60 100644 --- a/packages/react-native-reanimated/plugin/src/workletStringCode.ts +++ b/packages/react-native-reanimated/plugin/src/workletStringCode.ts @@ -9,6 +9,8 @@ import type { VariableDeclaration, } from '@babel/types'; import { + assertBlockStatement, + callExpression, functionExpression, identifier, isArrowFunctionExpression, @@ -16,6 +18,7 @@ import { isExpression, isExpressionStatement, isFunctionDeclaration, + isIdentifier, isObjectMethod, isProgram, memberExpression, @@ -28,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'; @@ -63,6 +67,43 @@ export function buildWorkletString( '[Reanimated] `expression.body` is not a `BlockStatement`' ); + const parsedClasses = new Set(); + + traverse(fun, { + NewExpression(path) { + if (!isIdentifier(path.node.callee)) { + return; + } + 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); + const workletClassFactoryName = + constructorName + workletClassFactorySuffix; + closureVariables.push(identifier(workletClassFactoryName)); + + assertBlockStatement(expression.body); + expression.body.body.unshift( + variableDeclaration('const', [ + variableDeclarator( + identifier(constructorName), + callExpression(identifier(workletClassFactoryName), []) + ), + ]) + ); + parsedClasses.add(constructorName); + }, + }); + const workletFunction = functionExpression( identifier(nameWithSource), expression.params, diff --git a/packages/react-native-reanimated/plugin/src/workletSubstitution.ts b/packages/react-native-reanimated/plugin/src/workletSubstitution.ts index 0d30e09fe5c..b688f65242c 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); } } @@ -102,27 +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 = - isScopable(path.parent) || isExportNamedDeclaration(path.parent); - - const replacement = - 'id' in path.node && path.node.id && needDeclaration - ? variableDeclaration('const', [ - variableDeclarator(path.node.id, workletFactoryCall), - ]) - : workletFactoryCall; - - path.replaceWith(replacement); -} diff --git a/yarn.lock b/yarn.lock index b92ef54e5f7..016be56364a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -114,6 +114,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.24.8": + version: 7.24.9 + resolution: "@babel/compat-data@npm:7.24.9" + checksum: 10/fcdbf3dd978305880f06ae20a23f4f68a8eddbe64fc5d2fbc98dfe4cdf15c174cff41e3a8eb9d935f9f3a68d3a23fa432044082ee9768a2ed4b15f769b8f6853 + languageName: node + linkType: hard + "@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.18.5, @babel/core@npm:^7.20.0, @babel/core@npm:^7.23.9": version: 7.24.5 resolution: "@babel/core@npm:7.24.5" @@ -233,6 +240,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.24.8": + version: 7.24.10 + resolution: "@babel/generator@npm:7.24.10" + dependencies: + "@babel/types": "npm:^7.24.9" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^2.5.1" + checksum: 10/c2491fb7d985527a165546cbcf9e5f6a2518f2a968c7564409c012acce1019056b21e67a152af89b3f4d4a295ca2e75a1a16858152f750efbc4b5087f0cb7253 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" @@ -308,6 +327,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-compilation-targets@npm:7.24.8" + dependencies: + "@babel/compat-data": "npm:^7.24.8" + "@babel/helper-validator-option": "npm:^7.24.8" + browserslist: "npm:^4.23.1" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 10/3489280d07b871af565b32f9b11946ff9a999fac0db9bec5df960760f6836c7a4b52fccb9d64229ccce835d37a43afb85659beb439ecedde04dcea7eb062a143 + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.24.1, @babel/helper-create-class-features-plugin@npm:^7.24.4, @babel/helper-create-class-features-plugin@npm:^7.24.5": version: 7.24.5 resolution: "@babel/helper-create-class-features-plugin@npm:7.24.5" @@ -346,6 +378,25 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.24.7": + version: 7.24.8 + resolution: "@babel/helper-create-class-features-plugin@npm:7.24.8" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.24.7" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-function-name": "npm:^7.24.7" + "@babel/helper-member-expression-to-functions": "npm:^7.24.8" + "@babel/helper-optimise-call-expression": "npm:^7.24.7" + "@babel/helper-replace-supers": "npm:^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/a779c5356fcc4881e807d85d973fd37e99e773fe95837b0f6582ca9a89331f84e5f26b0b6aa9a101181325b73cf3f54081d178b657a79819b8abadc53b0ea8ec + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.15, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.15" @@ -485,6 +536,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-member-expression-to-functions@npm:^7.24.7, @babel/helper-member-expression-to-functions@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-member-expression-to-functions@npm:7.24.8" + dependencies: + "@babel/traverse": "npm:^7.24.8" + "@babel/types": "npm:^7.24.8" + checksum: 10/ac878761cfd0a46c081cda0da75cc186f922cf16e8ecdd0c4fb6dca4330d9fe4871b41a9976224cf9669c9e7fe0421b5c27349f2e99c125fa0be871b327fa770 + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.24.1, @babel/helper-module-imports@npm:^7.24.3": version: 7.24.3 resolution: "@babel/helper-module-imports@npm:7.24.3" @@ -576,6 +637,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-optimise-call-expression@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-optimise-call-expression@npm:7.24.7" + dependencies: + "@babel/types": "npm:^7.24.7" + checksum: 10/da7a7f2d1bb1be4cffd5fa820bd605bc075c7dd014e0458f608bb6f34f450fe9412c8cea93e788227ab396e0e02c162d7b1db3fbcb755a6360e354c485d61df0 + languageName: node + linkType: hard + "@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.0, @babel/helper-plugin-utils@npm:^7.24.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": version: 7.24.5 resolution: "@babel/helper-plugin-utils@npm:7.24.5" @@ -597,6 +667,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-plugin-utils@npm:7.24.8" + checksum: 10/adbc9fc1142800a35a5eb0793296924ee8057fe35c61657774208670468a9fbfbb216f2d0bc46c680c5fefa785e5ff917cc1674b10bd75cdf9a6aa3444780630 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.18.9, @babel/helper-remap-async-to-generator@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-remap-async-to-generator@npm:7.22.20" @@ -636,6 +713,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-replace-supers@npm:7.24.7" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-member-expression-to-functions": "npm:^7.24.7" + "@babel/helper-optimise-call-expression": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/18b7c3709819d008a14953e885748f3e197537f131d8f7ae095fec245506d854ff40b236edb1754afb6467f795aa90ae42a1d961a89557702249bacfc3fdad19 + languageName: node + linkType: hard + "@babel/helper-simple-access@npm:^7.22.5, @babel/helper-simple-access@npm:^7.24.5": version: 7.24.5 resolution: "@babel/helper-simple-access@npm:7.24.5" @@ -682,6 +772,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.24.7" + dependencies: + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10/784a6fdd251a9a7e42ccd04aca087ecdab83eddc60fda76a2950e00eb239cc937d3c914266f0cc476298b52ac3f44ffd04c358e808bd17552a7e008d75494a77 + languageName: node + linkType: hard + "@babel/helper-split-export-declaration@npm:^7.24.5": version: 7.24.5 resolution: "@babel/helper-split-export-declaration@npm:7.24.5" @@ -730,6 +830,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-string-parser@npm:7.24.8" + checksum: 10/6d1bf8f27dd725ce02bdc6dffca3c95fb9ab8a06adc2edbd9c1c9d68500274230d1a609025833ed81981eff560045b6b38f7b4c6fb1ab19fc90e5004e3932535 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.22.20, @babel/helper-validator-identifier@npm:^7.24.5": version: 7.24.5 resolution: "@babel/helper-validator-identifier@npm:7.24.5" @@ -772,6 +879,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-validator-option@npm:7.24.8" + checksum: 10/a52442dfa74be6719c0608fee3225bd0493c4057459f3014681ea1a4643cd38b68ff477fe867c4b356da7330d085f247f0724d300582fa4ab9a02efaf34d107c + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.22.20": version: 7.24.5 resolution: "@babel/helper-wrap-function@npm:7.24.5" @@ -877,6 +991,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/parser@npm:7.24.8" + bin: + parser: ./bin/babel-parser.js + checksum: 10/e44b8327da46e8659bc9fb77f66e2dc4364dd66495fb17d046b96a77bf604f0446f1e9a89cf2f011d78fc3f5cdfbae2e9e0714708e1c985988335683b2e781ef + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.24.5": version: 7.24.5 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.24.5" @@ -1430,6 +1553,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-class-properties@npm:^7.0.0-0, @babel/plugin-transform-class-properties@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/plugin-transform-class-properties@npm:7.24.7" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.24.7" + "@babel/helper-plugin-utils": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/1c6f645dd3889257028f27bfbb04526ac7676763a923fc8203aa79aa5232820e0201cb858c73b684b1922327af10304121ac013c7b756876d54560a9c1a7bc79 + languageName: node + linkType: hard + "@babel/plugin-transform-class-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-class-properties@npm:7.24.1" @@ -1473,6 +1608,24 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-classes@npm:^7.0.0-0, @babel/plugin-transform-classes@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/plugin-transform-classes@npm:7.24.8" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.24.7" + "@babel/helper-compilation-targets": "npm:^7.24.8" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-function-name": "npm:^7.24.7" + "@babel/helper-plugin-utils": "npm:^7.24.8" + "@babel/helper-replace-supers": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + globals: "npm:^11.1.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/3d586018691423ed1fbcb4589cc29001226c96e5e060932bf99379568c684a4a230cca7871e7c825335336ef0326066ba6e3bf5e6d0209425b0f5ceeda3eaed2 + languageName: node + linkType: hard + "@babel/plugin-transform-computed-properties@npm:^7.0.0, @babel/plugin-transform-computed-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-computed-properties@npm:7.24.1" @@ -2209,7 +2362,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.24.7": +"@babel/plugin-transform-unicode-regex@npm:^7.0.0-0, @babel/plugin-transform-unicode-regex@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-unicode-regex@npm:7.24.7" dependencies: @@ -2530,6 +2683,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/traverse@npm:7.24.8" + dependencies: + "@babel/code-frame": "npm:^7.24.7" + "@babel/generator": "npm:^7.24.8" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-function-name": "npm:^7.24.7" + "@babel/helper-hoist-variables": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + "@babel/parser": "npm:^7.24.8" + "@babel/types": "npm:^7.24.8" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/47d8ecf8cfff58fe621fc4d8454b82c97c407816d8f9c435caa0c849ea7c357b91119a06f3c69f21a0228b5d06ac0b44f49d1f78cff032d6266317707f1fe615 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.7.0, @babel/types@npm:^7.8.3": version: 7.24.5 resolution: "@babel/types@npm:7.24.5" @@ -2563,6 +2734,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.24.8, @babel/types@npm:^7.24.9": + version: 7.24.9 + resolution: "@babel/types@npm:7.24.9" + dependencies: + "@babel/helper-string-parser": "npm:^7.24.8" + "@babel/helper-validator-identifier": "npm:^7.24.7" + to-fast-properties: "npm:^2.0.0" + checksum: 10/21873a08a124646824aa230de06af52149ab88206dca59849dcb3003990a6306ec2cdaa4147ec1127c0cfc5f133853cfc18f80d7f6337b6662a3c378ed565f15 + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -8496,6 +8678,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.23.1": + version: 4.23.2 + resolution: "browserslist@npm:4.23.2" + dependencies: + caniuse-lite: "npm:^1.0.30001640" + electron-to-chromium: "npm:^1.4.820" + node-releases: "npm:^2.0.14" + update-browserslist-db: "npm:^1.1.0" + bin: + browserslist: cli.js + checksum: 10/326a98b1c39bcc9a99b197f15790dc28e122b1aead3257c837421899377ac96239123f26868698085b3d9be916d72540602738e1f857e86a387e810af3fda6e5 + languageName: node + linkType: hard + "bs-logger@npm:0.x": version: 0.2.6 resolution: "bs-logger@npm:0.2.6" @@ -8741,6 +8937,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001640": + version: 1.0.30001642 + resolution: "caniuse-lite@npm:1.0.30001642" + checksum: 10/8d80ea82be453ae0fdfea8766d82740a4945c1b99189650f29bfc458d4e235d7e99027a8f8bc5a4228d8c4457ba896315284b0703f300353ad5f09d8e693de10 + languageName: node + linkType: hard + "caseless@npm:~0.12.0": version: 0.12.0 resolution: "caseless@npm:0.12.0" @@ -10324,6 +10527,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.820": + version: 1.4.829 + resolution: "electron-to-chromium@npm:1.4.829" + checksum: 10/43279561337582ff47bb3486439efbc7c1f2192455c76ebc7374754fca61334380025af9e5da7646b4d8c007d9dc6c25d8f6059dffb2207dc39d2f79287a296a + languageName: node + linkType: hard + "emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" @@ -17002,6 +17212,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:^1.0.1": + version: 1.0.1 + resolution: "picocolors@npm:1.0.1" + checksum: 10/fa68166d1f56009fc02a34cdfd112b0dd3cf1ef57667ac57281f714065558c01828cdf4f18600ad6851cbe0093952ed0660b1e0156bddf2184b6aaf5817553a5 + languageName: node + linkType: hard + "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" @@ -17090,6 +17307,8 @@ __metadata: dependencies: "@babel/cli": "npm:^7.20.0" "@babel/core": "npm:^7.20.0" + "@babel/plugin-transform-class-properties": "npm:^7.24.7" + "@babel/plugin-transform-classes": "npm:^7.24.8" "@babel/plugin-transform-unicode-regex": "npm:^7.24.7" "@babel/traverse": "npm:^7.20.0" "@babel/types": "npm:^7.20.0" @@ -17760,10 +17979,13 @@ __metadata: "@babel/cli": "npm:^7.20.0" "@babel/core": "npm:^7.20.0" "@babel/plugin-transform-arrow-functions": "npm:^7.0.0-0" + "@babel/plugin-transform-class-properties": "npm:^7.0.0-0" + "@babel/plugin-transform-classes": "npm:^7.0.0-0" "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.0.0-0" "@babel/plugin-transform-optional-chaining": "npm:^7.0.0-0" "@babel/plugin-transform-shorthand-properties": "npm:^7.0.0-0" "@babel/plugin-transform-template-literals": "npm:^7.0.0-0" + "@babel/plugin-transform-unicode-regex": "npm:^7.0.0-0" "@babel/preset-env": "npm:^7.20.0" "@babel/preset-typescript": "npm:^7.16.7" "@babel/types": "npm:^7.20.0" @@ -20636,6 +20858,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.1.0": + version: 1.1.0 + resolution: "update-browserslist-db@npm:1.1.0" + dependencies: + escalade: "npm:^3.1.2" + picocolors: "npm:^1.0.1" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10/d70b9efeaf4601aadb1a4f6456a7a5d9118e0063d995866b8e0c5e0cf559482671dab6ce7b079f9536b06758a344fbd83f974b965211e1c6e8d1958540b0c24c + languageName: node + linkType: hard + "update-check@npm:1.5.4": version: 1.5.4 resolution: "update-check@npm:1.5.4"