react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
808 lines (725 loc) • 22.3 kB
JavaScript
'use strict';
const generate = require('@babel/generator').default;
const hash = require('string-hash-64');
const traverse = require('@babel/traverse').default;
const { transformSync } = require('@babel/core');
const fs = require('fs');
const convertSourceMap = require('convert-source-map');
/**
* 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([
['useFrameCallback', [0]],
['useAnimatedStyle', [0]],
['useAnimatedProps', [0]],
['createAnimatedPropAdapter', [0]],
['useDerivedValue', [0]],
['useAnimatedScrollHandler', [0]],
['useAnimatedReaction', [0, 1]],
['useWorkletCallback', [0]],
// animations' callbacks
['withTiming', [2]],
['withSpring', [2]],
['withDecay', [1]],
['withRepeat', [3]],
]);
const objectHooks = new Set([
'useAnimatedGestureHandler',
'useAnimatedScrollHandler',
]);
const globals = new Set([
'this',
'console',
'performance',
'Date',
'Array',
'ArrayBuffer',
'Int8Array',
'Int16Array',
'Int32Array',
'Uint8Array',
'Uint8ClampedArray',
'Uint16Array',
'Uint32Array',
'Float32Array',
'Float64Array',
'Date',
'HermesInternal',
'JSON',
'Math',
'Number',
'Object',
'String',
'Symbol',
'undefined',
'null',
'UIManager',
'requestAnimationFrame',
'setImmediate',
'_WORKLET',
'arguments',
'Boolean',
'parseInt',
'parseFloat',
'Map',
'WeakMap',
'WeakRef',
'Set',
'_log',
'_scheduleOnJS',
'_makeShareableClone',
'_updateDataSynchronously',
'eval',
'_updatePropsPaper',
'_updatePropsFabric',
'_removeShadowNodeFromRegistry',
'RegExp',
'Error',
'ErrorUtils',
'global',
'_measure',
'_scrollTo',
'_dispatchCommand',
'_setGestureState',
'_getCurrentTime',
'isNaN',
'LayoutAnimationRepository',
'_notifyAboutProgress',
'_notifyAboutEnd',
]);
const gestureHandlerGestureObjects = new Set([
// from https://github.com/software-mansion/react-native-gesture-handler/blob/new-api/src/handlers/gestures/gestureObjects.ts
'Tap',
'Pan',
'Pinch',
'Rotation',
'Fling',
'LongPress',
'ForceTouch',
'Native',
'Manual',
'Race',
'Simultaneous',
'Exclusive',
]);
const gestureHandlerBuilderMethods = new Set([
'onBegin',
'onStart',
'onEnd',
'onFinalize',
'onUpdate',
'onChange',
'onTouchesDown',
'onTouchesMove',
'onTouchesUp',
'onTouchesCancelled',
]);
function isRelease() {
return ['production', 'release'].includes(process.env.BABEL_ENV);
}
function shouldGenerateSourceMap() {
if (isRelease()) {
return false;
}
if (process.env.REANIMATED_PLUGIN_TESTS === 'jest') {
// We want to detect this, so we can disable source maps (because they break
// snapshot tests with jest).
return false;
}
return true;
}
function buildWorkletString(t, fun, closureVariables, name, inputMap) {
function prependClosureVariablesIfNecessary() {
const closureDeclaration = 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.thisExpression(), t.identifier('_closure'))
),
]);
function prependClosure(path) {
if (closureVariables.length === 0 || path.parent.type !== 'Program') {
return;
}
path.node.body.body.unshift(closureDeclaration);
}
function prepandRecursiveDeclaration(path) {
if (path.parent.type === 'Program' && path.node.id && path.scope.parent) {
const hasRecursiveCalls =
path.scope.parent.bindings[path.node.id.name]?.references > 0;
if (hasRecursiveCalls) {
path.node.body.body.unshift(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(path.node.id.name),
t.memberExpression(t.thisExpression(), t.identifier('_recur'))
),
])
);
}
}
}
return {
visitor: {
'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ObjectMethod':
(path) => {
prependClosure(path);
prepandRecursiveDeclaration(path);
},
},
};
}
const expression =
fun.program.body.find(({ type }) => type === 'FunctionDeclaration') ||
fun.program.body.find(({ type }) => type === 'ExpressionStatement')
.expression;
const workletFunction = t.functionExpression(
t.identifier(name),
expression.params,
expression.body
);
const code = generate(workletFunction).code;
const includeSourceMap = shouldGenerateSourceMap();
if (includeSourceMap) {
// Clear contents array (should be empty anyways)
inputMap.sourcesContent = [];
// Include source contents in source map, because Flipper/iframe is not
// allowed to read files from disk.
for (const sourceFile of inputMap.sources) {
inputMap.sourcesContent.push(
fs.readFileSync(sourceFile).toString('utf-8')
);
}
}
const transformed = transformSync(code, {
plugins: [prependClosureVariablesIfNecessary()],
compact: !includeSourceMap,
sourceMaps: includeSourceMap,
inputSourceMap: inputMap,
ast: false,
babelrc: false,
configFile: false,
comments: false,
});
let sourceMap;
if (includeSourceMap) {
sourceMap = convertSourceMap.fromObject(transformed.map).toObject();
// sourcesContent field contains a full source code of the file which contains the worklet
// and is not needed by the source map interpreter in order to symbolicate a stack trace.
// Therefore, we remove it to reduce the bandwith and avoid sending it potentially multiple times
// in files that contain multiple worklets. Along with sourcesContent.
delete sourceMap.sourcesContent;
}
return [transformed.code, JSON.stringify(sourceMap)];
}
function makeWorkletName(t, fun) {
if (t.isObjectMethod(fun)) {
return fun.node.key.name;
}
if (t.isFunctionDeclaration(fun)) {
return fun.node.id.name;
}
if (t.isFunctionExpression(fun) && t.isIdentifier(fun.node.id)) {
return fun.node.id.name;
}
return 'anonymous'; // fallback for ArrowFunctionExpression and unnamed FunctionExpression
}
function makeWorklet(t, fun, state) {
// Returns a new FunctionExpression which is a workletized version of provided
// FunctionDeclaration, FunctionExpression, ArrowFunctionExpression or ObjectMethod.
const functionName = makeWorkletName(t, fun);
const closure = new Map();
// remove 'worklet'; directive before generating string
fun.traverse({
DirectiveLiteral(path) {
if (path.node.value === 'worklet' && path.getFunctionParent() === fun) {
path.parentPath.remove();
}
},
});
// We use copy because some of the plugins don't update bindings and
// some even break them
const codeObject = generate(fun.node, {
sourceMaps: true,
sourceFileName: state.file.opts.filename,
});
// We need to add a newline at the end, because there could potentially be a
// comment after the function that gets included here, and then the closing
// bracket would become part of the comment thus resulting in an error, since
// there is a missing closing bracket.
const code =
'(' + (t.isObjectMethod(fun) ? 'function ' : '') + codeObject.code + '\n)';
const transformed = transformSync(code, {
filename: state.file.opts.filename,
presets: ['@babel/preset-typescript'],
plugins: [
'@babel/plugin-transform-shorthand-properties',
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
['@babel/plugin-transform-template-literals', { loose: true }],
],
ast: true,
babelrc: false,
configFile: false,
inputSourceMap: codeObject.map,
});
traverse(transformed.ast, {
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);
},
});
const variables = Array.from(closure.values());
const privateFunctionId = t.identifier('_f');
const clone = t.cloneNode(fun.node);
let funExpression;
if (clone.body.type === 'BlockStatement') {
funExpression = t.functionExpression(null, clone.params, clone.body);
} else {
funExpression = clone;
}
const [funString, sourceMapString] = buildWorkletString(
t,
transformed.ast,
variables,
functionName,
transformed.map
);
const workletHash = hash(funString);
let location = state.file.opts.filename;
if (state.opts && state.opts.relativeSourceLocation) {
const path = require('path');
location = path.relative(state.cwd, location);
}
let lineOffset = 1;
if (closure.size > 0) {
// When worklet captures some variables, we append closure destructing at
// the beginning of the function body. This effectively results in line
// numbers shifting by the number of captured variables (size of the
// closure) + 2 (for the opening and closing brackets of the destruct
// statement)
lineOffset -= closure.size + 2;
}
const pathForStringDefinitions = fun.parentPath.isProgram()
? fun
: fun.findParent((path) => path.parentPath.isProgram());
const initDataId =
pathForStringDefinitions.parentPath.scope.generateUidIdentifier(
`worklet_${workletHash}_init_data`
);
const initDataObjectExpression = t.objectExpression([
t.objectProperty(t.identifier('code'), t.stringLiteral(funString)),
t.objectProperty(t.identifier('location'), t.stringLiteral(location)),
]);
if (sourceMapString) {
initDataObjectExpression.properties.push(
t.objectProperty(
t.identifier('sourceMap'),
t.stringLiteral(sourceMapString)
)
);
}
pathForStringDefinitions.insertBefore(
t.variableDeclaration('const', [
t.variableDeclarator(initDataId, initDataObjectExpression),
])
);
const statements = [
t.variableDeclaration('const', [
t.variableDeclarator(privateFunctionId, funExpression),
]),
t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(privateFunctionId, t.identifier('_closure'), false),
t.objectExpression(
variables.map((variable) =>
t.objectProperty(t.identifier(variable.name), variable, false, true)
)
)
)
),
t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(
privateFunctionId,
t.identifier('__initData'),
false
),
initDataId
)
),
t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(
privateFunctionId,
t.identifier('__workletHash'),
false
),
t.numericLiteral(workletHash)
)
),
];
if (!isRelease()) {
statements.unshift(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('_e'),
t.arrayExpression([
t.newExpression(t.identifier('Error'), []),
t.numericLiteral(lineOffset),
t.numericLiteral(-20), // the placement of opening bracket after Exception in line that defined '_e' variable
])
),
])
);
statements.push(
t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(
privateFunctionId,
t.identifier('__stackDetails'),
false
),
t.identifier('_e')
)
)
);
}
statements.push(t.returnStatement(privateFunctionId));
const newFun = t.functionExpression(fun.id, [], t.blockStatement(statements));
return newFun;
}
function processWorkletFunction(t, fun, state) {
// Replaces FunctionDeclaration, FunctionExpression or ArrowFunctionExpression
// with a workletized version of itself.
if (!t.isFunctionParent(fun)) {
return;
}
const newFun = makeWorklet(t, fun, state);
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 define 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 processWorkletObjectMethod(t, path, state) {
// Replaces ObjectMethod with a workletized version of itself.
if (!t.isFunctionParent(path)) {
return;
}
const newFun = makeWorklet(t, path, state);
const replacement = t.objectProperty(
t.identifier(path.node.key.name),
t.callExpression(newFun, [])
);
path.replaceWith(replacement);
}
function processIfWorkletNode(t, fun, state) {
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, state);
}
}
},
});
}
function processIfGestureHandlerEventCallbackFunctionNode(t, fun, state) {
// Auto-workletizes React Native Gesture Handler callback functions.
// Detects `Gesture.Tap().onEnd(<fun>)` or similar, but skips `something.onEnd(<fun>)`.
// Supports method chaining as well, e.g. `Gesture.Tap().onStart(<fun1>).onUpdate(<fun2>).onEnd(<fun3>)`.
// Example #1: `Gesture.Tap().onEnd(<fun>)`
/*
CallExpression(
callee: MemberExpression(
object: CallExpression(
callee: MemberExpression(
object: Identifier('Gesture')
property: Identifier('Tap')
)
)
property: Identifier('onEnd')
)
arguments: [fun]
)
*/
// Example #2: `Gesture.Tap().onStart(<fun1>).onUpdate(<fun2>).onEnd(<fun3>)`
/*
CallExpression(
callee: MemberExpression(
object: CallExpression(
callee: MemberExpression(
object: CallExpression(
callee: MemberExpression(
object: CallExpression(
callee: MemberExpression(
object: Identifier('Gesture')
property: Identifier('Tap')
)
)
property: Identifier('onStart')
)
arguments: [fun1]
)
property: Identifier('onUpdate')
)
arguments: [fun2]
)
property: Identifier('onEnd')
)
arguments: [fun3]
)
*/
if (
t.isCallExpression(fun.parent) &&
isGestureObjectEventCallbackMethod(t, fun.parent.callee)
) {
processWorkletFunction(t, fun, state);
}
}
function isGestureObjectEventCallbackMethod(t, node) {
// Checks if node matches the pattern `Gesture.Foo()[*].onBar`
// where `[*]` represents any number of method calls.
return (
t.isMemberExpression(node) &&
t.isIdentifier(node.property) &&
gestureHandlerBuilderMethods.has(node.property.name) &&
containsGestureObject(t, node.object)
);
}
function containsGestureObject(t, node) {
// Checks if node matches the pattern `Gesture.Foo()[*]`
// where `[*]` represents any number of chained method calls, like `.something(42)`.
// direct call
if (isGestureObject(t, node)) {
return true;
}
// method chaining
if (
t.isCallExpression(node) &&
t.isMemberExpression(node.callee) &&
containsGestureObject(t, node.callee.object)
) {
return true;
}
return false;
}
function isGestureObject(t, node) {
// Checks if node matches `Gesture.Tap()` or similar.
/*
node: CallExpression(
callee: MemberExpression(
object: Identifier('Gesture')
property: Identifier('Tap')
)
)
*/
return (
t.isCallExpression(node) &&
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object) &&
node.callee.object.name === 'Gesture' &&
t.isIdentifier(node.callee.property) &&
gestureHandlerGestureObjects.has(node.callee.property.name)
);
}
function processWorklets(t, path, state) {
const callee =
path.node.callee.type === 'SequenceExpression'
? path.node.callee.expressions[path.node.callee.expressions.length - 1]
: path.node.callee;
const name =
callee.type === 'MemberExpression' ? callee.property.name : callee.name;
if (
objectHooks.has(name) &&
path.get('arguments.0').type === 'ObjectExpression'
) {
const properties = path.get('arguments.0.properties');
for (const property of properties) {
if (t.isObjectMethod(property)) {
processWorkletObjectMethod(t, property, state);
} else {
const value = property.get('value');
processWorkletFunction(t, value, state);
}
}
} else {
const indexes = functionArgsToWorkletize.get(name);
if (Array.isArray(indexes)) {
indexes.forEach((index) => {
processWorkletFunction(t, path.get(`arguments.${index}`), state);
});
}
}
}
function generateInlineStylesWarning(t, memberExpression) {
// replaces `sharedvalue.value` with `(()=>{console.warn(require('react-native-reanimated').getUseOfValueInStyleWarning());return sharedvalue.value;})()`
return t.callExpression(
t.arrowFunctionExpression(
[],
t.blockStatement([
t.expressionStatement(
t.callExpression(
t.memberExpression(t.identifier('console'), t.identifier('warn')),
[
t.callExpression(
t.memberExpression(
t.callExpression(t.identifier('require'), [
t.stringLiteral('react-native-reanimated'),
]),
t.identifier('getUseOfValueInStyleWarning')
),
[]
),
]
)
),
t.returnStatement(memberExpression.node),
])
),
[]
);
}
function processPropertyValueForInlineStylesWarning(t, path) {
// if it's something like object.value then raise a warning
if (t.isMemberExpression(path)) {
if (path.get('property').node.name === 'value') {
path.replaceWith(generateInlineStylesWarning(t, path));
}
}
}
function processTransformPropertyForInlineStylesWarning(t, path) {
if (t.isArrayExpression(path)) {
const elements = path.get('elements');
for (const element of elements) {
if (t.isObjectExpression(element)) {
processStyleObjectForInlineStylesWarning(t, element);
}
}
}
}
function processStyleObjectForInlineStylesWarning(t, path) {
const properties = path.get('properties');
for (const property of properties) {
const value = property.get('value');
if (t.isProperty(property)) {
if (property.get('key').node.name === 'transform') {
processTransformPropertyForInlineStylesWarning(t, value);
} else {
processPropertyValueForInlineStylesWarning(t, value);
}
}
}
}
function processInlineStylesWarning(t, path, state) {
if (isRelease()) return;
if (state.opts.disableInlineStylesWarning) return;
if (path.get('name').node.name !== 'style') return;
if (!t.isJSXExpressionContainer(path.get('value'))) return;
const expression = path.get('value').get('expression');
// style={[{...}, {...}]}
if (t.isArrayExpression(expression)) {
const elements = expression.get('elements');
for (const element of elements) {
if (t.isObjectExpression(element)) {
processStyleObjectForInlineStylesWarning(t, element);
}
}
}
// style={{...}}
else if (t.isObjectExpression(expression)) {
processStyleObjectForInlineStylesWarning(t, expression);
}
}
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: {
enter(path, state) {
processWorklets(t, path, state);
},
},
'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression': {
enter(path, state) {
processIfWorkletNode(t, path, state);
processIfGestureHandlerEventCallbackFunctionNode(t, path, state);
},
},
JSXAttribute: {
enter(path, state) {
processInlineStylesWarning(t, path, state);
},
},
},
};
};