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
|
@@ -2,14 +2,16 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
|
-
import { runInNewContext } from 'node:vm'
|
|
5
|
+
import { createContext, runInContext, runInNewContext } from 'node:vm'
|
|
6
6
|
import { rolldown, type RolldownOutput } from 'rolldown'
|
|
7
7
|
import { dev } from 'rolldown/experimental'
|
|
8
|
-
import { describe, expect, it } from 'vitest'
|
|
8
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
9
9
|
import { getNativePrelude } from '../runtime/native-prelude'
|
|
10
|
+
import { workletImportsPlugin } from '../plugins/workletImportsPlugin'
|
|
10
11
|
import {
|
|
11
12
|
buildNativeBundle,
|
|
12
13
|
createNativeDevAssetRegistry,
|
|
14
|
+
createNativeDevEngine,
|
|
13
15
|
getNativeAssetData,
|
|
14
16
|
getHermesSWCIncludes,
|
|
15
17
|
getHmrRuntimeSource,
|
|
@@ -29,13 +31,225 @@ export const transformProbe = () => {
|
|
|
29
31
|
}
|
|
30
32
|
`
|
|
31
33
|
|
|
34
|
+
// node guards lowering semantics; the async-default regression itself needs
|
|
35
|
+
// hermes, where the original arrow resolves its await to undefined.
|
|
36
|
+
describe.each(['rolldown-dev', 'rolldown-build', 'metro-module', 'metro-script'])(
|
|
37
|
+
'native async through %s',
|
|
38
|
+
(pipeline) => {
|
|
39
|
+
it('preserves defaults, lexical bindings, bigint, rejection, and iterator cleanup', async () => {
|
|
40
|
+
const id = '/project/native-async.js'
|
|
41
|
+
const source = `
|
|
42
|
+
globalThis.result = (function(parentArgument) {
|
|
43
|
+
const events = [];
|
|
44
|
+
const run = async(value = 2n, fail = false) => {
|
|
45
|
+
try {
|
|
46
|
+
const awaited = await (fail ? Promise.reject(new Error('rejected')) : Promise.resolve(3n));
|
|
47
|
+
events.push('after-await');
|
|
48
|
+
return [String(value + awaited), this.base, arguments[0]];
|
|
49
|
+
} catch (error) { return error.message; }
|
|
50
|
+
finally { events.push('finally'); }
|
|
51
|
+
};
|
|
52
|
+
const badDefault = async(value = (() => { throw new Error('default'); })()) => value;
|
|
53
|
+
let synchronousThrow = false;
|
|
54
|
+
let rejectedDefault;
|
|
55
|
+
try { rejectedDefault = badDefault().catch(error => error.message); }
|
|
56
|
+
catch { synchronousThrow = true; }
|
|
57
|
+
async function* values() {
|
|
58
|
+
try { yield 1n; yield 2n; }
|
|
59
|
+
finally { events.push('closed'); }
|
|
60
|
+
}
|
|
61
|
+
const first = async() => { for await (const value of values()) return String(value); };
|
|
62
|
+
return Promise.all([run(), run(4n), run(undefined, true), rejectedDefault, first()])
|
|
63
|
+
.then(values => ({ values, events, synchronousThrow }));
|
|
64
|
+
}).call({ base: 7 }, 'lexical');
|
|
65
|
+
`
|
|
66
|
+
let output: any
|
|
67
|
+
if (pipeline.startsWith('metro')) {
|
|
68
|
+
const { transform } = await import('@vxrn/vite-plugin-metro/metroNativeWorker')
|
|
69
|
+
const result = await transform({}, '/project', id, Buffer.from(source), {
|
|
70
|
+
dev: false,
|
|
71
|
+
platform: 'ios',
|
|
72
|
+
type: pipeline === 'metro-script' ? 'script' : 'module',
|
|
73
|
+
})
|
|
74
|
+
const data = result.output[0].data
|
|
75
|
+
const { fromRawMappings } = await import('metro-source-map')
|
|
76
|
+
output = {
|
|
77
|
+
code: data.code,
|
|
78
|
+
map: (fromRawMappings as any)([
|
|
79
|
+
{ code: data.code, path: id, source, map: data.map },
|
|
80
|
+
]).toMap(),
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
const plugin = hermesCompatSWCPlugin(pipeline === 'rolldown-dev', true)
|
|
84
|
+
output = await Reflect.apply(plugin.transform as Function, undefined, [
|
|
85
|
+
source,
|
|
86
|
+
id,
|
|
87
|
+
])
|
|
88
|
+
}
|
|
89
|
+
const context: any = {}
|
|
90
|
+
context.__d = (factory: any) => {
|
|
91
|
+
const module = { exports: {} }
|
|
92
|
+
factory(
|
|
93
|
+
context,
|
|
94
|
+
(name: string) => {
|
|
95
|
+
throw new Error(`unexpected dependency ${name}`)
|
|
96
|
+
},
|
|
97
|
+
() => {},
|
|
98
|
+
() => {},
|
|
99
|
+
module,
|
|
100
|
+
module.exports,
|
|
101
|
+
[]
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
runInNewContext(output.code, context)
|
|
105
|
+
const result = await context.result
|
|
106
|
+
expect(result.values).toEqual([
|
|
107
|
+
['5', 7, 'lexical'],
|
|
108
|
+
['7', 7, 'lexical'],
|
|
109
|
+
'rejected',
|
|
110
|
+
'default',
|
|
111
|
+
'1',
|
|
112
|
+
])
|
|
113
|
+
expect(result.synchronousThrow).toBe(false)
|
|
114
|
+
expect(result.events.filter((value: string) => value === 'finally')).toHaveLength(3)
|
|
115
|
+
expect(result.events.filter((value: string) => value === 'closed')).toHaveLength(1)
|
|
116
|
+
const { originalPositionFor, TraceMap } = await import('@jridgewell/trace-mapping')
|
|
117
|
+
const lines = output.code.split('\n')
|
|
118
|
+
const line = lines.findIndex((value: string) => value.includes('after-await'))
|
|
119
|
+
const position = originalPositionFor(new TraceMap(output.map), {
|
|
120
|
+
line: line + 1,
|
|
121
|
+
column: lines[line].indexOf('events.push'),
|
|
122
|
+
})
|
|
123
|
+
expect(position.source).toBe(id)
|
|
124
|
+
expect(position.line).toBe(
|
|
125
|
+
source.split('\n').findIndex((value) => value.includes('after-await')) + 1
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
describe.each(['shared', 'rolldown', 'metro'])('React Compiler worklets through %s', (pipeline) => {
|
|
132
|
+
it.each([
|
|
133
|
+
['block', 'useDerivedValue(() => { return value + 8 }, [value])', 18],
|
|
134
|
+
['expression', 'useDerivedValue(() => value + 8, [value])', 18],
|
|
135
|
+
['object', 'useDerivedValue(() => ({ lift: value + 8 }), [value])', { lift: 18 }],
|
|
136
|
+
['nested', 'useDerivedValue(() => runOnUI(() => value + 8), [value])', 18],
|
|
137
|
+
['gesture', 'useDerivedValue(() => Gesture.Pan().onUpdate(() => value + 8), [value])', 18],
|
|
138
|
+
])('preserves %s callbacks on the UI runtime and memoizes stable inputs', async (_name, callback, expected) => {
|
|
139
|
+
const compiler = await import('@vxrn/compiler')
|
|
140
|
+
const { parseSync } = await import('oxc-parser')
|
|
141
|
+
const { default: MagicString } = await import('magic-string')
|
|
142
|
+
const { TraceMap, originalPositionFor } = await import('@jridgewell/trace-mapping')
|
|
143
|
+
const projectRoot = await createWorkletsProject(false)
|
|
144
|
+
const sourceMaps = process.env.VXRN_ENABLE_SOURCE_MAP
|
|
145
|
+
process.env.VXRN_ENABLE_SOURCE_MAP = '1'
|
|
146
|
+
compiler.configureVXRNCompilerPlugin({
|
|
147
|
+
enableCompiler: true,
|
|
148
|
+
enableReanimated: true,
|
|
149
|
+
enableNativeWorklets: true,
|
|
150
|
+
})
|
|
151
|
+
try {
|
|
152
|
+
const id = join(projectRoot, 'useProbe.ts')
|
|
153
|
+
const source = `export function useProbe(value) {\n return ${callback};\n}\n`
|
|
154
|
+
await writeFile(id, source)
|
|
155
|
+
let result: any
|
|
156
|
+
if (pipeline === 'shared') {
|
|
157
|
+
const plugins = await compiler.createVXRNCompilerPlugin()
|
|
158
|
+
const plugin = plugins.find((p: any) => p.name === 'one:compiler') as any
|
|
159
|
+
await plugin.configResolved({ root: projectRoot, build: {} })
|
|
160
|
+
const hook = plugin.transform.handler || plugin.transform
|
|
161
|
+
result = await hook.call({ environment: { name: 'ios' } }, source, id)
|
|
162
|
+
} else if (pipeline === 'metro') {
|
|
163
|
+
const { transform } = await import('@vxrn/vite-plugin-metro/metroNativeWorker')
|
|
164
|
+
const output = await transform({}, projectRoot, id, Buffer.from(source), {
|
|
165
|
+
dev: false,
|
|
166
|
+
platform: 'ios',
|
|
167
|
+
type: 'module',
|
|
168
|
+
customTransformOptions: { reactCompiler: true, worklets: true },
|
|
169
|
+
})
|
|
170
|
+
const data = output.output[0].data
|
|
171
|
+
const msm = await import('metro-source-map')
|
|
172
|
+
result = {
|
|
173
|
+
code: data.code,
|
|
174
|
+
map: (msm.fromRawMappings as any)([{ code: data.code, path: id, source, map: data.map }]).toMap(),
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
const plugin = vxrnCompilerPlugin('ios', false, projectRoot, true)
|
|
178
|
+
result = await Reflect.apply(plugin.transform as Function, undefined, [source, id])
|
|
179
|
+
}
|
|
180
|
+
const runnable = new MagicString(result.code)
|
|
181
|
+
for (const node of parseSync('probe.js', result.code).program.body) {
|
|
182
|
+
if (node.type === 'ImportDeclaration') runnable.remove(node.start, node.end)
|
|
183
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
184
|
+
runnable.remove(node.start, node.declaration.start)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let cache: any[] | undefined
|
|
188
|
+
let cacheCalls = 0
|
|
189
|
+
const memo = (size: number) => {
|
|
190
|
+
cacheCalls++
|
|
191
|
+
return cache ??= Array(size).fill(Symbol.for('react.memo_cache_sentinel'))
|
|
192
|
+
}
|
|
193
|
+
const module = { exports: {} as any }
|
|
194
|
+
const probe = new Function('_c', 'useDerivedValue', 'runOnUI', 'Gesture', '__d', 'module',
|
|
195
|
+
`${runnable}\nreturn ${pipeline === 'metro' ? 'module.exports.useProbe' : 'useProbe'}`
|
|
196
|
+
)(
|
|
197
|
+
memo,
|
|
198
|
+
(fn: any) => fn,
|
|
199
|
+
(fn: any) => fn,
|
|
200
|
+
{ Pan: () => ({ onUpdate: (fn: any) => fn }) },
|
|
201
|
+
(factory: any) => factory(globalThis, () => ({ c: memo }), () => {}, () => {}, module, module.exports, []),
|
|
202
|
+
module
|
|
203
|
+
)
|
|
204
|
+
const first = probe(10)
|
|
205
|
+
expect(first.__initData?.code).toBeTypeOf('string')
|
|
206
|
+
for (const onUI of [false, true]) {
|
|
207
|
+
let fn = first
|
|
208
|
+
let value: any
|
|
209
|
+
for (let depth = 0; depth < 2; depth++) {
|
|
210
|
+
expect(fn.__initData?.code).toBeTypeOf('string')
|
|
211
|
+
value = onUI
|
|
212
|
+
? new Function('runOnUI', `return (${fn.__initData.code})`)((fn: any) => fn)
|
|
213
|
+
.call({ __closure: fn.__closure })
|
|
214
|
+
: fn()
|
|
215
|
+
if (typeof value !== 'function') break
|
|
216
|
+
fn = value
|
|
217
|
+
}
|
|
218
|
+
expect(value).toEqual(expected)
|
|
219
|
+
}
|
|
220
|
+
expect(probe(10)).toBe(first)
|
|
221
|
+
expect(cacheCalls).toBe(2)
|
|
222
|
+
expect(probe(20)).not.toBe(first)
|
|
223
|
+
|
|
224
|
+
const lines = result.code.split('\n')
|
|
225
|
+
const line = lines.findIndex((text: string) => text.includes('function useProbe('))
|
|
226
|
+
expect(line).toBeGreaterThanOrEqual(0)
|
|
227
|
+
const position = originalPositionFor(new TraceMap(result.map), {
|
|
228
|
+
line: line + 1,
|
|
229
|
+
column: lines[line].indexOf('function'),
|
|
230
|
+
})
|
|
231
|
+
expect(position.source).toBe(id)
|
|
232
|
+
expect(position.line).toBe(1)
|
|
233
|
+
} finally {
|
|
234
|
+
if (sourceMaps === undefined) delete process.env.VXRN_ENABLE_SOURCE_MAP
|
|
235
|
+
else process.env.VXRN_ENABLE_SOURCE_MAP = sourceMaps
|
|
236
|
+
compiler.configureVXRNCompilerPlugin({
|
|
237
|
+
enableCompiler: false,
|
|
238
|
+
enableReanimated: false,
|
|
239
|
+
enableNativeWorklets: false,
|
|
240
|
+
})
|
|
241
|
+
await rm(projectRoot, { recursive: true, force: true })
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
32
246
|
async function createWorkletsProject(throwOnTransform = true) {
|
|
33
247
|
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-transform-failure-'))
|
|
34
248
|
const packageRoot = join(testRoot, 'node_modules/react-native-worklets')
|
|
35
249
|
await mkdir(packageRoot, { recursive: true })
|
|
36
250
|
await writeFile(
|
|
37
251
|
join(packageRoot, 'package.json'),
|
|
38
|
-
JSON.stringify({ name: 'react-native-worklets' })
|
|
252
|
+
JSON.stringify({ name: 'react-native-worklets', version: '0.10.1' })
|
|
39
253
|
)
|
|
40
254
|
await writeFile(
|
|
41
255
|
join(packageRoot, 'plugin.js'),
|
|
@@ -76,7 +290,283 @@ describe('native prelude', () => {
|
|
|
76
290
|
})
|
|
77
291
|
})
|
|
78
292
|
|
|
293
|
+
describe('native pure-function worklet imports', () => {
|
|
294
|
+
it('keeps live module versions isolated across a transitive HMR update', async () => {
|
|
295
|
+
const root = await createWorkletsProject(false)
|
|
296
|
+
const packageRoot = join(root, 'node_modules/pure-math')
|
|
297
|
+
await mkdir(packageRoot, { recursive: true })
|
|
298
|
+
await writeFile(
|
|
299
|
+
join(packageRoot, 'package.json'),
|
|
300
|
+
JSON.stringify({ type: 'module', exports: './index.js' })
|
|
301
|
+
)
|
|
302
|
+
const dependency = join(packageRoot, 'amount.ios.js')
|
|
303
|
+
await writeFile(dependency, 'export const amount = 2')
|
|
304
|
+
await writeFile(join(packageRoot, 'amount.android.js'), 'export const amount = 99')
|
|
305
|
+
await writeFile(
|
|
306
|
+
join(packageRoot, 'index.js'),
|
|
307
|
+
`
|
|
308
|
+
import { amount } from './amount'
|
|
309
|
+
const instance = globalThis.moduleInitializations = (globalThis.moduleInitializations || 0) + 1
|
|
310
|
+
export default function calculate(value) { return value + amount }
|
|
311
|
+
export function reads() { return instance }
|
|
312
|
+
`
|
|
313
|
+
)
|
|
314
|
+
await writeFile(
|
|
315
|
+
join(root, 'entry.mjs'),
|
|
316
|
+
`
|
|
317
|
+
import calculate, { reads } from 'pure-math'
|
|
318
|
+
globalThis.calculate = calculate
|
|
319
|
+
globalThis.reads = reads
|
|
320
|
+
if (import.meta.hot) import.meta.hot.accept(() => {})
|
|
321
|
+
`
|
|
322
|
+
)
|
|
323
|
+
const compiler = await import('@vxrn/compiler')
|
|
324
|
+
compiler.configureVXRNCompilerPlugin({
|
|
325
|
+
enableReanimated: true,
|
|
326
|
+
enableNativeWorklets: true,
|
|
327
|
+
})
|
|
328
|
+
let resolveUpdate!: (value: any) => void
|
|
329
|
+
const updated = new Promise<any>((resolve) => {
|
|
330
|
+
resolveUpdate = resolve
|
|
331
|
+
})
|
|
332
|
+
const native = await createNativeDevEngine({
|
|
333
|
+
root,
|
|
334
|
+
port: 0,
|
|
335
|
+
platform: 'ios',
|
|
336
|
+
plugins: [
|
|
337
|
+
workletImportsPlugin({ 'pure-math': ['default', 'reads'] }),
|
|
338
|
+
{
|
|
339
|
+
name: 'worklet-import-fixture',
|
|
340
|
+
transform(_code, id) {
|
|
341
|
+
if (id.endsWith('/__virtual-native-entry.tsx')) return `import './entry.mjs'`
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
onHmrUpdate: resolveUpdate,
|
|
346
|
+
})
|
|
347
|
+
try {
|
|
348
|
+
const initial = await native.getBundle()
|
|
349
|
+
const rn: any = { console, setTimeout, clearTimeout }
|
|
350
|
+
runInNewContext(initial.code, rn)
|
|
351
|
+
expect(rn.calculate(10)).toBe(12)
|
|
352
|
+
expect(rn.reads()).toBe(1)
|
|
353
|
+
const ui: any = createContext({})
|
|
354
|
+
const compiled = new Map<number, Function>()
|
|
355
|
+
const reconstruct = (worklet: any): Function => {
|
|
356
|
+
if (!worklet.__initData || !worklet.__workletHash)
|
|
357
|
+
throw new Error('remote function')
|
|
358
|
+
let fn = compiled.get(worklet.__workletHash)
|
|
359
|
+
if (!fn) {
|
|
360
|
+
fn = runInContext(`(${worklet.__initData.code})`, ui)
|
|
361
|
+
compiled.set(worklet.__workletHash, fn!)
|
|
362
|
+
}
|
|
363
|
+
const closure = Object.fromEntries(
|
|
364
|
+
Object.entries(worklet.__closure).map(([name, value]) => [
|
|
365
|
+
name,
|
|
366
|
+
typeof value === 'function' ? reconstruct(value) : value,
|
|
367
|
+
])
|
|
368
|
+
)
|
|
369
|
+
return fn!.bind({ __closure: closure })
|
|
370
|
+
}
|
|
371
|
+
expect(() => reconstruct((value: number) => value + 2)).toThrow('remote function')
|
|
372
|
+
const old = reconstruct(rn.calculate)
|
|
373
|
+
const oldReads = reconstruct(rn.reads)
|
|
374
|
+
expect(old(10)).toBe(12)
|
|
375
|
+
expect(reconstruct(rn.calculate)(11)).toBe(13)
|
|
376
|
+
expect(oldReads()).toBe(1)
|
|
377
|
+
expect(rn.reads()).toBe(1)
|
|
378
|
+
|
|
379
|
+
const runtime = rn.__rolldown_runtime__
|
|
380
|
+
await native.engine.registerClient(runtime.clientId)
|
|
381
|
+
await writeFile(dependency, 'export const amount = 3')
|
|
382
|
+
const update = await updated
|
|
383
|
+
expect(update.type).toBe('hmr:update')
|
|
384
|
+
expect(runtime.applyHmrUpdate(update.code, update.changedIds, update.seq)).toBe(
|
|
385
|
+
true
|
|
386
|
+
)
|
|
387
|
+
expect(rn.calculate(10)).toBe(13)
|
|
388
|
+
const next = reconstruct(rn.calculate)
|
|
389
|
+
expect([old(10), next(10), old(10), next(10)]).toEqual([12, 13, 12, 13])
|
|
390
|
+
const nextReads = reconstruct(rn.reads)
|
|
391
|
+
expect([oldReads(), nextReads(), oldReads(), nextReads()]).toEqual([1, 2, 1, 2])
|
|
392
|
+
expect(ui.moduleInitializations).toBe(2)
|
|
393
|
+
expect(rn.reads()).toBe(2)
|
|
394
|
+
|
|
395
|
+
await writeFile(
|
|
396
|
+
join(root, 'production.mjs'),
|
|
397
|
+
`import calculate, { reads } from 'pure-math'; globalThis.calculate = calculate; globalThis.reads = reads`
|
|
398
|
+
)
|
|
399
|
+
const production = await buildNativeBundle({
|
|
400
|
+
root,
|
|
401
|
+
platform: 'android',
|
|
402
|
+
entryFile: 'production.mjs',
|
|
403
|
+
plugins: [workletImportsPlugin({ 'pure-math': ['default', 'reads'] })],
|
|
404
|
+
})
|
|
405
|
+
const productionRN: any = { console }
|
|
406
|
+
runInNewContext(production.code, productionRN)
|
|
407
|
+
expect(productionRN.calculate(10)).toBe(109)
|
|
408
|
+
expect(reconstruct(productionRN.calculate)(10)).toBe(109)
|
|
409
|
+
expect(reconstruct(productionRN.reads)()).toBe(3)
|
|
410
|
+
expect(productionRN.reads()).toBe(1)
|
|
411
|
+
|
|
412
|
+
const sharedPlugin = workletImportsPlugin({ 'pure-math': ['default', 'reads'] })
|
|
413
|
+
const parallel = await Promise.all(
|
|
414
|
+
(['ios', 'android'] as const).map(async (platform) => {
|
|
415
|
+
const output = await buildNativeBundle({
|
|
416
|
+
root,
|
|
417
|
+
platform,
|
|
418
|
+
entryFile: 'production.mjs',
|
|
419
|
+
plugins: [sharedPlugin],
|
|
420
|
+
})
|
|
421
|
+
const context: any = { console }
|
|
422
|
+
runInNewContext(output.code, context)
|
|
423
|
+
return context.calculate(10)
|
|
424
|
+
})
|
|
425
|
+
)
|
|
426
|
+
expect(parallel).toEqual([13, 109])
|
|
427
|
+
} finally {
|
|
428
|
+
await native.close()
|
|
429
|
+
compiler.configureVXRNCompilerPlugin({
|
|
430
|
+
enableReanimated: false,
|
|
431
|
+
enableNativeWorklets: false,
|
|
432
|
+
})
|
|
433
|
+
await rm(root, { recursive: true, force: true })
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it.each([
|
|
438
|
+
[
|
|
439
|
+
'unselected',
|
|
440
|
+
'export default function value() { return 1 }; export const other = 2',
|
|
441
|
+
`import { other } from 'pure-math'; globalThis.value = other`,
|
|
442
|
+
/unselected export other/,
|
|
443
|
+
],
|
|
444
|
+
[
|
|
445
|
+
'missing',
|
|
446
|
+
'export const other = 2',
|
|
447
|
+
`import value from 'pure-math'; globalThis.value = value`,
|
|
448
|
+
/default.*not exported|MISSING_EXPORT/,
|
|
449
|
+
],
|
|
450
|
+
[
|
|
451
|
+
'builtin',
|
|
452
|
+
`import fs from 'node:fs'; export default function value() { return fs.readFileSync('x') }`,
|
|
453
|
+
`import value from 'pure-math'; globalThis.value = value`,
|
|
454
|
+
/runtime dependency node:fs/,
|
|
455
|
+
],
|
|
456
|
+
[
|
|
457
|
+
'global',
|
|
458
|
+
'export default function value() { return process.pid }',
|
|
459
|
+
`import value from 'pure-math'; globalThis.value = value`,
|
|
460
|
+
/unsupported runtime globals: process/,
|
|
461
|
+
],
|
|
462
|
+
[
|
|
463
|
+
'dynamic',
|
|
464
|
+
`export default function value() { return import('./other.js') }`,
|
|
465
|
+
`import value from 'pure-math'; globalThis.value = value`,
|
|
466
|
+
/dynamic imports are unsupported/,
|
|
467
|
+
],
|
|
468
|
+
] as const)(
|
|
469
|
+
'rejects %s through the native engine',
|
|
470
|
+
async (_name, source, entry, expected) => {
|
|
471
|
+
const root = await createWorkletsProject(false)
|
|
472
|
+
const packageRoot = join(root, 'node_modules/pure-math')
|
|
473
|
+
await mkdir(packageRoot, { recursive: true })
|
|
474
|
+
await writeFile(
|
|
475
|
+
join(packageRoot, 'package.json'),
|
|
476
|
+
JSON.stringify({ type: 'module', exports: './index.js' })
|
|
477
|
+
)
|
|
478
|
+
await writeFile(join(packageRoot, 'index.js'), source)
|
|
479
|
+
await writeFile(join(root, 'entry.mjs'), entry)
|
|
480
|
+
const compiler = await import('@vxrn/compiler')
|
|
481
|
+
compiler.configureVXRNCompilerPlugin({
|
|
482
|
+
enableReanimated: true,
|
|
483
|
+
enableNativeWorklets: true,
|
|
484
|
+
})
|
|
485
|
+
const native = await createNativeDevEngine({
|
|
486
|
+
root,
|
|
487
|
+
port: 0,
|
|
488
|
+
platform: 'ios',
|
|
489
|
+
plugins: [
|
|
490
|
+
workletImportsPlugin({ 'pure-math': ['default'] }),
|
|
491
|
+
{
|
|
492
|
+
name: 'worklet-import-negative-fixture',
|
|
493
|
+
transform(_code, id) {
|
|
494
|
+
if (id.endsWith('/__virtual-native-entry.tsx'))
|
|
495
|
+
return `import './entry.mjs'`
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
})
|
|
500
|
+
try {
|
|
501
|
+
await expect(native.getBundle()).rejects.toThrow(expected)
|
|
502
|
+
} finally {
|
|
503
|
+
await native.close()
|
|
504
|
+
compiler.configureVXRNCompilerPlugin({
|
|
505
|
+
enableReanimated: false,
|
|
506
|
+
enableNativeWorklets: false,
|
|
507
|
+
})
|
|
508
|
+
await rm(root, { recursive: true, force: true })
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
})
|
|
513
|
+
|
|
79
514
|
describe('native Rolldown HMR runtime', () => {
|
|
515
|
+
it.each([true, false])('keeps CommonJS default components callable through the native HMR wire path (Babel=%s)', async (babel) => {
|
|
516
|
+
const root = await mkdtemp(join(tmpdir(), 'vxrn-native-hmr-interop-'))
|
|
517
|
+
const entry = join(root, 'entry.mjs')
|
|
518
|
+
const packageRoot = join(root, 'node_modules/component')
|
|
519
|
+
await mkdir(packageRoot, { recursive: true })
|
|
520
|
+
await writeFile(
|
|
521
|
+
join(packageRoot, 'package.json'),
|
|
522
|
+
JSON.stringify({ main: './component(view).js' })
|
|
523
|
+
)
|
|
524
|
+
await writeFile(
|
|
525
|
+
join(packageRoot, 'component(view).js'),
|
|
526
|
+
babel
|
|
527
|
+
? `Object.defineProperty(exports, '__esModule', { value: true }); exports.default = function Component() { return 42 }`
|
|
528
|
+
: `module.exports = function Component() { return 42 }`
|
|
529
|
+
)
|
|
530
|
+
const source = (version: number) => `
|
|
531
|
+
import Component from 'component'
|
|
532
|
+
globalThis.renderComponent = Component
|
|
533
|
+
export const version = ${version}
|
|
534
|
+
if (import.meta.hot) import.meta.hot.accept(() => {})
|
|
535
|
+
`
|
|
536
|
+
await writeFile(entry, source(1))
|
|
537
|
+
let resolveUpdate!: (update: any) => void
|
|
538
|
+
const updateReceived = new Promise<any>((resolve) => { resolveUpdate = resolve })
|
|
539
|
+
const native = await createNativeDevEngine({
|
|
540
|
+
root,
|
|
541
|
+
port: 0,
|
|
542
|
+
platform: 'ios',
|
|
543
|
+
plugins: [{
|
|
544
|
+
name: 'native-hmr-fixture-entry',
|
|
545
|
+
transform(_code, id) {
|
|
546
|
+
if (id.endsWith('/__virtual-native-entry.tsx')) return `import './entry.mjs'`
|
|
547
|
+
},
|
|
548
|
+
}],
|
|
549
|
+
onHmrUpdate: resolveUpdate,
|
|
550
|
+
})
|
|
551
|
+
try {
|
|
552
|
+
const initial = await native.getBundle()
|
|
553
|
+
const context: any = { console, setTimeout, clearTimeout }
|
|
554
|
+
runInNewContext(initial.code, context)
|
|
555
|
+
expect(context.renderComponent()).toBe(42)
|
|
556
|
+
const runtime = context.__rolldown_runtime__
|
|
557
|
+
await native.engine.registerClient(runtime.clientId)
|
|
558
|
+
await writeFile(entry, source(2))
|
|
559
|
+
const update = await updateReceived
|
|
560
|
+
expect(update.type).toBe('hmr:update')
|
|
561
|
+
expect(update.clientId).toBe(runtime.clientId)
|
|
562
|
+
expect(runtime.applyHmrUpdate(update.code, update.changedIds, update.seq)).toBe(true)
|
|
563
|
+
expect(context.renderComponent()).toBe(42)
|
|
564
|
+
} finally {
|
|
565
|
+
await native.close()
|
|
566
|
+
await rm(root, { recursive: true, force: true })
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
|
|
80
570
|
it(
|
|
81
571
|
'registers a Rolldown 1.2 client and applies a self-accepted patch',
|
|
82
572
|
{ timeout: 30_000 },
|
|
@@ -493,6 +983,35 @@ describe('getHermesSWCIncludes', () => {
|
|
|
493
983
|
expect(result.code).not.toContain('async function *')
|
|
494
984
|
})
|
|
495
985
|
|
|
986
|
+
it('downlevels private fields, public class fields, and static blocks for Hermes', async () => {
|
|
987
|
+
const plugin = hermesCompatSWCPlugin(true)
|
|
988
|
+
if (typeof plugin.transform !== 'function') {
|
|
989
|
+
throw new Error('Hermes compatibility transform hook is not callable')
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const result = await Reflect.apply(plugin.transform, undefined, [
|
|
993
|
+
`
|
|
994
|
+
class TransformProbe {
|
|
995
|
+
#privateField = 1
|
|
996
|
+
publicField = 2
|
|
997
|
+
static {
|
|
998
|
+
TransformProbe.initialized = true
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
getSecret() {
|
|
1002
|
+
return this.#privateField
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
`,
|
|
1006
|
+
'/project/TransformProbe.ts',
|
|
1007
|
+
])
|
|
1008
|
+
|
|
1009
|
+
expect(result.code).not.toContain('#privateField')
|
|
1010
|
+
expect(result.code).not.toMatch(/^\s*publicField\s*=/m)
|
|
1011
|
+
expect(result.code).not.toContain('static {')
|
|
1012
|
+
expect(result.code).toContain('this.publicField = 2')
|
|
1013
|
+
})
|
|
1014
|
+
|
|
496
1015
|
it('preserves per-iteration bindings used by lazy method getters', async () => {
|
|
497
1016
|
const plugin = hermesCompatSWCPlugin(true)
|
|
498
1017
|
if (typeof plugin.transform !== 'function') {
|
|
@@ -661,6 +1180,9 @@ describe('native required transform failures', () => {
|
|
|
661
1180
|
async (dev) => {
|
|
662
1181
|
const testRoot = await createWorkletsProject()
|
|
663
1182
|
const compiler = await import('@vxrn/compiler')
|
|
1183
|
+
const spy = vi
|
|
1184
|
+
.spyOn(compiler, 'transformWorklets')
|
|
1185
|
+
.mockRejectedValue(new Error('NATIVE_TRANSFORM_NEGATIVE_CONTROL'))
|
|
664
1186
|
compiler.configureVXRNCompilerPlugin({ enableReanimated: true })
|
|
665
1187
|
|
|
666
1188
|
try {
|
|
@@ -676,6 +1198,7 @@ describe('native required transform failures', () => {
|
|
|
676
1198
|
])
|
|
677
1199
|
).rejects.toThrow('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
|
|
678
1200
|
} finally {
|
|
1201
|
+
spy.mockRestore()
|
|
679
1202
|
compiler.configureVXRNCompilerPlugin({ enableReanimated: false })
|
|
680
1203
|
await rm(testRoot, { recursive: true, force: true })
|
|
681
1204
|
}
|
|
@@ -685,6 +1208,9 @@ describe('native required transform failures', () => {
|
|
|
685
1208
|
it('returns a dev build error and no output when the required compiler transform fails', async () => {
|
|
686
1209
|
const testRoot = await createWorkletsProject()
|
|
687
1210
|
const compiler = await import('@vxrn/compiler')
|
|
1211
|
+
const spy = vi
|
|
1212
|
+
.spyOn(compiler, 'transformWorklets')
|
|
1213
|
+
.mockRejectedValue(new Error('NATIVE_TRANSFORM_NEGATIVE_CONTROL'))
|
|
688
1214
|
compiler.configureVXRNCompilerPlugin({ enableReanimated: true })
|
|
689
1215
|
let resolveOutput!: (output: unknown) => void
|
|
690
1216
|
const output = new Promise<unknown>((resolve) => {
|
|
@@ -706,6 +1232,7 @@ describe('native required transform failures', () => {
|
|
|
706
1232
|
expect(result).toBeInstanceOf(Error)
|
|
707
1233
|
expect(String(result)).toContain('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
|
|
708
1234
|
} finally {
|
|
1235
|
+
spy.mockRestore()
|
|
709
1236
|
await engine.close()
|
|
710
1237
|
compiler.configureVXRNCompilerPlugin({ enableReanimated: false })
|
|
711
1238
|
await rm(testRoot, { recursive: true, force: true })
|
|
@@ -715,6 +1242,9 @@ describe('native required transform failures', () => {
|
|
|
715
1242
|
it('emits no production bundle when the required compiler transform fails', async () => {
|
|
716
1243
|
const testRoot = await createWorkletsProject()
|
|
717
1244
|
const compiler = await import('@vxrn/compiler')
|
|
1245
|
+
const spy = vi
|
|
1246
|
+
.spyOn(compiler, 'transformWorklets')
|
|
1247
|
+
.mockRejectedValue(new Error('NATIVE_TRANSFORM_NEGATIVE_CONTROL'))
|
|
718
1248
|
compiler.configureVXRNCompilerPlugin({ enableReanimated: true })
|
|
719
1249
|
|
|
720
1250
|
try {
|
|
@@ -726,6 +1256,7 @@ describe('native required transform failures', () => {
|
|
|
726
1256
|
})
|
|
727
1257
|
).rejects.toThrow('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
|
|
728
1258
|
} finally {
|
|
1259
|
+
spy.mockRestore()
|
|
729
1260
|
compiler.configureVXRNCompilerPlugin({ enableReanimated: false })
|
|
730
1261
|
await rm(testRoot, { recursive: true, force: true })
|
|
731
1262
|
}
|
|
@@ -775,6 +1306,292 @@ describe('native required transform failures', () => {
|
|
|
775
1306
|
await rm(testRoot, { recursive: true, force: true })
|
|
776
1307
|
}
|
|
777
1308
|
})
|
|
1309
|
+
|
|
1310
|
+
it('composes source maps across React Compiler, Fast Refresh, and module wrapper back to original source lines', async () => {
|
|
1311
|
+
const { TraceMap, originalPositionFor } = await import('@jridgewell/trace-mapping')
|
|
1312
|
+
const compiler = await import('@vxrn/compiler')
|
|
1313
|
+
compiler.configureVXRNCompilerPlugin({ enableCompiler: true })
|
|
1314
|
+
|
|
1315
|
+
try {
|
|
1316
|
+
const plugin = vxrnCompilerPlugin('ios', true, '/project', true)
|
|
1317
|
+
if (typeof plugin.transform !== 'function') {
|
|
1318
|
+
throw new Error('transform hook not callable')
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const inputCode = `import { useState } from 'react'
|
|
1322
|
+
|
|
1323
|
+
export function Counter() {
|
|
1324
|
+
const [count, setCount] = useState(0)
|
|
1325
|
+
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
1326
|
+
}
|
|
1327
|
+
`
|
|
1328
|
+
const id = '/project/Counter.tsx'
|
|
1329
|
+
const result = await Reflect.apply(plugin.transform, undefined, [inputCode, id])
|
|
1330
|
+
|
|
1331
|
+
expect(result).toBeDefined()
|
|
1332
|
+
expect(result.map).toBeDefined()
|
|
1333
|
+
|
|
1334
|
+
const tracer = new TraceMap(result.map)
|
|
1335
|
+
const lines = result.code.split('\n')
|
|
1336
|
+
const counterLineIndex = lines.findIndex((l: string) =>
|
|
1337
|
+
l.includes('function Counter')
|
|
1338
|
+
)
|
|
1339
|
+
expect(counterLineIndex).toBeGreaterThan(0)
|
|
1340
|
+
|
|
1341
|
+
const pos = originalPositionFor(tracer, {
|
|
1342
|
+
line: counterLineIndex + 1,
|
|
1343
|
+
column: 16,
|
|
1344
|
+
})
|
|
1345
|
+
|
|
1346
|
+
expect(pos.source).toBe(id)
|
|
1347
|
+
expect(pos.line).toBe(3)
|
|
1348
|
+
} finally {
|
|
1349
|
+
compiler.configureVXRNCompilerPlugin({ enableCompiler: false })
|
|
1350
|
+
}
|
|
1351
|
+
})
|
|
1352
|
+
|
|
1353
|
+
it('composes source maps accurately across mixed path (React Compiler + Babel + Fast Refresh + module wrapper)', async () => {
|
|
1354
|
+
const { TraceMap, originalPositionFor } = await import('@jridgewell/trace-mapping')
|
|
1355
|
+
const compiler = await import('@vxrn/compiler')
|
|
1356
|
+
const origEnv = process.env.VXRN_USE_BABEL_FOR_GENERATORS
|
|
1357
|
+
process.env.VXRN_USE_BABEL_FOR_GENERATORS = '1'
|
|
1358
|
+
compiler.configureVXRNCompilerPlugin({
|
|
1359
|
+
enableCompiler: true,
|
|
1360
|
+
enableReanimated: false,
|
|
1361
|
+
})
|
|
1362
|
+
|
|
1363
|
+
try {
|
|
1364
|
+
const plugin = vxrnCompilerPlugin('ios', true, '/project', true)
|
|
1365
|
+
if (typeof plugin.transform !== 'function') {
|
|
1366
|
+
throw new Error('transform hook not callable')
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const inputCode = `export function Counter() { return <View />; }
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
export async function* probe() { throw new Error("MARKER"); }
|
|
1378
|
+
`
|
|
1379
|
+
const id = '/project/Counter.tsx'
|
|
1380
|
+
const result = await Reflect.apply(plugin.transform, undefined, [inputCode, id])
|
|
1381
|
+
|
|
1382
|
+
expect(result).toBeDefined()
|
|
1383
|
+
expect(result.map).toBeDefined()
|
|
1384
|
+
// Assert that Babel's generator downleveling actually executed
|
|
1385
|
+
expect(
|
|
1386
|
+
result.code.includes('_wrapAsyncGenerator') ||
|
|
1387
|
+
result.code.includes('regeneratorRuntime')
|
|
1388
|
+
).toBe(true)
|
|
1389
|
+
|
|
1390
|
+
const tracer = new TraceMap(result.map)
|
|
1391
|
+
const lines = result.code.split('\n')
|
|
1392
|
+
const markerLineIndex = lines.findIndex((l: string) => l.includes('MARKER'))
|
|
1393
|
+
expect(markerLineIndex).toBeGreaterThan(0)
|
|
1394
|
+
const markerCol = lines[markerLineIndex].indexOf('throw')
|
|
1395
|
+
|
|
1396
|
+
const pos = originalPositionFor(tracer, {
|
|
1397
|
+
line: markerLineIndex + 1,
|
|
1398
|
+
column: markerCol,
|
|
1399
|
+
})
|
|
1400
|
+
|
|
1401
|
+
expect(pos.source).toBe(id)
|
|
1402
|
+
expect(pos.line).toBe(9)
|
|
1403
|
+
expect(pos.column).toBe(33)
|
|
1404
|
+
|
|
1405
|
+
// Negative control: verify that remapping through compilerOut.map a second time
|
|
1406
|
+
// (the prior double-composition bug) breaks the trace and returns null source/line.
|
|
1407
|
+
const compilerOut = await compiler.transformOxcReactCompiler(
|
|
1408
|
+
id,
|
|
1409
|
+
inputCode,
|
|
1410
|
+
'19',
|
|
1411
|
+
true
|
|
1412
|
+
)
|
|
1413
|
+
const remapping = (await import('@jridgewell/remapping')).default
|
|
1414
|
+
const doubleComposedMap = remapping(
|
|
1415
|
+
[result.map as any, compilerOut.map as any],
|
|
1416
|
+
() => null
|
|
1417
|
+
)
|
|
1418
|
+
const badTracer = new TraceMap(doubleComposedMap as any)
|
|
1419
|
+
const badPos = originalPositionFor(badTracer, {
|
|
1420
|
+
line: markerLineIndex + 1,
|
|
1421
|
+
column: markerCol,
|
|
1422
|
+
})
|
|
1423
|
+
expect(badPos.source).toBeNull()
|
|
1424
|
+
expect(badPos.line).toBeNull()
|
|
1425
|
+
} finally {
|
|
1426
|
+
compiler.configureVXRNCompilerPlugin({ enableCompiler: false })
|
|
1427
|
+
if (origEnv !== undefined) {
|
|
1428
|
+
process.env.VXRN_USE_BABEL_FOR_GENERATORS = origEnv
|
|
1429
|
+
} else {
|
|
1430
|
+
delete process.env.VXRN_USE_BABEL_FOR_GENERATORS
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
})
|
|
1434
|
+
|
|
1435
|
+
it('transforms worklets via native Rust/Wasm SWC with zero Babel execution and accurate sourcemaps', async () => {
|
|
1436
|
+
const { TraceMap, originalPositionFor } = await import('@jridgewell/trace-mapping')
|
|
1437
|
+
const compiler = await import('@vxrn/compiler')
|
|
1438
|
+
const { vi } = await import('vitest')
|
|
1439
|
+
const babelSpy = vi.spyOn(compiler, 'transformBabel')
|
|
1440
|
+
|
|
1441
|
+
compiler.configureVXRNCompilerPlugin({
|
|
1442
|
+
enableReanimated: true,
|
|
1443
|
+
enableNativeWorklets: true,
|
|
1444
|
+
enableCompiler: false,
|
|
1445
|
+
})
|
|
1446
|
+
|
|
1447
|
+
try {
|
|
1448
|
+
const projectRoot = process.cwd()
|
|
1449
|
+
const plugin = vxrnCompilerPlugin('ios', true, projectRoot, true)
|
|
1450
|
+
if (typeof plugin.transform !== 'function') {
|
|
1451
|
+
throw new Error('transform hook not callable')
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const inputCode = `import { useAnimatedStyle } from 'react-native-reanimated'
|
|
1455
|
+
|
|
1456
|
+
export function Box() {
|
|
1457
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
1458
|
+
return { opacity: 1 }
|
|
1459
|
+
})
|
|
1460
|
+
return <div style={animatedStyle} />
|
|
1461
|
+
}
|
|
1462
|
+
`
|
|
1463
|
+
const id = join(projectRoot, 'Box.tsx')
|
|
1464
|
+
const result = await Reflect.apply(plugin.transform, undefined, [inputCode, id])
|
|
1465
|
+
|
|
1466
|
+
expect(result).toBeDefined()
|
|
1467
|
+
expect(result.code).toContain('__workletHash')
|
|
1468
|
+
expect(result.code).toContain('__closure')
|
|
1469
|
+
// Zero Babel calls!
|
|
1470
|
+
expect(babelSpy).not.toHaveBeenCalled()
|
|
1471
|
+
|
|
1472
|
+
// Source map tracing
|
|
1473
|
+
expect(result.map).toBeDefined()
|
|
1474
|
+
const tracer = new TraceMap(result.map)
|
|
1475
|
+
const lines = result.code.split('\n')
|
|
1476
|
+
const boxLineIndex = lines.findIndex((l: string) =>
|
|
1477
|
+
l.includes('export function Box')
|
|
1478
|
+
)
|
|
1479
|
+
expect(boxLineIndex).toBeGreaterThan(0)
|
|
1480
|
+
|
|
1481
|
+
const pos = originalPositionFor(tracer, {
|
|
1482
|
+
line: boxLineIndex + 1,
|
|
1483
|
+
column: 16,
|
|
1484
|
+
})
|
|
1485
|
+
|
|
1486
|
+
expect(pos.source).toBe(id)
|
|
1487
|
+
expect(pos.line).toBe(3)
|
|
1488
|
+
} finally {
|
|
1489
|
+
babelSpy.mockRestore()
|
|
1490
|
+
compiler.configureVXRNCompilerPlugin({
|
|
1491
|
+
enableReanimated: false,
|
|
1492
|
+
enableNativeWorklets: false,
|
|
1493
|
+
})
|
|
1494
|
+
}
|
|
1495
|
+
})
|
|
1496
|
+
|
|
1497
|
+
it('retains the existing Babel backend by default when enableNativeWorklets is false for auto-detected Reanimated and ordinary configured worklets plugin', async () => {
|
|
1498
|
+
const compiler = await import('@vxrn/compiler')
|
|
1499
|
+
const { vi } = await import('vitest')
|
|
1500
|
+
const babelSpy = vi.spyOn(compiler, 'transformBabel').mockResolvedValue({
|
|
1501
|
+
code: '/* babel transformed */',
|
|
1502
|
+
} as any)
|
|
1503
|
+
const workletSpy = vi.spyOn(compiler, 'transformWorklets')
|
|
1504
|
+
|
|
1505
|
+
// enableReanimated is auto-detected, but enableNativeWorklets is NOT explicitly enabled
|
|
1506
|
+
compiler.configureVXRNCompilerPlugin({
|
|
1507
|
+
enableReanimated: true,
|
|
1508
|
+
enableNativeWorklets: false,
|
|
1509
|
+
})
|
|
1510
|
+
|
|
1511
|
+
const getBabelOptionsSpy = vi.spyOn(compiler, 'getBabelOptions').mockReturnValue({
|
|
1512
|
+
plugins: ['react-native-reanimated/plugin'],
|
|
1513
|
+
} as any)
|
|
1514
|
+
|
|
1515
|
+
try {
|
|
1516
|
+
const projectRoot = process.cwd()
|
|
1517
|
+
const plugin = vxrnCompilerPlugin('ios', true, projectRoot, false)
|
|
1518
|
+
if (typeof plugin.transform !== 'function') {
|
|
1519
|
+
throw new Error('transform hook not callable')
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const inputCode = `export function fn() { 'worklet'; return 1 }`
|
|
1523
|
+
const id = join(projectRoot, 'DefaultWorklet.tsx')
|
|
1524
|
+
const result = await Reflect.apply(plugin.transform, undefined, [inputCode, id])
|
|
1525
|
+
|
|
1526
|
+
expect(result).toBeDefined()
|
|
1527
|
+
// Retained existing Babel backend: Babel was called with the plugin
|
|
1528
|
+
expect(babelSpy).toHaveBeenCalledWith(
|
|
1529
|
+
id,
|
|
1530
|
+
inputCode,
|
|
1531
|
+
expect.objectContaining({
|
|
1532
|
+
plugins: expect.arrayContaining(['react-native-reanimated/plugin']),
|
|
1533
|
+
})
|
|
1534
|
+
)
|
|
1535
|
+
// Native SWC was NOT called
|
|
1536
|
+
expect(workletSpy).not.toHaveBeenCalled()
|
|
1537
|
+
} finally {
|
|
1538
|
+
babelSpy.mockRestore()
|
|
1539
|
+
workletSpy.mockRestore()
|
|
1540
|
+
getBabelOptionsSpy.mockRestore()
|
|
1541
|
+
compiler.configureVXRNCompilerPlugin({
|
|
1542
|
+
enableReanimated: false,
|
|
1543
|
+
enableNativeWorklets: false,
|
|
1544
|
+
})
|
|
1545
|
+
}
|
|
1546
|
+
})
|
|
1547
|
+
|
|
1548
|
+
it('intercepts tuple worklet plugins with options, executes native SWC with options, and skips Babel when enableNativeWorklets is true', async () => {
|
|
1549
|
+
const compiler = await import('@vxrn/compiler')
|
|
1550
|
+
const { vi } = await import('vitest')
|
|
1551
|
+
const babelSpy = vi.spyOn(compiler, 'transformBabel')
|
|
1552
|
+
const workletSpy = vi.spyOn(compiler, 'transformWorklets')
|
|
1553
|
+
|
|
1554
|
+
compiler.configureVXRNCompilerPlugin({
|
|
1555
|
+
enableReanimated: true,
|
|
1556
|
+
enableNativeWorklets: true,
|
|
1557
|
+
})
|
|
1558
|
+
|
|
1559
|
+
const getBabelOptionsSpy = vi.spyOn(compiler, 'getBabelOptions').mockReturnValue({
|
|
1560
|
+
plugins: [['react-native-reanimated/plugin', { globals: ['customGlobal'] }]],
|
|
1561
|
+
} as any)
|
|
1562
|
+
|
|
1563
|
+
try {
|
|
1564
|
+
const projectRoot = process.cwd()
|
|
1565
|
+
const plugin = vxrnCompilerPlugin('ios', true, projectRoot, false)
|
|
1566
|
+
if (typeof plugin.transform !== 'function') {
|
|
1567
|
+
throw new Error('transform hook not callable')
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const inputCode = `export function fn() { 'worklet'; return customGlobal * 2 }`
|
|
1571
|
+
const id = join(projectRoot, 'TupleWorklet.tsx')
|
|
1572
|
+
const result = await Reflect.apply(plugin.transform, undefined, [inputCode, id])
|
|
1573
|
+
|
|
1574
|
+
expect(result).toBeDefined()
|
|
1575
|
+
expect(result.code).toContain('__workletHash')
|
|
1576
|
+
// Forwarded options
|
|
1577
|
+
expect(workletSpy).toHaveBeenCalledWith(
|
|
1578
|
+
id,
|
|
1579
|
+
expect.any(String),
|
|
1580
|
+
false,
|
|
1581
|
+
expect.objectContaining({ globals: ['customGlobal'], projectRoot })
|
|
1582
|
+
)
|
|
1583
|
+
// Babel was completely skipped
|
|
1584
|
+
expect(babelSpy).not.toHaveBeenCalled()
|
|
1585
|
+
} finally {
|
|
1586
|
+
babelSpy.mockRestore()
|
|
1587
|
+
workletSpy.mockRestore()
|
|
1588
|
+
getBabelOptionsSpy.mockRestore()
|
|
1589
|
+
compiler.configureVXRNCompilerPlugin({
|
|
1590
|
+
enableReanimated: false,
|
|
1591
|
+
enableNativeWorklets: false,
|
|
1592
|
+
})
|
|
1593
|
+
}
|
|
1594
|
+
})
|
|
778
1595
|
})
|
|
779
1596
|
|
|
780
1597
|
describe('native production assets', () => {
|
|
@@ -1016,3 +1833,60 @@ globalThis.__vxrnConditionalExportProbe = helper()
|
|
|
1016
1833
|
}
|
|
1017
1834
|
})
|
|
1018
1835
|
})
|
|
1836
|
+
|
|
1837
|
+
describe('native Flow sources', () => {
|
|
1838
|
+
it.each(['', `/* ${'license text '.repeat(150)} */\n`])('strips third-party Flow following a license header (%#)', async (license) => {
|
|
1839
|
+
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-flow-'))
|
|
1840
|
+
// mirrors @react-native-masked-view/masked-view: a Flow `.js` component
|
|
1841
|
+
// outside the react-native / @react-native scopes, which is what reached
|
|
1842
|
+
// rolldown unstripped when its patch was skipped.
|
|
1843
|
+
const packageRoot = join(testRoot, 'node_modules/@flowy-scope/flowy-lib')
|
|
1844
|
+
await mkdir(packageRoot, { recursive: true })
|
|
1845
|
+
await writeFile(
|
|
1846
|
+
join(packageRoot, 'package.json'),
|
|
1847
|
+
JSON.stringify({ name: '@flowy-scope/flowy-lib', main: './js/Flowy.js' })
|
|
1848
|
+
)
|
|
1849
|
+
await mkdir(join(packageRoot, 'js'), { recursive: true })
|
|
1850
|
+
await writeFile(
|
|
1851
|
+
join(packageRoot, 'js/Flowy.js'),
|
|
1852
|
+
`${license}/**
|
|
1853
|
+
* @flow
|
|
1854
|
+
* @format
|
|
1855
|
+
*/
|
|
1856
|
+
|
|
1857
|
+
type FlowyProps = {|
|
|
1858
|
+
+value: number,
|
|
1859
|
+
|}
|
|
1860
|
+
|
|
1861
|
+
export default class Flowy {
|
|
1862
|
+
_seen: boolean = false
|
|
1863
|
+
|
|
1864
|
+
measure(props: FlowyProps): number {
|
|
1865
|
+
const { value, ...rest }: FlowyProps = props
|
|
1866
|
+
this._seen = true
|
|
1867
|
+
return ((value: any): number) + Object.keys(rest).length
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
`
|
|
1871
|
+
)
|
|
1872
|
+
await writeFile(
|
|
1873
|
+
join(testRoot, 'entry.js'),
|
|
1874
|
+
`import Flowy from '@flowy-scope/flowy-lib'
|
|
1875
|
+
globalThis.__vxrnFlowProbe = new Flowy().measure({ value: 41 }) + 1
|
|
1876
|
+
`
|
|
1877
|
+
)
|
|
1878
|
+
|
|
1879
|
+
try {
|
|
1880
|
+
const result = await buildNativeBundle({
|
|
1881
|
+
root: testRoot,
|
|
1882
|
+
platform: 'ios',
|
|
1883
|
+
entryFile: 'entry.js',
|
|
1884
|
+
})
|
|
1885
|
+
const context = { clearTimeout, console, process: { env: {} }, setTimeout }
|
|
1886
|
+
runInNewContext(result.code, context)
|
|
1887
|
+
expect(Reflect.get(context, '__vxrnFlowProbe')).toBe(42)
|
|
1888
|
+
} finally {
|
|
1889
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
1890
|
+
}
|
|
1891
|
+
})
|
|
1892
|
+
})
|