GT2/Ejectable/node_modules/react-native-reanimated/plugin.js

585 lines
15 KiB
JavaScript

'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 <somethin>.value = <something> 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);
},
};
};