Skip to content

Commit 6d6aa87

Browse files
hannojgtjzel
andauthored
worklets plugin: preserve JSX imports in bundle mode (#9212)
## Summary This adds support for importing JSX statements/imports in worklets bundle mode! ## Test plan - Unit tests - example app babel plugin tests: <img width="1419" height="801" alt="Screenshot 2026-06-06 at 12 56 56" src="https://github.com/user-attachments/assets/69385021-22b4-47f5-85f5-8d2e7dd7f2ee" /> --------- Co-authored-by: Tomasz Żelawski <tzelawski@gmail.com>
1 parent 3206cb1 commit 6d6aa87

6 files changed

Lines changed: 128 additions & 4 deletions

File tree

apps/common-app/src/apps/reanimated/examples/RuntimeTests/RuntimeTestsExample.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export default function RuntimeTestsExample() {
187187
require('./tests/plugin/contextObjects.test');
188188
require('./tests/plugin/workletClasses.test');
189189
}
190+
require('./tests/plugin/jsxInWorklets.test');
190191
require('./tests/plugin/recursion.test');
191192
require('./tests/plugin/versionMismatch.test');
192193
},
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
3+
import {
4+
beforeEach,
5+
describe,
6+
expect,
7+
notify,
8+
test,
9+
waitForNotification,
10+
} from '../../ReJest/RuntimeTestsApi';
11+
import {
12+
isUIRuntime as ImportedComponent,
13+
scheduleOnRN,
14+
scheduleOnUI,
15+
} from 'react-native-worklets';
16+
17+
describe('Test JSX in worklets', () => {
18+
const PASS_NOTIFICATION = 'PASS';
19+
let result = false;
20+
21+
const callbackPass = (value: boolean) => {
22+
result = value;
23+
notify(PASS_NOTIFICATION);
24+
};
25+
26+
beforeEach(() => {
27+
result = false;
28+
});
29+
30+
if (globalThis._WORKLETS_BUNDLE_MODE_ENABLED) {
31+
test('worklets with JSX work on Worklet runtime', async () => {
32+
function renderView() {
33+
'worklet';
34+
return <ImportedComponent />;
35+
}
36+
37+
scheduleOnUI(() => {
38+
'worklet';
39+
const element = renderView() as React.ReactElement;
40+
scheduleOnRN(callbackPass, typeof element.type === 'function');
41+
});
42+
43+
await waitForNotification(PASS_NOTIFICATION);
44+
expect(result).toBe(true);
45+
});
46+
}
47+
});

packages/react-native-worklets/__tests__/plugin-bundleMode.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type { PluginOptions } from '../plugin';
2828
import plugin from '../plugin';
2929

3030
const MOCK_LOCATION = 'test.js';
31+
const MOCK_TSX_LOCATION = 'test.tsx';
3132
const MOCK_WORKLET_RUNTIME_ENTRY = 'react-native-worklets/src/index.ts';
3233
const MOCK_OTHER_FILE = 'someOtherFile.ts';
3334

@@ -200,6 +201,46 @@ describe('babel plugin in bundleMode', () => {
200201
expect(files[0].content).toMatchSnapshot();
201202
});
202203

204+
test('strips JSX dev attributes in written worklet files', () => {
205+
const input = html`<script>
206+
import { ImportedComponent } from 'react-native-worklets';
207+
208+
function renderView() {
209+
'worklet';
210+
return <ImportedComponent />;
211+
}
212+
</script>`;
213+
214+
const control = transformSync(input.replace(/<\/?script[^>]*>/g, ''), {
215+
filename: MOCK_TSX_LOCATION,
216+
compact: false,
217+
babelrc: false,
218+
configFile: false,
219+
presets: [
220+
['@babel/preset-react', { runtime: 'classic', development: true }],
221+
],
222+
envName: 'development',
223+
})!.code;
224+
expect(control).toContain('__self');
225+
expect(control).toContain('__source');
226+
227+
const { files } = runPlugin(
228+
input,
229+
{
230+
presets: [
231+
['@babel/preset-react', { runtime: 'classic', development: true }],
232+
],
233+
envName: 'development',
234+
},
235+
{ importForwarding: { moduleNames: ['react-native-worklets'] } },
236+
MOCK_TSX_LOCATION
237+
);
238+
expect(files).toHaveLength(1);
239+
expect(files[0].content).toContain('return <ImportedComponent />;');
240+
expect(files[0].content).not.toContain('__self');
241+
expect(files[0].content).not.toContain('__source');
242+
});
243+
203244
test('rebases relative imports against the worklets directory', () => {
204245
const input = html`<script>
205246
import { foo } from './bar';

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

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

packages/react-native-worklets/plugin/src/closure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function getClosure(
3232
typePath.skip();
3333
},
3434
ReferencedIdentifier(idPath) {
35-
if (idPath.isJSXIdentifier()) {
35+
if (idPath.isJSXIdentifier() && !state.opts.bundleMode) {
3636
return;
3737
}
3838

packages/react-native-worklets/plugin/src/generate.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
FunctionExpression,
55
ImportDeclaration,
66
ImportSpecifier,
7+
JSXAttribute,
78
} from '@babel/types';
89
import {
910
cloneNode,
@@ -72,7 +73,7 @@ export function generateWorkletFile(
7273
const transformedProg = transformFromAstSync(newProg, undefined, {
7374
filename: state.file.opts.filename,
7475
presets: ['@babel/preset-typescript'],
75-
plugins: [state.autoworkletizationPlugin],
76+
plugins: [state.autoworkletizationPlugin, stripJsxDevAttributesPlugin],
7677
ast: false,
7778
babelrc: false,
7879
configFile: false,
@@ -85,3 +86,22 @@ export function generateWorkletFile(
8586

8687
writeFileSync(dedicatedFilePath, transformedProg);
8788
}
89+
90+
const stripJsxDevAttributesPlugin = {
91+
name: 'worklets-strip-jsx-dev-attributes',
92+
visitor: {
93+
JSXAttribute(path: NodePath<JSXAttribute>) {
94+
const name = path.node.name;
95+
96+
if (name.type !== 'JSXIdentifier') {
97+
return;
98+
}
99+
100+
if (name.name !== '__self' && name.name !== '__source') {
101+
return;
102+
}
103+
104+
path.remove();
105+
},
106+
},
107+
};

0 commit comments

Comments
 (0)