Skip to content

Commit 7927605

Browse files
authored
fix: Allow TypeScript in worklet classes (#6667)
## Summary Turns out `@babel/preset-typescript` always has to be included when calling `transformSync` from `babel` (it cannot be included in a later call). Due to that I made all `transformSync` calls into a wrapper call that uses a required set of plugins and presets. Fixes - #6642 ## Test plan - [x] Plugin unit tests pass - [x] Runtime tests pass
1 parent bdfb866 commit 7927605

File tree

9 files changed

+115
-43
lines changed

9 files changed

+115
-43
lines changed

apps/common-app/src/examples/RuntimeTests/tests/plugin/fileWorkletization.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@ export const implicitContextObject = {
2323
},
2424
};
2525

26-
export class ImplicitWorkletClass {
27-
getSix() {
26+
interface IWorkletClass {
27+
getSix(): number;
28+
getSeven(): number;
29+
}
30+
31+
export class ImplicitWorkletClass implements IWorkletClass {
32+
getSix(): number {
2833
return 6;
2934
}
3035

31-
getSeven() {
36+
getSeven(): number {
3237
return this.getSix() + 1;
3338
}
3439
}

apps/common-app/src/examples/RuntimeTests/tests/plugin/recursion.test.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect } from 'react';
22
import { View } from 'react-native';
3-
import { useSharedValue, runOnUI, runOnJS } from 'react-native-reanimated';
3+
import { useSharedValue, runOnUI } from 'react-native-reanimated';
44
import { render, wait, describe, getRegisteredValue, registerValue, test, expect } from '../../ReJest/RuntimeTestsApi';
55

66
const SHARED_VALUE_REF = 'SHARED_VALUE_REF';
@@ -85,15 +85,13 @@ describe('Test recursion in worklets', () => {
8585
const output = useSharedValue<number | null>(null);
8686
registerValue(SHARED_VALUE_REF, output);
8787
function recursiveWorklet(a: number) {
88-
if (a === 2) {
88+
if (a === 1) {
8989
output.value = a;
90-
} else if (a === 1) {
91-
try {
92-
// TODO: Such case isn't supported at the moment -
93-
// a function can't be a Worklet and a Remote function at the same time.
94-
// Consider supporting it in the future.
95-
runOnJS(recursiveWorklet)(a + 1);
96-
} catch {}
90+
} else if (a === 2) {
91+
// TODO: Such case isn't supported at the moment -
92+
// a function can't be a Worklet and a Remote function at the same time.
93+
// Consider supporting it in the future.
94+
// runOnJS(recursiveWorklet)(a + 1);
9795
} else {
9896
recursiveWorklet(a + 1);
9997
}
@@ -108,6 +106,6 @@ describe('Test recursion in worklets', () => {
108106
await render(<ExampleComponent />);
109107
await wait(100);
110108
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
111-
expect(sharedValue.onJS).toBe(null);
109+
expect(sharedValue.onJS).toBe(1);
112110
});
113111
});

apps/common-app/src/examples/RuntimeTests/tests/plugin/workletClasses.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,28 @@ class WorkletClass {
2121
}
2222
}
2323

24+
interface ITypeScriptClass {
25+
getOne(): number;
26+
getTwo(): number;
27+
getIncremented(): number;
28+
}
29+
30+
class TypeScriptClass implements ITypeScriptClass {
31+
__workletClass: boolean = true;
32+
value: number = 0;
33+
getOne(): number {
34+
return 1;
35+
}
36+
37+
getTwo(): number {
38+
return this.getOne() + 1;
39+
}
40+
41+
getIncremented(): number {
42+
return ++this.value;
43+
}
44+
}
45+
2446
describe('Test worklet classes', () => {
2547
test('class works on React runtime', async () => {
2648
const ExampleComponent = () => {
@@ -134,5 +156,26 @@ describe('Test worklet classes', () => {
134156
expect(sharedValue.onUI).toBe(true);
135157
});
136158

159+
test('TypeScript classes work on Worklet runtime', async () => {
160+
const ExampleComponent = () => {
161+
const output = useSharedValue<number | null>(null);
162+
registerValue(SHARED_VALUE_REF, output);
163+
164+
useEffect(() => {
165+
runOnUI(() => {
166+
const clazz = new TypeScriptClass();
167+
output.value = clazz.getOne();
168+
})();
169+
});
170+
171+
return <View />;
172+
};
173+
await render(<ExampleComponent />);
174+
await wait(100);
175+
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
176+
expect(sharedValue.onUI).toBe(1);
177+
});
178+
137179
// TODO: Add a test that throws when class is sent from React to Worklet runtime.
180+
// TODO: Add a test that throws when trying to use Worklet Class with inheritance.
138181
});

packages/docs-reanimated/docs/reanimated-babel-plugin/about.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ runOnUI(() => new Clazz().foo())(); // Logs 'Hello from WorkletClass'
146146

147147
**Pitfalls:**
148148

149+
- Worklet Classes don't support inheritance.
149150
- Worklet Classes don't support static methods and properties.
150151
- Class instances cannot be shared between JS and UI threads.
151152

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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { NodePath } from '@babel/core';
2-
import { transformSync } from '@babel/core';
32
import generate from '@babel/generator';
43
import traverse from '@babel/traverse';
54
import type {
@@ -36,6 +35,7 @@ import { strict as assert } from 'assert';
3635
import type { ReanimatedPluginPass } from './types';
3736
import { workletClassFactorySuffix } from './types';
3837
import { replaceWithFactoryCall } from './utils';
38+
import { workletTransformSync } from './transform';
3939

4040
const classWorkletMarker = '__workletClass';
4141

@@ -96,8 +96,8 @@ function getPolyfilledAst(
9696
) {
9797
const classCode = generate(classNode).code;
9898

99-
const classWithPolyfills = transformSync(classCode, {
100-
plugins: [
99+
const classWithPolyfills = workletTransformSync(classCode, {
100+
extraPlugins: [
101101
'@babel/plugin-transform-class-properties',
102102
'@babel/plugin-transform-classes',
103103
'@babel/plugin-transform-unicode-regex',
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { transformSync } from '@babel/core';
2+
import type { PluginItem, TransformOptions } from '@babel/core';
3+
4+
export function workletTransformSync(
5+
code: string,
6+
opts: WorkletTransformOptions
7+
) {
8+
const { extraPlugins = [], extraPresets = [], ...rest } = opts;
9+
10+
return transformSync(code, {
11+
...rest,
12+
plugins: [...defaultPlugins, ...extraPlugins],
13+
presets: [...defaultPresets, ...extraPresets],
14+
});
15+
}
16+
17+
const defaultPresets: PluginItem[] = [
18+
require.resolve('@babel/preset-typescript'),
19+
];
20+
21+
const defaultPlugins: PluginItem[] = [];
22+
23+
interface WorkletTransformOptions
24+
extends Omit<TransformOptions, 'plugins' | 'presets'> {
25+
extraPlugins?: PluginItem[];
26+
extraPresets?: PluginItem[];
27+
filename: TransformOptions['filename'];
28+
}

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

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
22
import type { NodePath } from '@babel/core';
3-
import { transformSync, traverse } from '@babel/core';
3+
import { traverse } from '@babel/core';
44
import generate from '@babel/generator';
55
import type {
66
File as BabelFile,
@@ -44,25 +44,11 @@ import type { ReanimatedPluginPass, WorkletizableFunction } from './types';
4444
import { workletClassFactorySuffix } from './types';
4545
import { isRelease } from './utils';
4646
import { buildWorkletString } from './workletStringCode';
47+
import { workletTransformSync } from './transform';
4748

4849
const REAL_VERSION = require('../../package.json').version;
4950
const MOCK_VERSION = 'x.y.z';
5051

51-
const workletStringTransformPresets = [
52-
require.resolve('@babel/preset-typescript'),
53-
];
54-
55-
const workletStringTransformPlugins = [
56-
require.resolve('@babel/plugin-transform-shorthand-properties'),
57-
require.resolve('@babel/plugin-transform-arrow-functions'),
58-
require.resolve('@babel/plugin-transform-optional-chaining'),
59-
require.resolve('@babel/plugin-transform-nullish-coalescing-operator'),
60-
[
61-
require.resolve('@babel/plugin-transform-template-literals'),
62-
{ loose: true },
63-
],
64-
];
65-
6652
export function makeWorkletFactory(
6753
fun: NodePath<WorkletizableFunction>,
6854
state: ReanimatedPluginPass
@@ -91,10 +77,9 @@ export function makeWorkletFactory(
9177
codeObject.code =
9278
'(' + (fun.isObjectMethod() ? 'function ' : '') + codeObject.code + '\n)';
9379

94-
const transformed = transformSync(codeObject.code, {
80+
const transformed = workletTransformSync(codeObject.code, {
81+
extraPlugins,
9582
filename: state.file.opts.filename,
96-
presets: workletStringTransformPresets,
97-
plugins: workletStringTransformPlugins,
9883
ast: true,
9984
babelrc: false,
10085
configFile: false,
@@ -469,3 +454,14 @@ function makeArrayFromCapturedBindings(
469454

470455
return Array.from(closure.values());
471456
}
457+
458+
const extraPlugins = [
459+
require.resolve('@babel/plugin-transform-shorthand-properties'),
460+
require.resolve('@babel/plugin-transform-arrow-functions'),
461+
require.resolve('@babel/plugin-transform-optional-chaining'),
462+
require.resolve('@babel/plugin-transform-nullish-coalescing-operator'),
463+
[
464+
require.resolve('@babel/plugin-transform-template-literals'),
465+
{ loose: true },
466+
],
467+
];

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { BabelFileResult, NodePath, PluginItem } from '@babel/core';
2-
import { transformSync, traverse } from '@babel/core';
2+
import { traverse } from '@babel/core';
33
import generate from '@babel/generator';
44
import type {
55
File as BabelFile,
@@ -34,6 +34,7 @@ import * as fs from 'fs';
3434
import type { ReanimatedPluginPass, WorkletizableFunction } from './types';
3535
import { workletClassFactorySuffix } from './types';
3636
import { isRelease } from './utils';
37+
import { workletTransformSync } from './transform';
3738

3839
const MOCK_SOURCE_MAP = 'mock source map';
3940

@@ -130,8 +131,9 @@ export function buildWorkletString(
130131
}
131132
}
132133

133-
const transformed = transformSync(code, {
134-
plugins: [prependClosureVariablesIfNecessary(closureVariables)],
134+
const transformed = workletTransformSync(code, {
135+
filename: state.file.opts.filename,
136+
extraPlugins: [getClosurePlugin(closureVariables)],
135137
compact: true,
136138
sourceMaps: includeSourceMap,
137139
inputSourceMap: inputMap,
@@ -222,9 +224,8 @@ function prependRecursiveDeclaration(path: NodePath<WorkletizableFunction>) {
222224
}
223225
}
224226

225-
function prependClosureVariablesIfNecessary(
226-
closureVariables: Array<Identifier>
227-
): PluginItem {
227+
/** Prepends necessary closure variables to the worklet function. */
228+
function getClosurePlugin(closureVariables: Array<Identifier>): PluginItem {
228229
const closureDeclaration = variableDeclaration('const', [
229230
variableDeclarator(
230231
objectPattern(

0 commit comments

Comments
 (0)