vxrn 1.25.16 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -1
- package/dist/index.native.js +2 -0
- package/dist/index.native.js.map +1 -1
- package/dist/patches/builtInDepPatches.mjs +21 -0
- package/dist/patches/builtInDepPatches.mjs.map +1 -1
- package/dist/patches/builtInDepPatches.native.js +21 -0
- package/dist/patches/builtInDepPatches.native.js.map +1 -1
- package/dist/plugins/reactNativeDevServer.mjs +5 -0
- package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
- package/dist/plugins/reactNativeDevServer.native.js +5 -0
- package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
- package/dist/plugins/rnCodegenPlugin.mjs +16 -0
- package/dist/plugins/rnCodegenPlugin.mjs.map +1 -0
- package/dist/plugins/rnCodegenPlugin.native.js +16 -0
- package/dist/plugins/rnCodegenPlugin.native.js.map +1 -0
- package/dist/plugins/rnCodegenPlugin.test.mjs +239 -0
- package/dist/plugins/rnCodegenPlugin.test.mjs.map +1 -0
- package/dist/plugins/rnCodegenPlugin.test.native.js +242 -0
- package/dist/plugins/rnCodegenPlugin.test.native.js.map +1 -0
- package/dist/plugins/workletImportsPlugin.mjs +160 -0
- package/dist/plugins/workletImportsPlugin.mjs.map +1 -0
- package/dist/plugins/workletImportsPlugin.native.js +230 -0
- package/dist/plugins/workletImportsPlugin.native.js.map +1 -0
- package/dist/utils/createNativeDevEngine.mjs +198 -92
- package/dist/utils/createNativeDevEngine.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.native.js +252 -99
- package/dist/utils/createNativeDevEngine.native.js.map +1 -1
- package/dist/utils/createNativeDevEngine.test.mjs +775 -4
- package/dist/utils/createNativeDevEngine.test.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.test.native.js +836 -4
- package/dist/utils/createNativeDevEngine.test.native.js.map +1 -1
- package/dist/utils/patches.mjs +12 -5
- package/dist/utils/patches.mjs.map +1 -1
- package/dist/utils/patches.native.js +13 -5
- package/dist/utils/patches.native.js.map +1 -1
- package/dist/utils/patches.test.mjs +28 -0
- package/dist/utils/patches.test.mjs.map +1 -1
- package/dist/utils/patches.test.native.js +47 -0
- package/dist/utils/patches.test.native.js.map +1 -1
- package/dist/utils/scanDepsToOptimize.mjs +5 -1
- package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
- package/dist/utils/scanDepsToOptimize.native.js +5 -1
- package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
- package/package.json +19 -12
- package/src/index.ts +2 -0
- package/src/patches/builtInDepPatches.ts +25 -0
- package/src/plugins/reactNativeDevServer.ts +12 -0
- package/src/plugins/rnCodegenPlugin.test.ts +313 -0
- package/src/plugins/rnCodegenPlugin.ts +20 -0
- package/src/plugins/workletImportsPlugin.ts +226 -0
- package/src/utils/createNativeDevEngine.test.ts +877 -3
- package/src/utils/createNativeDevEngine.ts +259 -107
- package/src/utils/patches.test.ts +33 -0
- package/src/utils/patches.ts +17 -6
- package/src/utils/scanDepsToOptimize.ts +9 -0
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -1
- package/types/patches/builtInDepPatches.d.ts.map +1 -1
- package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
- package/types/plugins/rnCodegenPlugin.d.ts +6 -0
- package/types/plugins/rnCodegenPlugin.d.ts.map +1 -0
- package/types/plugins/rnCodegenPlugin.test.d.ts +2 -0
- package/types/plugins/rnCodegenPlugin.test.d.ts.map +1 -0
- package/types/plugins/workletImportsPlugin.d.ts +8 -0
- package/types/plugins/workletImportsPlugin.d.ts.map +1 -0
- package/types/utils/createNativeDevEngine.d.ts +13 -0
- package/types/utils/createNativeDevEngine.d.ts.map +1 -1
- package/types/utils/patches.d.ts.map +1 -1
- package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { rnCodegenPlugin, transformReactNativeCodegen } from './rnCodegenPlugin'
|
|
3
|
+
|
|
4
|
+
describe('rnCodegenPlugin', () => {
|
|
5
|
+
it('generates view-config replacement for codegenNativeComponent default export', () => {
|
|
6
|
+
const inputCode = `
|
|
7
|
+
import type { ViewProps, HostComponent } from 'react-native';
|
|
8
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
9
|
+
|
|
10
|
+
export interface NativeProps extends ViewProps {
|
|
11
|
+
color?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default codegenNativeComponent<NativeProps>('MyCustomView') as HostComponent<NativeProps>;
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
const result = transformReactNativeCodegen(
|
|
18
|
+
inputCode,
|
|
19
|
+
'/workspace/src/MyCustomViewNativeComponent.ts'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(result).toBeDefined()
|
|
23
|
+
expect(result?.code).not.toContain('codegenNativeComponent<NativeProps>')
|
|
24
|
+
expect(result?.code).toContain(
|
|
25
|
+
'export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);'
|
|
26
|
+
)
|
|
27
|
+
expect(result?.code).toContain('uiViewClassName: "MyCustomView"')
|
|
28
|
+
expect(result?.code).toContain('export const __INTERNAL_VIEW_CONFIG')
|
|
29
|
+
expect(result?.map).toBeDefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('leaves already-generated output alone on a second pass', () => {
|
|
33
|
+
// the transform runs more than once over the same module. its own output
|
|
34
|
+
// keeps the codegenNativeComponent import and turns Commands into a plain
|
|
35
|
+
// object, which the reserved-export check would otherwise reject.
|
|
36
|
+
const inputCode = `
|
|
37
|
+
import type { ViewProps, HostComponent } from 'react-native';
|
|
38
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
39
|
+
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
|
|
40
|
+
|
|
41
|
+
export interface NativeProps extends ViewProps {
|
|
42
|
+
color?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface NativeCommands {
|
|
46
|
+
reload: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const Commands = codegenNativeCommands<NativeCommands>({
|
|
50
|
+
supportedCommands: ['reload'],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export default codegenNativeComponent<NativeProps>('MyCustomView') as HostComponent<NativeProps>;
|
|
54
|
+
`
|
|
55
|
+
|
|
56
|
+
const id = '/workspace/src/MyCustomViewNativeComponent.ts'
|
|
57
|
+
const first = transformReactNativeCodegen(inputCode, id)
|
|
58
|
+
expect(first?.code).toContain('export const Commands')
|
|
59
|
+
|
|
60
|
+
expect(() => transformReactNativeCodegen(first!.code, id)).not.toThrow()
|
|
61
|
+
expect(transformReactNativeCodegen(first!.code, id)).toBeUndefined()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('removes export const Commands = codegenNativeCommands(...) and generates runtime commands', () => {
|
|
65
|
+
const inputCode = `
|
|
66
|
+
import * as React from 'react';
|
|
67
|
+
import type { ViewProps, HostComponent } from 'react-native';
|
|
68
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
69
|
+
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
|
|
70
|
+
|
|
71
|
+
export interface NativeProps extends ViewProps {
|
|
72
|
+
color?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type ComponentType = HostComponent<NativeProps>;
|
|
76
|
+
|
|
77
|
+
interface NativeCommands {
|
|
78
|
+
flash: (viewRef: React.ElementRef<ComponentType>, enabled: boolean) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
82
|
+
supportedCommands: ['flash'],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export default codegenNativeComponent<NativeProps>('MyCustomView') as HostComponent<NativeProps>;
|
|
86
|
+
`
|
|
87
|
+
|
|
88
|
+
const result = transformReactNativeCodegen(
|
|
89
|
+
inputCode,
|
|
90
|
+
'/workspace/src/MyCustomViewNativeComponent.ts'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
expect(result).toBeDefined()
|
|
94
|
+
// The original codegenNativeCommands export must be removed
|
|
95
|
+
expect(result?.code).not.toContain('codegenNativeCommands<NativeCommands>')
|
|
96
|
+
// The runtime Commands implementation must be generated
|
|
97
|
+
expect(result?.code).toContain('export const Commands = {')
|
|
98
|
+
expect(result?.code).toContain('dispatchCommand(ref, "flash", [enabled]);')
|
|
99
|
+
expect(result?.code).toContain(
|
|
100
|
+
'export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);'
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('throws an error if native commands are exported with a name other than Commands', () => {
|
|
105
|
+
const inputCode = `
|
|
106
|
+
import * as React from 'react';
|
|
107
|
+
import type { ViewProps, HostComponent } from 'react-native';
|
|
108
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
109
|
+
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
|
|
110
|
+
|
|
111
|
+
export interface NativeProps extends ViewProps {
|
|
112
|
+
color?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type ComponentType = HostComponent<NativeProps>;
|
|
116
|
+
|
|
117
|
+
interface NativeCommands {
|
|
118
|
+
flash: (viewRef: React.ElementRef<ComponentType>, enabled: boolean) => void;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const OtherCommands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
122
|
+
supportedCommands: ['flash'],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export default codegenNativeComponent<NativeProps>('MyCustomView') as HostComponent<NativeProps>;
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
expect(() => {
|
|
129
|
+
transformReactNativeCodegen(
|
|
130
|
+
inputCode,
|
|
131
|
+
'/workspace/src/MyCustomViewNativeComponent.ts'
|
|
132
|
+
)
|
|
133
|
+
}).toThrow("Native commands must be exported with the name 'Commands'")
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("throws an error if 'Commands' is exported without codegenNativeCommands", () => {
|
|
137
|
+
const inputCode = `
|
|
138
|
+
import type { ViewProps, HostComponent } from 'react-native';
|
|
139
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
140
|
+
|
|
141
|
+
export interface NativeProps extends ViewProps {
|
|
142
|
+
color?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const Commands = 123;
|
|
146
|
+
|
|
147
|
+
export default codegenNativeComponent<NativeProps>('MyCustomView') as HostComponent<NativeProps>;
|
|
148
|
+
`
|
|
149
|
+
|
|
150
|
+
expect(() => {
|
|
151
|
+
transformReactNativeCodegen(
|
|
152
|
+
inputCode,
|
|
153
|
+
'/workspace/src/MyCustomViewNativeComponent.ts'
|
|
154
|
+
)
|
|
155
|
+
}).toThrow(
|
|
156
|
+
"'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands."
|
|
157
|
+
)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("throws an error if 'Commands' is exported via export specifier without codegenNativeCommands", () => {
|
|
161
|
+
const inputCode = `
|
|
162
|
+
import type { ViewProps, HostComponent } from 'react-native';
|
|
163
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
164
|
+
|
|
165
|
+
export interface NativeProps extends ViewProps {
|
|
166
|
+
color?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const Commands = 123;
|
|
170
|
+
export { Commands };
|
|
171
|
+
|
|
172
|
+
export default codegenNativeComponent<NativeProps>('MyCustomView') as HostComponent<NativeProps>;
|
|
173
|
+
`
|
|
174
|
+
|
|
175
|
+
expect(() => {
|
|
176
|
+
transformReactNativeCodegen(
|
|
177
|
+
inputCode,
|
|
178
|
+
'/workspace/src/MyCustomViewNativeComponent.ts'
|
|
179
|
+
)
|
|
180
|
+
}).toThrow(
|
|
181
|
+
"'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands."
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('ignores files that do not match the codegen pattern', () => {
|
|
186
|
+
const inputCode = `
|
|
187
|
+
export default function Button() {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
`
|
|
191
|
+
const result = transformReactNativeCodegen(inputCode, '/workspace/src/Button.tsx')
|
|
192
|
+
expect(result).toBeUndefined()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('ignores matching files without codegenNativeComponent', () => {
|
|
196
|
+
const inputCode = `
|
|
197
|
+
export default function MyNativeComponent() {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
`
|
|
201
|
+
const result = transformReactNativeCodegen(
|
|
202
|
+
inputCode,
|
|
203
|
+
'/workspace/src/MyNativeComponent.tsx'
|
|
204
|
+
)
|
|
205
|
+
expect(result).toBeUndefined()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('supports files inside specs/ directory', () => {
|
|
209
|
+
const inputCode = `
|
|
210
|
+
import type { ViewProps, HostComponent } from 'react-native';
|
|
211
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
212
|
+
|
|
213
|
+
export interface NativeProps extends ViewProps {
|
|
214
|
+
color?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default codegenNativeComponent<NativeProps>('SpecView') as HostComponent<NativeProps>;
|
|
218
|
+
`
|
|
219
|
+
|
|
220
|
+
const result = transformReactNativeCodegen(inputCode, '/workspace/specs/SpecView.ts')
|
|
221
|
+
|
|
222
|
+
expect(result).toBeDefined()
|
|
223
|
+
expect(result?.code).toContain('uiViewClassName: "SpecView"')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('works through the Vite plugin interface', () => {
|
|
227
|
+
const plugin = rnCodegenPlugin()
|
|
228
|
+
expect(plugin.name).toBe('vxrn:rn-codegen')
|
|
229
|
+
expect(plugin.enforce).toBe('pre')
|
|
230
|
+
|
|
231
|
+
const inputCode = `
|
|
232
|
+
import type { ViewProps, HostComponent } from 'react-native';
|
|
233
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
234
|
+
|
|
235
|
+
export interface NativeProps extends ViewProps {
|
|
236
|
+
color?: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export default codegenNativeComponent<NativeProps>('PluginView') as HostComponent<NativeProps>;
|
|
240
|
+
`
|
|
241
|
+
|
|
242
|
+
const transformFn =
|
|
243
|
+
typeof plugin.transform === 'function'
|
|
244
|
+
? plugin.transform
|
|
245
|
+
: (plugin.transform as any)?.handler
|
|
246
|
+
|
|
247
|
+
const result = transformFn.call(
|
|
248
|
+
{} as any,
|
|
249
|
+
inputCode,
|
|
250
|
+
'/workspace/src/PluginViewNativeComponent.ts'
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
expect(result).toBeDefined()
|
|
254
|
+
expect(result?.code).toContain('uiViewClassName: "PluginView"')
|
|
255
|
+
expect(result?.map).toBeDefined()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('removes export const Commands with sequence expression (e.g. coverage instrumentation)', () => {
|
|
259
|
+
const inputCode = `
|
|
260
|
+
import type { ViewProps, HostComponent } from 'react-native';
|
|
261
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
262
|
+
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
|
|
263
|
+
|
|
264
|
+
export interface NativeProps extends ViewProps {
|
|
265
|
+
color?: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export const Commands = (0, codegenNativeCommands({
|
|
269
|
+
supportedCommands: [],
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
export default codegenNativeComponent<NativeProps>('SequenceView') as HostComponent<NativeProps>;
|
|
273
|
+
`
|
|
274
|
+
|
|
275
|
+
const result = transformReactNativeCodegen(
|
|
276
|
+
inputCode,
|
|
277
|
+
'/workspace/src/SequenceViewNativeComponent.ts'
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
expect(result).toBeDefined()
|
|
281
|
+
expect(result?.code).not.toContain('export const Commands')
|
|
282
|
+
expect(result?.code).toContain('uiViewClassName: "SequenceView"')
|
|
283
|
+
expect(result?.code).toContain('export default NativeComponentRegistry.get')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('transforms Flow component specs correctly', () => {
|
|
287
|
+
const flowCode = `/**
|
|
288
|
+
* @flow strict-local
|
|
289
|
+
* @format
|
|
290
|
+
*/
|
|
291
|
+
|
|
292
|
+
import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes';
|
|
293
|
+
import type {HostComponent} from 'react-native/Libraries/Renderer/shims/ReactNativeTypes';
|
|
294
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
295
|
+
|
|
296
|
+
type FlowViewProps = $ReadOnly<{
|
|
297
|
+
...ViewProps,
|
|
298
|
+
title?: string,
|
|
299
|
+
}>;
|
|
300
|
+
|
|
301
|
+
export default codegenNativeComponent<FlowViewProps>('FlowView') as HostComponent<FlowViewProps>;
|
|
302
|
+
`
|
|
303
|
+
|
|
304
|
+
const result = transformReactNativeCodegen(
|
|
305
|
+
flowCode,
|
|
306
|
+
'/workspace/src/FlowViewNativeComponent.js'
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
expect(result).toBeDefined()
|
|
310
|
+
expect(result?.code).toContain('uiViewClassName: "FlowView"')
|
|
311
|
+
expect(result?.code).toContain('export default NativeComponentRegistry.get')
|
|
312
|
+
})
|
|
313
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Plugin } from 'vite'
|
|
2
|
+
import { transformReactNativeCodegen } from '@vxrn/compiler'
|
|
3
|
+
|
|
4
|
+
// the transform itself lives in @vxrn/compiler so the metro transformer worker
|
|
5
|
+
// can run it too. vxrn depends on @vxrn/vite-plugin-metro, so the worker cannot
|
|
6
|
+
// reach back into this package.
|
|
7
|
+
export { transformReactNativeCodegen } from '@vxrn/compiler'
|
|
8
|
+
|
|
9
|
+
export function rnCodegenPlugin(options?: { projectRoot?: string }): Plugin {
|
|
10
|
+
return {
|
|
11
|
+
name: 'vxrn:rn-codegen',
|
|
12
|
+
enforce: 'pre',
|
|
13
|
+
transform: {
|
|
14
|
+
order: 'pre',
|
|
15
|
+
handler(code: string, id: string) {
|
|
16
|
+
return transformReactNativeCodegen(code, id, options?.projectRoot)
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} as Plugin
|
|
20
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { isBuiltin } from 'node:module'
|
|
3
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
4
|
+
import { statSync } from 'node:fs'
|
|
5
|
+
import { rolldown, type Plugin } from 'rolldown'
|
|
6
|
+
import { parseSync } from 'oxc-parser'
|
|
7
|
+
import { getClosureVariables, JS_GLOBALS, transformHermesLoops } from '@vxrn/compiler'
|
|
8
|
+
import { hermesCompatSWCPlugin, hermesLoopsPlugin } from '../utils/createNativeDevEngine'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* bundles selected, synchronous pure-function exports for normal worklets mode.
|
|
12
|
+
* install in native.bundlerOptions.plugins. the replacement module exposes only
|
|
13
|
+
* the configured exports; each entry owns its complete dependency graph.
|
|
14
|
+
*/
|
|
15
|
+
export function workletImportsPlugin(imports: Record<string, readonly string[]>): Plugin {
|
|
16
|
+
const selections = new Map(
|
|
17
|
+
Object.entries(imports).map(([source, names]) => [source, [...new Set(names)]])
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const rejectRuntimeDependency = (source: string) => {
|
|
21
|
+
const normalized = source.replace(/\\/g, '/')
|
|
22
|
+
if (
|
|
23
|
+
isBuiltin(source) ||
|
|
24
|
+
/(?:^|\/node_modules\/)(?:react|react-native|react-native-worklets|react-native-reanimated|expo-modules-core)(?:\/|$)/.test(
|
|
25
|
+
normalized
|
|
26
|
+
) ||
|
|
27
|
+
/(?:^|\/node_modules\/)@react-native\//.test(normalized)
|
|
28
|
+
) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`[worklet imports] runtime dependency ${source} is not pure JavaScript`
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
name: 'vxrn:worklet-imports',
|
|
37
|
+
options(nativeOptions) {
|
|
38
|
+
// one config can share this plugin across simultaneous ios/android builds.
|
|
39
|
+
const modules = new Map<string, { entry: string; names: string[]; key: string }>()
|
|
40
|
+
const instance: Plugin = {
|
|
41
|
+
name: 'vxrn:worklet-imports:bundle',
|
|
42
|
+
async resolveId(source, importer, options) {
|
|
43
|
+
const names = selections.get(source)
|
|
44
|
+
if (!names) return
|
|
45
|
+
if (!names.length)
|
|
46
|
+
throw new Error(`[worklet imports] no exports selected for ${source}`)
|
|
47
|
+
if (options.kind !== 'import-statement') {
|
|
48
|
+
throw new Error(`[worklet imports] ${source} requires a static ESM import`)
|
|
49
|
+
}
|
|
50
|
+
rejectRuntimeDependency(source)
|
|
51
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true })
|
|
52
|
+
if (
|
|
53
|
+
!resolved ||
|
|
54
|
+
resolved.external ||
|
|
55
|
+
!isAbsolute(resolved.id) ||
|
|
56
|
+
!statSync(resolved.id, { throwIfNoEntry: false })?.isFile()
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`[worklet imports] ${source} must resolve to a JavaScript file`
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
rejectRuntimeDependency(resolved.id)
|
|
63
|
+
const key = createHash('sha256')
|
|
64
|
+
.update(JSON.stringify([resolved.id, names]))
|
|
65
|
+
.digest('hex')
|
|
66
|
+
.slice(0, 16)
|
|
67
|
+
// a file-shaped id lets the existing native compiler process the directives.
|
|
68
|
+
const id = join(dirname(resolved.id), `__one_worklet_imports_${key}.js`)
|
|
69
|
+
modules.set(id, { entry: resolved.id, names, key })
|
|
70
|
+
return id
|
|
71
|
+
},
|
|
72
|
+
moduleParsed(info) {
|
|
73
|
+
if (
|
|
74
|
+
!info.code ||
|
|
75
|
+
![...selections.keys()].some((source) => info.code!.includes(source))
|
|
76
|
+
)
|
|
77
|
+
return
|
|
78
|
+
const { program, errors } = parseSync(info.id, info.code, {
|
|
79
|
+
lang: /\.[cm]?ts$/.test(info.id)
|
|
80
|
+
? 'ts'
|
|
81
|
+
: info.id.endsWith('.tsx')
|
|
82
|
+
? 'tsx'
|
|
83
|
+
: 'jsx',
|
|
84
|
+
})
|
|
85
|
+
if (errors.length) throw new Error(errors[0].message)
|
|
86
|
+
for (const node of program.body) {
|
|
87
|
+
if (
|
|
88
|
+
node.type !== 'ImportDeclaration' &&
|
|
89
|
+
node.type !== 'ExportNamedDeclaration'
|
|
90
|
+
)
|
|
91
|
+
continue
|
|
92
|
+
const source = node.source?.value
|
|
93
|
+
if (!source) continue
|
|
94
|
+
const names = selections.get(source)
|
|
95
|
+
if (!names) continue
|
|
96
|
+
for (const specifier of node.specifiers) {
|
|
97
|
+
const imported =
|
|
98
|
+
specifier.type === 'ImportDefaultSpecifier'
|
|
99
|
+
? 'default'
|
|
100
|
+
: specifier.type === 'ImportSpecifier'
|
|
101
|
+
? specifier.imported.type === 'Identifier'
|
|
102
|
+
? specifier.imported.name
|
|
103
|
+
: specifier.imported.value
|
|
104
|
+
: specifier.type === 'ExportSpecifier'
|
|
105
|
+
? specifier.local.type === 'Identifier'
|
|
106
|
+
? specifier.local.name
|
|
107
|
+
: specifier.local.value
|
|
108
|
+
: undefined
|
|
109
|
+
// native's shimMissingExports would otherwise silently emit undefined.
|
|
110
|
+
if (imported !== undefined && !names.includes(imported)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`[worklet imports] ${info.id} imports unselected export ${imported} from ${source}`
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
async load(id) {
|
|
119
|
+
const selected = modules.get(id)
|
|
120
|
+
if (!selected) return
|
|
121
|
+
const outerContext = this
|
|
122
|
+
const innerEntry = '\0one-worklet-import-entry'
|
|
123
|
+
const bundle = await rolldown({
|
|
124
|
+
cwd: nativeOptions.cwd,
|
|
125
|
+
input: innerEntry,
|
|
126
|
+
platform: 'neutral',
|
|
127
|
+
resolve: nativeOptions.resolve,
|
|
128
|
+
transform: nativeOptions.transform,
|
|
129
|
+
shimMissingExports: false,
|
|
130
|
+
plugins: [
|
|
131
|
+
{
|
|
132
|
+
name: 'vxrn:pure-worklet-module',
|
|
133
|
+
resolveId(source, _importer, options) {
|
|
134
|
+
if (source === innerEntry) return source
|
|
135
|
+
if (options.kind === 'dynamic-import') {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`[worklet imports] dynamic imports are unsupported in ${selected.entry}`
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
rejectRuntimeDependency(source)
|
|
141
|
+
},
|
|
142
|
+
load(source) {
|
|
143
|
+
if (source === innerEntry) {
|
|
144
|
+
return `export { ${selected.names.map((name, index) => `${JSON.stringify(name)} as __export${index}`).join(', ')} } from ${JSON.stringify(selected.entry)};`
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
moduleParsed(info) {
|
|
148
|
+
rejectRuntimeDependency(info.id)
|
|
149
|
+
if (isAbsolute(info.id)) outerContext.addWatchFile(info.id)
|
|
150
|
+
if (info.packageJsonPath)
|
|
151
|
+
outerContext.addWatchFile(info.packageJsonPath)
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
hermesCompatSWCPlugin(false),
|
|
155
|
+
hermesLoopsPlugin(),
|
|
156
|
+
],
|
|
157
|
+
})
|
|
158
|
+
let code: string
|
|
159
|
+
try {
|
|
160
|
+
const output = await bundle.generate({
|
|
161
|
+
format: 'iife',
|
|
162
|
+
name: '__oneWorkletBundle',
|
|
163
|
+
})
|
|
164
|
+
const chunk = output.output[0]
|
|
165
|
+
if (
|
|
166
|
+
output.output.length !== 1 ||
|
|
167
|
+
chunk.type !== 'chunk' ||
|
|
168
|
+
chunk.imports.length ||
|
|
169
|
+
chunk.dynamicImports.length
|
|
170
|
+
) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`[worklet imports] ${selected.entry} must produce one self-contained JavaScript module`
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
// also cover closure-bearing loops in helpers emitted by the bundler.
|
|
176
|
+
code = transformHermesLoops(chunk.code, id)?.code ?? chunk.code
|
|
177
|
+
for (const file of await bundle.watchFiles) {
|
|
178
|
+
if (isAbsolute(file)) outerContext.addWatchFile(file)
|
|
179
|
+
}
|
|
180
|
+
} finally {
|
|
181
|
+
await bundle.close()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const loader = `__oneLoadWorkletModule_${selected.key}`
|
|
185
|
+
const loaderCode = `function ${loader}() {
|
|
186
|
+
'worklet';
|
|
187
|
+
const cache = globalThis.__oneWorkletImportCache || (globalThis.__oneWorkletImportCache = new WeakMap());
|
|
188
|
+
let namespace = cache.get(${loader});
|
|
189
|
+
if (namespace === undefined) {
|
|
190
|
+
${code}
|
|
191
|
+
namespace = __oneWorkletBundle;
|
|
192
|
+
cache.set(${loader}, namespace);
|
|
193
|
+
}
|
|
194
|
+
return namespace;
|
|
195
|
+
}`
|
|
196
|
+
const parsed = parseSync(id, loaderCode, { lang: 'js' })
|
|
197
|
+
if (parsed.errors.length) throw new Error(parsed.errors[0].message)
|
|
198
|
+
const captures = getClosureVariables(
|
|
199
|
+
parsed.program.body[0],
|
|
200
|
+
new Set([...JS_GLOBALS, 'arguments'])
|
|
201
|
+
)
|
|
202
|
+
if (captures.length) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`[worklet imports] ${selected.entry} requires unsupported runtime globals: ${captures.join(', ')}`
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
return `${loaderCode}\n${selected.names
|
|
208
|
+
.map(
|
|
209
|
+
(name, index) => `
|
|
210
|
+
function __oneExport${index}(...args) {
|
|
211
|
+
'worklet';
|
|
212
|
+
return (0, ${loader}().__export${index})(...args);
|
|
213
|
+
}
|
|
214
|
+
export { __oneExport${index} as ${JSON.stringify(name)} };
|
|
215
|
+
`
|
|
216
|
+
)
|
|
217
|
+
.join('\n')}`
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
...nativeOptions,
|
|
222
|
+
plugins: [instance, nativeOptions.plugins],
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
}
|