UNPKG

15 kBJavaScriptView Raw
1'use strict';
2
3const generate = require('@babel/generator').default;
4const hash = require('string-hash-64');
5const { visitors } = require('@babel/traverse');
6const traverse = require('@babel/traverse').default;
7const parse = require('@babel/parser').parse;
8
9/**
10 * 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)
11 */
12const functionArgsToWorkletize = new Map([
13 ['useAnimatedStyle', [0]],
14 ['useAnimatedProps', [0]],
15 ['createAnimatedPropAdapter', [0]],
16 ['useDerivedValue', [0]],
17 ['useAnimatedScrollHandler', [0]],
18 ['useAnimatedReaction', [0, 1]],
19 ['useWorkletCallback', [0]],
20 ['createWorklet', [0]],
21 // animations' callbacks
22 ['withTiming', [2]],
23 ['withSpring', [2]],
24 ['withDecay', [1]],
25 ['withRepeat', [3]],
26]);
27
28const objectHooks = new Set([
29 'useAnimatedGestureHandler',
30 'useAnimatedScrollHandler',
31]);
32
33const globals = new Set([
34 'this',
35 'console',
36 '_setGlobalConsole',
37 'Date',
38 'Array',
39 'ArrayBuffer',
40 'Date',
41 'HermesInternal',
42 'JSON',
43 'Math',
44 'Number',
45 'Object',
46 'String',
47 'Symbol',
48 'undefined',
49 'null',
50 'UIManager',
51 'requestAnimationFrame',
52 '_WORKLET',
53 'arguments',
54 'Boolean',
55 'parseInt',
56 'parseFloat',
57 'Map',
58 'Set',
59 '_log',
60 '_updateProps',
61 'RegExp',
62 'Error',
63 'global',
64 '_measure',
65 '_scrollTo',
66 '_getCurrentTime',
67 '_eventTimestamp',
68 '_frameTimestamp',
69 'isNaN',
70]);
71
72// leaving way to avoid deep capturing by adding 'stopCapturing' to the blacklist
73const blacklistedFunctions = new Set([
74 'stopCapturing',
75 'toString',
76 'map',
77 'filter',
78 'forEach',
79 'valueOf',
80 'toPrecision',
81 'toExponential',
82 'constructor',
83 'toFixed',
84 'toLocaleString',
85 'toSource',
86 'charAt',
87 'charCodeAt',
88 'concat',
89 'indexOf',
90 'lastIndexOf',
91 'localeCompare',
92 'length',
93 'match',
94 'replace',
95 'search',
96 'slice',
97 'split',
98 'substr',
99 'substring',
100 'toLocaleLowerCase',
101 'toLocaleUpperCase',
102 'toLowerCase',
103 'toUpperCase',
104 'every',
105 'join',
106 'pop',
107 'push',
108 'reduce',
109 'reduceRight',
110 'reverse',
111 'shift',
112 'slice',
113 'some',
114 'sort',
115 'splice',
116 'unshift',
117 'hasOwnProperty',
118 'isPrototypeOf',
119 'propertyIsEnumerable',
120 'bind',
121 'apply',
122 'call',
123 '__callAsync',
124]);
125
126class ClosureGenerator {
127 constructor() {
128 this.trie = [{}, false];
129 }
130
131 mergeAns(oldAns, newAns) {
132 const [purePath, node] = oldAns;
133 const [purePathUp, nodeUp] = newAns;
134 if (purePathUp.length !== 0) {
135 return [purePath.concat(purePathUp), nodeUp];
136 } else {
137 return [purePath, node];
138 }
139 }
140
141 findPrefixRec(path) {
142 const notFound = [[], null];
143 if (!path || path.node.type !== 'MemberExpression') {
144 return notFound;
145 }
146 const memberExpressionNode = path.node;
147 if (memberExpressionNode.property.type !== 'Identifier') {
148 return notFound;
149 }
150 if (
151 memberExpressionNode.computed ||
152 memberExpressionNode.property.name === 'value' ||
153 blacklistedFunctions.has(memberExpressionNode.property.name)
154 ) {
155 // a.b[w] -> a.b.w in babel nodes
156 // a.v.value
157 // sth.map(() => )
158 return notFound;
159 }
160 if (
161 path.parent &&
162 path.parent.type === 'AssignmentExpression' &&
163 path.parent.left === path.node
164 ) {
165 /// captured.newProp = 5;
166 return notFound;
167 }
168 const purePath = [memberExpressionNode.property.name];
169 const node = memberExpressionNode;
170 const upAns = this.findPrefixRec(path.parentPath);
171 return this.mergeAns([purePath, node], upAns);
172 }
173
174 findPrefix(base, babelPath) {
175 const purePath = [base];
176 const node = babelPath.node;
177 const upAns = this.findPrefixRec(babelPath.parentPath);
178 return this.mergeAns([purePath, node], upAns);
179 }
180
181 addPath(base, babelPath) {
182 const [purePath, node] = this.findPrefix(base, babelPath);
183 let parent = this.trie;
184 let index = -1;
185 for (const current of purePath) {
186 index++;
187 if (parent[1]) {
188 continue;
189 }
190 if (!parent[0][current]) {
191 parent[0][current] = [{}, false];
192 }
193 if (index === purePath.length - 1) {
194 parent[0][current] = [node, true];
195 }
196 parent = parent[0][current];
197 }
198 }
199
200 generateNodeForBase(t, current, parent) {
201 const currentNode = parent[0][current];
202 if (currentNode[1]) {
203 return currentNode[0];
204 }
205 return t.objectExpression(
206 Object.keys(currentNode[0]).map((propertyName) =>
207 t.objectProperty(
208 t.identifier(propertyName),
209 this.generateNodeForBase(t, propertyName, currentNode),
210 false,
211 true
212 )
213 )
214 );
215 }
216
217 generate(t, variables, names) {
218 const arrayOfKeys = [...names];
219 return t.objectExpression(
220 variables.map((variable, index) =>
221 t.objectProperty(
222 t.identifier(variable.name),
223 this.generateNodeForBase(t, arrayOfKeys[index], this.trie),
224 false,
225 true
226 )
227 )
228 );
229 }
230}
231
232function buildWorkletString(t, fun, closureVariables, name) {
233 function prependClosureVariablesIfNecessary(closureVariables, body) {
234 if (closureVariables.length === 0) {
235 return body;
236 }
237
238 return t.blockStatement([
239 t.variableDeclaration('const', [
240 t.variableDeclarator(
241 t.objectPattern(
242 closureVariables.map((variable) =>
243 t.objectProperty(
244 t.identifier(variable.name),
245 t.identifier(variable.name),
246 false,
247 true
248 )
249 )
250 ),
251 t.memberExpression(t.identifier('jsThis'), t.identifier('_closure'))
252 ),
253 ]),
254 body,
255 ]);
256 }
257
258 fun.traverse({
259 enter(path) {
260 t.removeComments(path.node);
261 },
262 });
263
264 const workletFunction = t.functionExpression(
265 t.identifier(name),
266 fun.node.params,
267 prependClosureVariablesIfNecessary(closureVariables, fun.get('body').node)
268 );
269
270 return generate(workletFunction, { compact: true }).code;
271}
272
273function processWorkletFunction(t, fun, fileName) {
274 if (!t.isFunctionParent(fun)) {
275 return;
276 }
277
278 const functionName = fun.node.id ? fun.node.id.name : '_f';
279
280 const closure = new Map();
281 const outputs = new Set();
282 const closureGenerator = new ClosureGenerator();
283
284 // We use copy because some of the plugins don't update bindings and
285 // some even break them
286 const astWorkletCopy = parse('\n(' + fun.toString() + '\n)');
287
288 traverse(astWorkletCopy, {
289 ReferencedIdentifier(path) {
290 const name = path.node.name;
291 if (globals.has(name) || (fun.node.id && fun.node.id.name === name)) {
292 return;
293 }
294
295 const parentNode = path.parent;
296
297 if (
298 parentNode.type === 'MemberExpression' &&
299 (parentNode.property === path.node && !parentNode.computed)
300 ) {
301 return;
302 }
303
304 if (
305 parentNode.type === 'ObjectProperty' &&
306 path.parentPath.parent.type === 'ObjectExpression' &&
307 path.node !== parentNode.value
308 ) {
309 return;
310 }
311
312 let currentScope = path.scope;
313
314 while (currentScope != null) {
315 if (currentScope.bindings[name] != null) {
316 return;
317 }
318 currentScope = currentScope.parent;
319 }
320 closure.set(name, path.node);
321 closureGenerator.addPath(name, path);
322 },
323 AssignmentExpression(path) {
324 // test for <somethin>.value = <something> expressions
325 const left = path.node.left;
326 if (
327 t.isMemberExpression(left) &&
328 t.isIdentifier(left.object) &&
329 t.isIdentifier(left.property, { name: 'value' })
330 ) {
331 outputs.add(left.object.name);
332 }
333 },
334 });
335
336 fun.traverse({
337 DirectiveLiteral(path) {
338 if (path.node.value === 'worklet' && path.getFunctionParent() === fun) {
339 path.parentPath.remove();
340 }
341 },
342 });
343 const variables = Array.from(closure.values());
344
345 const privateFunctionId = t.identifier('_f');
346
347 // if we don't clone other modules won't process parts of newFun defined below
348 // this is weird but couldn't find a better way to force transform helper to
349 // process the function
350 const clone = t.cloneNode(fun.node);
351 const funExpression = t.functionExpression(null, clone.params, clone.body);
352
353 const funString = buildWorkletString(t, fun, variables, functionName);
354 const workletHash = hash(funString);
355
356 const loc = fun && fun.node && fun.node.loc && fun.node.loc.start;
357 if (loc) {
358 const { line, column } = loc;
359 if (typeof line === 'number' && typeof column === 'number') {
360 fileName = `${fileName} (${line}:${column})`;
361 }
362 }
363
364 const newFun = t.functionExpression(
365 fun.id,
366 [],
367 t.blockStatement([
368 t.variableDeclaration('const', [
369 t.variableDeclarator(privateFunctionId, funExpression),
370 ]),
371 t.expressionStatement(
372 t.assignmentExpression(
373 '=',
374 t.memberExpression(
375 privateFunctionId,
376 t.identifier('_closure'),
377 false
378 ),
379 closureGenerator.generate(t, variables, closure.keys())
380 )
381 ),
382 t.expressionStatement(
383 t.assignmentExpression(
384 '=',
385 t.memberExpression(
386 privateFunctionId,
387 t.identifier('asString'),
388 false
389 ),
390 t.stringLiteral(funString)
391 )
392 ),
393 t.expressionStatement(
394 t.assignmentExpression(
395 '=',
396 t.memberExpression(
397 privateFunctionId,
398 t.identifier('__workletHash'),
399 false
400 ),
401 t.numericLiteral(workletHash)
402 )
403 ),
404 t.expressionStatement(
405 t.assignmentExpression(
406 '=',
407 t.memberExpression(
408 privateFunctionId,
409 t.identifier('__location'),
410 false
411 ),
412 t.stringLiteral(fileName)
413 )
414 ),
415 t.expressionStatement(
416 t.callExpression(
417 t.memberExpression(
418 t.identifier('global'),
419 t.identifier('__reanimatedWorkletInit'),
420 false
421 ),
422 [privateFunctionId]
423 )
424 ),
425 t.returnStatement(privateFunctionId),
426 ])
427 );
428
429 const replacement = t.callExpression(newFun, []);
430 // we check if function needs to be assigned to variable declaration.
431 // This is needed if function definition directly in a scope. Some other ways
432 // where function definition can be used is for example with variable declaration:
433 // const ggg = function foo() { }
434 // ^ in such a case we don't need to definte variable for the function
435 const needDeclaration =
436 t.isScopable(fun.parent) || t.isExportNamedDeclaration(fun.parent);
437 fun.replaceWith(
438 fun.node.id && needDeclaration
439 ? t.variableDeclaration('const', [
440 t.variableDeclarator(fun.node.id, replacement),
441 ])
442 : replacement
443 );
444}
445
446function processIfWorkletNode(t, fun, fileName) {
447 fun.traverse({
448 DirectiveLiteral(path) {
449 const value = path.node.value;
450 if (value === 'worklet' && path.getFunctionParent() === fun) {
451 // make sure "worklet" is listed among directives for the fun
452 // this is necessary as because of some bug, babel will attempt to
453 // process replaced function if it is nested inside another function
454 const directives = fun.node.body.directives;
455 if (
456 directives &&
457 directives.length > 0 &&
458 directives.some(
459 (directive) =>
460 t.isDirectiveLiteral(directive.value) &&
461 directive.value.value === 'worklet'
462 )
463 ) {
464 processWorkletFunction(t, fun, fileName);
465 }
466 }
467 },
468 });
469}
470
471function processWorklets(t, path, fileName) {
472 const name =
473 path.node.callee.type === 'MemberExpression'
474 ? path.node.callee.property.name
475 : path.node.callee.name;
476 if (
477 objectHooks.has(name) &&
478 path.get('arguments.0').type === 'ObjectExpression'
479 ) {
480 const objectPath = path.get('arguments.0.properties.0');
481 if (!objectPath) {
482 // edge case empty object
483 return;
484 }
485 for (let i = 0; i < objectPath.container.length; i++) {
486 processWorkletFunction(
487 t,
488 objectPath.getSibling(i).get('value'),
489 fileName
490 );
491 }
492 } else {
493 const indexes = functionArgsToWorkletize.get(name);
494 if (Array.isArray(indexes)) {
495 indexes.forEach((index) => {
496 processWorkletFunction(t, path.get(`arguments.${index}`), fileName);
497 });
498 }
499 }
500}
501
502const PLUGIN_BLACKLIST_NAMES = ['@babel/plugin-transform-object-assign'];
503
504const PLUGIN_BLACKLIST = PLUGIN_BLACKLIST_NAMES.map((pluginName) => {
505 try {
506 const blacklistedPluginObject = require(pluginName);
507 // All Babel polyfills use the declare method that's why we can create them like that.
508 // https://github.com/babel/babel/blob/32279147e6a69411035dd6c43dc819d668c74466/packages/babel-helper-plugin-utils/src/index.js#L1
509 const blacklistedPlugin = blacklistedPluginObject.default({
510 assertVersion: (_x) => true,
511 });
512
513 visitors.explode(blacklistedPlugin.visitor);
514 return blacklistedPlugin;
515 } catch (e) {
516 console.warn(`Plugin ${pluginName} couldn't be removed!`);
517 }
518});
519
520// plugin objects are created by babel internals and they don't carry any identifier
521function removePluginsFromBlacklist(plugins) {
522 PLUGIN_BLACKLIST.forEach((blacklistedPlugin) => {
523 if (!blacklistedPlugin) {
524 return;
525 }
526
527 const toRemove = [];
528 for (let i = 0; i < plugins.length; i++) {
529 if (
530 JSON.stringify(Object.keys(plugins[i].visitor)) !==
531 JSON.stringify(Object.keys(blacklistedPlugin.visitor))
532 ) {
533 continue;
534 }
535 let areEqual = true;
536 for (const key of Object.keys(blacklistedPlugin.visitor)) {
537 if (
538 blacklistedPlugin.visitor[key].toString() !==
539 plugins[i].visitor[key].toString()
540 ) {
541 areEqual = false;
542 break;
543 }
544 }
545
546 if (areEqual) {
547 toRemove.push(i);
548 }
549 }
550
551 toRemove.forEach((x) => plugins.splice(x, 1));
552 });
553}
554
555module.exports = function({ types: t }) {
556 return {
557 visitor: {
558 CallExpression: {
559 exit(path, state) {
560 processWorklets(t, path, state.file.opts.filename);
561 },
562 },
563 'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression': {
564 exit(path, state) {
565 processIfWorkletNode(t, path, state.file.opts.filename);
566 },
567 },
568 },
569 // In this way we can modify babel options
570 // https://github.com/babel/babel/blob/eea156b2cb8deecfcf82d52aa1b71ba4995c7d68/packages/babel-core/src/transformation/normalize-opts.js#L64
571 manipulateOptions(opts, parserOpts) {
572 const plugins = opts.plugins;
573 removePluginsFromBlacklist(plugins);
574 },
575 };
576};