1 | 'use strict';
|
2 |
|
3 | const generate = require('@babel/generator').default;
|
4 | const hash = require('string-hash-64');
|
5 | const { visitors } = require('@babel/traverse');
|
6 | const traverse = require('@babel/traverse').default;
|
7 | const parse = require('@babel/parser').parse;
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | const 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 |
|
22 | ['withTiming', [2]],
|
23 | ['withSpring', [2]],
|
24 | ['withDecay', [1]],
|
25 | ['withRepeat', [3]],
|
26 | ]);
|
27 |
|
28 | const objectHooks = new Set([
|
29 | 'useAnimatedGestureHandler',
|
30 | 'useAnimatedScrollHandler',
|
31 | ]);
|
32 |
|
33 | const 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 |
|
73 | const 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 |
|
126 | class 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 |
|
156 |
|
157 |
|
158 | return notFound;
|
159 | }
|
160 | if (
|
161 | path.parent &&
|
162 | path.parent.type === 'AssignmentExpression' &&
|
163 | path.parent.left === path.node
|
164 | ) {
|
165 |
|
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 |
|
232 | function 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 |
|
273 | function 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 |
|
285 |
|
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 |
|
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 |
|
348 |
|
349 |
|
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 |
|
431 |
|
432 |
|
433 |
|
434 |
|
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 |
|
446 | function processIfWorkletNode(t, fun, fileName) {
|
447 | fun.traverse({
|
448 | DirectiveLiteral(path) {
|
449 | const value = path.node.value;
|
450 | if (value === 'worklet' && path.getFunctionParent() === fun) {
|
451 |
|
452 |
|
453 |
|
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 |
|
471 | function 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 |
|
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 |
|
502 | const PLUGIN_BLACKLIST_NAMES = ['@babel/plugin-transform-object-assign'];
|
503 |
|
504 | const PLUGIN_BLACKLIST = PLUGIN_BLACKLIST_NAMES.map((pluginName) => {
|
505 | try {
|
506 | const blacklistedPluginObject = require(pluginName);
|
507 |
|
508 |
|
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 |
|
521 | function 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 |
|
555 | module.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 |
|
570 |
|
571 | manipulateOptions(opts, parserOpts) {
|
572 | const plugins = opts.plugins;
|
573 | removePluginsFromBlacklist(plugins);
|
574 | },
|
575 | };
|
576 | };
|