create-expo-cljs-app
Version:
Create a react native application with Expo and Shadow-CLJS!
788 lines (723 loc) • 19.8 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');
/**
* 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',
'Int8Array',
'Int16Array',
'Int32Array',
'Uint8Array',
'Uint8ClampedArray',
'Uint16Array',
'Uint32Array',
'Float32Array',
'Float64Array',
'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',
'_setGestureState',
'_getCurrentTime',
'_eventTimestamp',
'_frameTimestamp',
'isNaN',
'LayoutAnimationRepository',
'_stopObservingProgress',
'_startObservingProgress',
]);
// 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',
'includes',
]);
const possibleOptFunction = new Set(['interpolate']);
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',
'onTouchesDown',
'onTouchesMove',
'onTouchesUp',
'onTouchesCancelled',
]);
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,
]);
}
traverse(fun, {
enter(path) {
t.removeComments(path.node);
},
});
const workletFunction = t.functionExpression(
t.identifier(name),
fun.program.body[0].expression.params,
prependClosureVariablesIfNecessary(
closureVariables,
fun.program.body[0].expression.body
)
);
return generate(workletFunction, { compact: true }).code;
}
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 '_f'; // fallback for ArrowFunctionExpression and unnamed FunctionExpression
}
function makeWorklet(t, fun, fileName) {
// 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();
const outputs = new Set();
const closureGenerator = new ClosureGenerator();
const options = {};
// remove 'worklet'; directive before calling .toString()
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 code =
'\n(' + (t.isObjectMethod(fun) ? 'function ' : '') + fun.toString() + '\n)';
const transformed = transformSync(code, {
filename: 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,
});
if (
fun.parent &&
fun.parent.callee &&
fun.parent.callee.name === 'useAnimatedStyle'
) {
options.optFlags = isPossibleOptimization(transformed.ast);
}
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);
closureGenerator.addPath(name, path);
},
AssignmentExpression(path) {
// test for <something>.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);
}
},
});
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 = buildWorkletString(
t,
transformed.ast,
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 statements = [
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)
)
),
];
if (options && options.optFlags) {
statements.push(
t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(
privateFunctionId,
t.identifier('__optimalization'),
false
),
t.numericLiteral(options.optFlags)
)
)
);
}
statements.push(
t.expressionStatement(
t.callExpression(
t.memberExpression(
t.identifier('global'),
t.identifier('__reanimatedWorkletInit'),
false
),
[privateFunctionId]
)
)
);
statements.push(t.returnStatement(privateFunctionId));
const newFun = t.functionExpression(fun.id, [], t.blockStatement(statements));
return newFun;
}
function processWorkletFunction(t, fun, fileName) {
// Replaces FunctionDeclaration, FunctionExpression or ArrowFunctionExpression
// with a workletized version of itself.
if (!t.isFunctionParent(fun)) {
return;
}
const newFun = makeWorklet(t, fun, fileName);
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, fileName) {
// Replaces ObjectMethod with a workletized version of itself.
if (!t.isFunctionParent(path)) {
return;
}
const newFun = makeWorklet(t, path, fileName);
const replacement = t.objectProperty(
t.identifier(path.node.key.name),
t.callExpression(newFun, [])
);
path.replaceWith(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 processIfGestureHandlerEventCallbackFunctionNode(t, fun, fileName) {
// 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, fileName);
}
}
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, 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 properties = path.get('arguments.0.properties');
for (const property of properties) {
if (t.isObjectMethod(property)) {
processWorkletObjectMethod(t, property, fileName);
} else {
const value = property.get('value');
processWorkletFunction(t, value, fileName);
}
}
} else {
const indexes = functionArgsToWorkletize.get(name);
if (Array.isArray(indexes)) {
indexes.forEach((index) => {
processWorkletFunction(t, path.get(`arguments.${index}`), fileName);
});
}
}
}
const FUNCTIONLESS_FLAG = 0b00000001;
const STATEMENTLESS_FLAG = 0b00000010;
function isPossibleOptimization(fun) {
let isFunctionCall = false;
let isStatement = false;
traverse(fun, {
CallExpression(path) {
if (!possibleOptFunction.has(path.node.callee.name)) {
isFunctionCall = true;
}
},
IfStatement() {
isStatement = true;
},
});
let flags = 0;
if (!isFunctionCall) {
flags = flags | FUNCTIONLESS_FLAG;
}
if (!isStatement) {
flags = flags | STATEMENTLESS_FLAG;
}
return flags;
}
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.file.opts.filename);
},
},
'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression': {
enter(path, state) {
const fileName = state.file.opts.filename;
processIfWorkletNode(t, path, fileName);
processIfGestureHandlerEventCallbackFunctionNode(t, path, fileName);
},
},
},
};
};