Skip to content

Commit 1eed1a9

Browse files
authored
feat: Add fallback detection methods for Worklet Classes (#6706)
## Summary Sometimes the class property ```javascript class Clazz { __workletClass = true; }; ``` Can get stripped away by some Babel plugins. We add some additional fallback methods so the users doesn't have to change his Babel pipeline to use this feature. ## Test plan 🚀
1 parent 0b2690a commit 1eed1a9

File tree

5 files changed

+72
-19
lines changed

5 files changed

+72
-19
lines changed

packages/react-native-reanimated/plugin/index.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-native-reanimated/plugin/src/class.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,7 @@ export function processIfWorkletClass(
4343
classPath: NodePath<ClassDeclaration>,
4444
state: ReanimatedPluginPass
4545
): boolean {
46-
if (!classPath.node.id) {
47-
// We don't support unnamed classes yet.
48-
return false;
49-
}
50-
51-
if (!hasWorkletClassMarker(classPath.node.body)) {
46+
if (!isWorkletizableClass(classPath, state)) {
5247
return false;
5348
}
5449

@@ -342,3 +337,42 @@ type Polyfill = {
342337
index: number;
343338
dependencies: Set<string>;
344339
};
340+
341+
function isWorkletizableClass(
342+
classPath: NodePath<ClassDeclaration>,
343+
state: ReanimatedPluginPass
344+
): boolean {
345+
const className = classPath.node.id?.name;
346+
const classNode = classPath.node;
347+
348+
// We don't support unnamed classes yet.
349+
if (!className) {
350+
return false;
351+
}
352+
353+
// Primary method of determining if a class is workletizable. However, some
354+
// Babel plugins might remove Class Properties.
355+
const isMarked = hasWorkletClassMarker(classNode.body);
356+
357+
// Secondary method of determining if a class is workletizable. We look for the
358+
// reference we memoized earlier. However, some plugin could've changed the reference.
359+
const isMemoizedNode = state.classesToWorkletize.some(
360+
(record) => record.node === classNode
361+
);
362+
363+
// Fallback for the name of the class.
364+
// We bail on non-top-level declarations.
365+
const isTopLevelMemoizedName =
366+
classPath.parentPath.isProgram() &&
367+
state.classesToWorkletize.some((record) => record.name === className);
368+
369+
// Remove the class from the list of classes to workletize. There are some edge
370+
// cases when leaving it as is would lead to multiple workletizations.
371+
state.classesToWorkletize = state.classesToWorkletize.filter(
372+
(record) => record.node !== classNode && record.name !== className
373+
);
374+
375+
const result = isMarked || isMemoizedNode || isTopLevelMemoizedName;
376+
377+
return result;
378+
}

packages/react-native-reanimated/plugin/src/file.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { contextObjectMarker } from './contextObject';
3636

3737
export function processIfWorkletFile(
3838
path: NodePath<Program>,
39-
_state: ReanimatedPluginPass
39+
state: ReanimatedPluginPass
4040
): boolean {
4141
if (
4242
!path.node.directives.some(
@@ -50,18 +50,21 @@ export function processIfWorkletFile(
5050
path.node.directives = path.node.directives.filter(
5151
(functionDirective) => functionDirective.value.value !== 'worklet'
5252
);
53-
processWorkletFile(path);
53+
processWorkletFile(path, state);
5454

5555
return true;
5656
}
5757

5858
/** Adds a worklet directive to each viable top-level entity in the file. */
59-
function processWorkletFile(programPath: NodePath<Program>) {
59+
function processWorkletFile(
60+
programPath: NodePath<Program>,
61+
state: ReanimatedPluginPass
62+
) {
6063
const statements = programPath.get('body');
6164
dehoistCommonJSExports(programPath.node);
6265
statements.forEach((statement) => {
6366
const candidatePath = getCandidate(statement);
64-
processWorkletizableEntity(candidatePath);
67+
processWorkletizableEntity(candidatePath, state);
6568
});
6669
}
6770

@@ -76,7 +79,10 @@ function getCandidate(statementPath: NodePath<Statement>) {
7679
}
7780
}
7881

79-
function processWorkletizableEntity(nodePath: NodePath<unknown>) {
82+
function processWorkletizableEntity(
83+
nodePath: NodePath<unknown>,
84+
state: ReanimatedPluginPass
85+
) {
8086
if (isWorkletizableFunctionPath(nodePath)) {
8187
if (nodePath.isArrowFunctionExpression()) {
8288
replaceImplicitReturnWithBlock(nodePath.node);
@@ -86,35 +92,46 @@ function processWorkletizableEntity(nodePath: NodePath<unknown>) {
8692
if (isImplicitContextObject(nodePath)) {
8793
appendWorkletContextObjectMarker(nodePath.node);
8894
} else {
89-
processWorkletAggregator(nodePath);
95+
processWorkletAggregator(nodePath, state);
9096
}
9197
} else if (nodePath.isVariableDeclaration()) {
92-
processVariableDeclaration(nodePath);
98+
processVariableDeclaration(nodePath, state);
9399
} else if (nodePath.isClassDeclaration()) {
94100
appendWorkletClassMarker(nodePath.node.body);
101+
if (nodePath.node.id?.name) {
102+
// We don't support unnamed classes yet.
103+
state.classesToWorkletize.push({
104+
node: nodePath.node,
105+
name: nodePath.node.id.name,
106+
});
107+
}
95108
}
96109
}
97110

98111
function processVariableDeclaration(
99-
variableDeclarationPath: NodePath<VariableDeclaration>
112+
variableDeclarationPath: NodePath<VariableDeclaration>,
113+
state: ReanimatedPluginPass
100114
) {
101115
const declarations = variableDeclarationPath.get('declarations');
102116
declarations.forEach((declaration) => {
103117
const initPath = declaration.get('init');
104118
if (initPath.isExpression()) {
105-
processWorkletizableEntity(initPath);
119+
processWorkletizableEntity(initPath, state);
106120
}
107121
});
108122
}
109123

110-
function processWorkletAggregator(objectPath: NodePath<ObjectExpression>) {
124+
function processWorkletAggregator(
125+
objectPath: NodePath<ObjectExpression>,
126+
state: ReanimatedPluginPass
127+
) {
111128
const properties = objectPath.get('properties');
112129
properties.forEach((property) => {
113130
if (property.isObjectMethod()) {
114131
appendWorkletDirective(property.node.body);
115132
} else if (property.isObjectProperty()) {
116133
const valuePath = property.get('value');
117-
processWorkletizableEntity(valuePath);
134+
processWorkletizableEntity(valuePath, state);
118135
}
119136
});
120137
}

packages/react-native-reanimated/plugin/src/globals.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ const notCapturedIdentifiers_DEPRECATED = [
156156

157157
export function initializeState(state: ReanimatedPluginPass) {
158158
state.workletNumber = 1;
159+
state.classesToWorkletize = [];
159160
initializeGlobals();
160161
addCustomGlobals(state);
161162
}

packages/react-native-reanimated/plugin/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface ReanimatedPluginPass {
3232
cwd: string;
3333
filename: string | undefined;
3434
workletNumber: number;
35+
classesToWorkletize: { node: BabelNode; name: string }[];
3536
}
3637

3738
export type WorkletizableFunction =

0 commit comments

Comments
 (0)