'use strict'; const generate = require('@babel/generator').default; const hash = require('string-hash-64'); const { visitors } = require('@babel/traverse'); const traverse = require('@babel/traverse').default; const parse = require('@babel/parser').parse; /** * holds a map of function names as keys and array of argument indexes as values which should be automatically workletized(they have to be functions)(starting from 0) */ const functionArgsToWorkletize = new Map([ ['useAnimatedStyle', [0]], ['useAnimatedProps', [0]], ['createAnimatedPropAdapter', [0]], ['useDerivedValue', [0]], ['useAnimatedScrollHandler', [0]], ['useAnimatedReaction', [0, 1]], ['useWorkletCallback', [0]], ['createWorklet', [0]], // animations' callbacks ['withTiming', [2]], ['withSpring', [2]], ['withDecay', [1]], ['withRepeat', [3]], ]); const objectHooks = new Set([ 'useAnimatedGestureHandler', 'useAnimatedScrollHandler', ]); const globals = new Set([ 'this', 'console', '_setGlobalConsole', 'Date', 'Array', 'ArrayBuffer', 'Date', 'HermesInternal', 'JSON', 'Math', 'Number', 'Object', 'String', 'Symbol', 'undefined', 'null', 'UIManager', 'requestAnimationFrame', '_WORKLET', 'arguments', 'Boolean', 'parseInt', 'parseFloat', 'Map', 'Set', '_log', '_updateProps', 'RegExp', 'Error', 'global', '_measure', '_scrollTo', '_getCurrentTime', '_eventTimestamp', '_frameTimestamp', 'isNaN', ]); // leaving way to avoid deep capturing by adding 'stopCapturing' to the blacklist const blacklistedFunctions = new Set([ 'stopCapturing', 'toString', 'map', 'filter', 'forEach', 'valueOf', 'toPrecision', 'toExponential', 'constructor', 'toFixed', 'toLocaleString', 'toSource', 'charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf', 'localeCompare', 'length', 'match', 'replace', 'search', 'slice', 'split', 'substr', 'substring', 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toUpperCase', 'every', 'join', 'pop', 'push', 'reduce', 'reduceRight', 'reverse', 'shift', 'slice', 'some', 'sort', 'splice', 'unshift', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'bind', 'apply', 'call', '__callAsync', ]); class ClosureGenerator { constructor() { this.trie = [{}, false]; } mergeAns(oldAns, newAns) { const [purePath, node] = oldAns; const [purePathUp, nodeUp] = newAns; if (purePathUp.length !== 0) { return [purePath.concat(purePathUp), nodeUp]; } else { return [purePath, node]; } } findPrefixRec(path) { const notFound = [[], null]; if (!path || path.node.type !== 'MemberExpression') { return notFound; } const memberExpressionNode = path.node; if (memberExpressionNode.property.type !== 'Identifier') { return notFound; } if ( memberExpressionNode.computed || memberExpressionNode.property.name === 'value' || blacklistedFunctions.has(memberExpressionNode.property.name) ) { // a.b[w] -> a.b.w in babel nodes // a.v.value // sth.map(() => ) return notFound; } if ( path.parent && path.parent.type === 'AssignmentExpression' && path.parent.left === path.node ) { /// captured.newProp = 5; return notFound; } const purePath = [memberExpressionNode.property.name]; const node = memberExpressionNode; const upAns = this.findPrefixRec(path.parentPath); return this.mergeAns([purePath, node], upAns); } findPrefix(base, babelPath) { const purePath = [base]; const node = babelPath.node; const upAns = this.findPrefixRec(babelPath.parentPath); return this.mergeAns([purePath, node], upAns); } addPath(base, babelPath) { const [purePath, node] = this.findPrefix(base, babelPath); let parent = this.trie; let index = -1; for (const current of purePath) { index++; if (parent[1]) { continue; } if (!parent[0][current]) { parent[0][current] = [{}, false]; } if (index === purePath.length - 1) { parent[0][current] = [node, true]; } parent = parent[0][current]; } } generateNodeForBase(t, current, parent) { const currentNode = parent[0][current]; if (currentNode[1]) { return currentNode[0]; } return t.objectExpression( Object.keys(currentNode[0]).map((propertyName) => t.objectProperty( t.identifier(propertyName), this.generateNodeForBase(t, propertyName, currentNode), false, true ) ) ); } generate(t, variables, names) { const arrayOfKeys = [...names]; return t.objectExpression( variables.map((variable, index) => t.objectProperty( t.identifier(variable.name), this.generateNodeForBase(t, arrayOfKeys[index], this.trie), false, true ) ) ); } } function buildWorkletString(t, fun, closureVariables, name) { function prependClosureVariablesIfNecessary(closureVariables, body) { if (closureVariables.length === 0) { return body; } return t.blockStatement([ t.variableDeclaration('const', [ t.variableDeclarator( t.objectPattern( closureVariables.map((variable) => t.objectProperty( t.identifier(variable.name), t.identifier(variable.name), false, true ) ) ), t.memberExpression(t.identifier('jsThis'), t.identifier('_closure')) ), ]), body, ]); } fun.traverse({ enter(path) { t.removeComments(path.node); }, }); const workletFunction = t.functionExpression( t.identifier(name), fun.node.params, prependClosureVariablesIfNecessary(closureVariables, fun.get('body').node) ); return generate(workletFunction, { compact: true }).code; } function processWorkletFunction(t, fun, fileName) { if (!t.isFunctionParent(fun)) { return; } const functionName = fun.node.id ? fun.node.id.name : '_f'; const closure = new Map(); const outputs = new Set(); const closureGenerator = new ClosureGenerator(); // We use copy because some of the plugins don't update bindings and // some even break them const astWorkletCopy = parse('\n(' + fun.toString() + '\n)'); traverse(astWorkletCopy, { ReferencedIdentifier(path) { const name = path.node.name; if (globals.has(name) || (fun.node.id && fun.node.id.name === name)) { return; } const parentNode = path.parent; if ( parentNode.type === 'MemberExpression' && (parentNode.property === path.node && !parentNode.computed) ) { return; } if ( parentNode.type === 'ObjectProperty' && path.parentPath.parent.type === 'ObjectExpression' && path.node !== parentNode.value ) { return; } let currentScope = path.scope; while (currentScope != null) { if (currentScope.bindings[name] != null) { return; } currentScope = currentScope.parent; } closure.set(name, path.node); closureGenerator.addPath(name, path); }, AssignmentExpression(path) { // test for .value = expressions const left = path.node.left; if ( t.isMemberExpression(left) && t.isIdentifier(left.object) && t.isIdentifier(left.property, { name: 'value' }) ) { outputs.add(left.object.name); } }, }); fun.traverse({ DirectiveLiteral(path) { if (path.node.value === 'worklet' && path.getFunctionParent() === fun) { path.parentPath.remove(); } }, }); const variables = Array.from(closure.values()); const privateFunctionId = t.identifier('_f'); // if we don't clone other modules won't process parts of newFun defined below // this is weird but couldn't find a better way to force transform helper to // process the function const clone = t.cloneNode(fun.node); const funExpression = t.functionExpression(null, clone.params, clone.body); const funString = buildWorkletString(t, fun, variables, functionName); const workletHash = hash(funString); const loc = fun && fun.node && fun.node.loc && fun.node.loc.start; if (loc) { const { line, column } = loc; if (typeof line === 'number' && typeof column === 'number') { fileName = `${fileName} (${line}:${column})`; } } const newFun = t.functionExpression( fun.id, [], t.blockStatement([ t.variableDeclaration('const', [ t.variableDeclarator(privateFunctionId, funExpression), ]), t.expressionStatement( t.assignmentExpression( '=', t.memberExpression( privateFunctionId, t.identifier('_closure'), false ), closureGenerator.generate(t, variables, closure.keys()) ) ), t.expressionStatement( t.assignmentExpression( '=', t.memberExpression( privateFunctionId, t.identifier('asString'), false ), t.stringLiteral(funString) ) ), t.expressionStatement( t.assignmentExpression( '=', t.memberExpression( privateFunctionId, t.identifier('__workletHash'), false ), t.numericLiteral(workletHash) ) ), t.expressionStatement( t.assignmentExpression( '=', t.memberExpression( privateFunctionId, t.identifier('__location'), false ), t.stringLiteral(fileName) ) ), t.expressionStatement( t.callExpression( t.memberExpression( t.identifier('global'), t.identifier('__reanimatedWorkletInit'), false ), [privateFunctionId] ) ), t.returnStatement(privateFunctionId), ]) ); const replacement = t.callExpression(newFun, []); // 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 ggg = function foo() { } // ^ in such a case we don't need to definte variable for the function const needDeclaration = t.isScopable(fun.parent) || t.isExportNamedDeclaration(fun.parent); fun.replaceWith( fun.node.id && needDeclaration ? t.variableDeclaration('const', [ t.variableDeclarator(fun.node.id, replacement), ]) : replacement ); } function processIfWorkletNode(t, fun, fileName) { fun.traverse({ DirectiveLiteral(path) { const value = path.node.value; if (value === 'worklet' && path.getFunctionParent() === fun) { // make sure "worklet" is listed among directives for the fun // this is necessary as because of some bug, babel will attempt to // process replaced function if it is nested inside another function const directives = fun.node.body.directives; if ( directives && directives.length > 0 && directives.some( (directive) => t.isDirectiveLiteral(directive.value) && directive.value.value === 'worklet' ) ) { processWorkletFunction(t, fun, fileName); } } }, }); } function processWorklets(t, path, fileName) { const name = path.node.callee.type === 'MemberExpression' ? path.node.callee.property.name : path.node.callee.name; if ( objectHooks.has(name) && path.get('arguments.0').type === 'ObjectExpression' ) { const objectPath = path.get('arguments.0.properties.0'); if (!objectPath) { // edge case empty object return; } for (let i = 0; i < objectPath.container.length; i++) { processWorkletFunction( t, objectPath.getSibling(i).get('value'), fileName ); } } else { const indexes = functionArgsToWorkletize.get(name); if (Array.isArray(indexes)) { indexes.forEach((index) => { processWorkletFunction(t, path.get(`arguments.${index}`), fileName); }); } } } const PLUGIN_BLACKLIST_NAMES = ['@babel/plugin-transform-object-assign']; const PLUGIN_BLACKLIST = PLUGIN_BLACKLIST_NAMES.map((pluginName) => { try { const blacklistedPluginObject = require(pluginName); // All Babel polyfills use the declare method that's why we can create them like that. // https://github.com/babel/babel/blob/32279147e6a69411035dd6c43dc819d668c74466/packages/babel-helper-plugin-utils/src/index.js#L1 const blacklistedPlugin = blacklistedPluginObject.default({ assertVersion: (_x) => true, }); visitors.explode(blacklistedPlugin.visitor); return blacklistedPlugin; } catch (e) { console.warn(`Plugin ${pluginName} couldn't be removed!`); } }); // plugin objects are created by babel internals and they don't carry any identifier function removePluginsFromBlacklist(plugins) { PLUGIN_BLACKLIST.forEach((blacklistedPlugin) => { if (!blacklistedPlugin) { return; } const toRemove = []; for (let i = 0; i < plugins.length; i++) { if ( JSON.stringify(Object.keys(plugins[i].visitor)) !== JSON.stringify(Object.keys(blacklistedPlugin.visitor)) ) { continue; } let areEqual = true; for (const key of Object.keys(blacklistedPlugin.visitor)) { if ( blacklistedPlugin.visitor[key].toString() !== plugins[i].visitor[key].toString() ) { areEqual = false; break; } } if (areEqual) { toRemove.push(i); } } toRemove.forEach((x) => plugins.splice(x, 1)); }); } module.exports = function({ types: t }) { return { pre() { // allows adding custom globals such as host-functions if (this.opts != null && Array.isArray(this.opts.globals)) { this.opts.globals.forEach((name) => { globals.add(name) }) } }, visitor: { CallExpression: { exit(path, state) { processWorklets(t, path, state.file.opts.filename); }, }, 'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression': { exit(path, state) { processIfWorkletNode(t, path, state.file.opts.filename); }, }, }, // In this way we can modify babel options // https://github.com/babel/babel/blob/eea156b2cb8deecfcf82d52aa1b71ba4995c7d68/packages/babel-core/src/transformation/normalize-opts.js#L64 manipulateOptions(opts, parserOpts) { const plugins = opts.plugins; removePluginsFromBlacklist(plugins); }, }; };