vxrn 1.25.8 → 1.25.9
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/config/getViteServerConfig.mjs +6 -0
- package/dist/config/getViteServerConfig.mjs.map +1 -1
- package/dist/config/getViteServerConfig.native.js +6 -0
- package/dist/config/getViteServerConfig.native.js.map +1 -1
- package/dist/config/getViteServerConfig.test.mjs +26 -0
- package/dist/config/getViteServerConfig.test.mjs.map +1 -0
- package/dist/config/getViteServerConfig.test.native.js +26 -0
- package/dist/config/getViteServerConfig.test.native.js.map +1 -0
- package/dist/exports/dev.mjs +26 -1
- package/dist/exports/dev.mjs.map +1 -1
- package/dist/exports/dev.native.js +26 -1
- package/dist/exports/dev.native.js.map +1 -1
- package/dist/plugins/reactNativeDevServer.mjs +80 -7
- package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
- package/dist/plugins/reactNativeDevServer.native.js +80 -7
- package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
- package/dist/runtime/native-prelude.mjs +7 -0
- package/dist/runtime/native-prelude.mjs.map +1 -1
- package/dist/runtime/native-prelude.native.js +7 -0
- package/dist/runtime/native-prelude.native.js.map +1 -1
- package/dist/utils/createNativeDevEngine.mjs +302 -197
- package/dist/utils/createNativeDevEngine.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.native.js +410 -221
- package/dist/utils/createNativeDevEngine.native.js.map +1 -1
- package/dist/utils/createNativeDevEngine.test.mjs +536 -6
- package/dist/utils/createNativeDevEngine.test.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.test.native.js +559 -6
- package/dist/utils/createNativeDevEngine.test.native.js.map +1 -1
- package/package.json +11 -11
- package/src/config/getViteServerConfig.test.ts +29 -0
- package/src/config/getViteServerConfig.ts +11 -0
- package/src/exports/dev.ts +34 -3
- package/src/plugins/reactNativeDevServer.ts +89 -12
- package/src/runtime/native-prelude.ts +7 -0
- package/src/utils/createNativeDevEngine.test.ts +682 -5
- package/src/utils/createNativeDevEngine.ts +516 -229
- package/types/config/getViteServerConfig.d.ts.map +1 -1
- package/types/config/getViteServerConfig.test.d.ts +2 -0
- package/types/config/getViteServerConfig.test.d.ts.map +1 -0
- package/types/exports/dev.d.ts.map +1 -1
- package/types/plugins/reactNativeDevServer.d.ts +1 -0
- package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
- package/types/runtime/native-prelude.d.ts.map +1 -1
- package/types/utils/createNativeDevEngine.d.ts +40 -2
- package/types/utils/createNativeDevEngine.d.ts.map +1 -1
|
@@ -1,19 +1,81 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
2
3
|
import { tmpdir } from 'node:os'
|
|
3
4
|
import { join } from 'node:path'
|
|
4
|
-
import {
|
|
5
|
+
import { runInNewContext } from 'node:vm'
|
|
6
|
+
import { rolldown, type RolldownOutput } from 'rolldown'
|
|
5
7
|
import { dev } from 'rolldown/experimental'
|
|
6
8
|
import { describe, expect, it } from 'vitest'
|
|
9
|
+
import { getNativePrelude } from '../runtime/native-prelude'
|
|
7
10
|
import {
|
|
11
|
+
buildNativeBundle,
|
|
12
|
+
createNativeDevAssetRegistry,
|
|
13
|
+
getNativeAssetData,
|
|
8
14
|
getHermesSWCIncludes,
|
|
9
15
|
getHmrRuntimeSource,
|
|
10
16
|
getNativeTransformConfig,
|
|
11
17
|
hermesCompatSWCPlugin,
|
|
12
18
|
hmrClientNoopPlugin,
|
|
19
|
+
nativeAnimatedGuardPlugin,
|
|
20
|
+
normalizeNativeCommonJSInterop,
|
|
13
21
|
vxrnCompilerPlugin,
|
|
14
22
|
wrapNativeBundleModuleScope,
|
|
15
23
|
} from './createNativeDevEngine'
|
|
16
24
|
|
|
25
|
+
const nativeTransformProbe = `
|
|
26
|
+
export const transformProbe = () => {
|
|
27
|
+
'worklet'
|
|
28
|
+
return 'transformed'
|
|
29
|
+
}
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
async function createWorkletsProject(throwOnTransform = true) {
|
|
33
|
+
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-transform-failure-'))
|
|
34
|
+
const packageRoot = join(testRoot, 'node_modules/react-native-worklets')
|
|
35
|
+
await mkdir(packageRoot, { recursive: true })
|
|
36
|
+
await writeFile(
|
|
37
|
+
join(packageRoot, 'package.json'),
|
|
38
|
+
JSON.stringify({ name: 'react-native-worklets' })
|
|
39
|
+
)
|
|
40
|
+
await writeFile(
|
|
41
|
+
join(packageRoot, 'plugin.js'),
|
|
42
|
+
throwOnTransform
|
|
43
|
+
? `module.exports = () => ({
|
|
44
|
+
visitor: {
|
|
45
|
+
Program() {
|
|
46
|
+
throw new Error('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
})`
|
|
50
|
+
: `module.exports = () => ({ visitor: {} })`
|
|
51
|
+
)
|
|
52
|
+
await writeFile(join(testRoot, 'entry.ts'), nativeTransformProbe)
|
|
53
|
+
return testRoot
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('native prelude', () => {
|
|
57
|
+
it('does not advertise a host event API that cannot remove listeners', () => {
|
|
58
|
+
const context = {
|
|
59
|
+
addEventListener() {},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
runInNewContext(getNativePrelude({ dev: false, platform: 'ios' }), context)
|
|
63
|
+
|
|
64
|
+
expect(Reflect.get(context, 'addEventListener')).toBeUndefined()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('preserves a complete host event API', () => {
|
|
68
|
+
const addEventListener = () => {}
|
|
69
|
+
const removeEventListener = () => {}
|
|
70
|
+
const context = { addEventListener, removeEventListener }
|
|
71
|
+
|
|
72
|
+
runInNewContext(getNativePrelude({ dev: false, platform: 'ios' }), context)
|
|
73
|
+
|
|
74
|
+
expect(context.addEventListener).toBe(addEventListener)
|
|
75
|
+
expect(context.removeEventListener).toBe(removeEventListener)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
17
79
|
describe('native Rolldown HMR runtime', () => {
|
|
18
80
|
it(
|
|
19
81
|
'registers a Rolldown 1.2 client and applies a self-accepted patch',
|
|
@@ -97,7 +159,14 @@ if (import.meta.hot) {
|
|
|
97
159
|
)?.update
|
|
98
160
|
expect(patch).toBeTruthy()
|
|
99
161
|
|
|
100
|
-
|
|
162
|
+
const applyHmrUpdate = Reflect.get(runtime, 'applyHmrUpdate')
|
|
163
|
+
expect(
|
|
164
|
+
Reflect.apply(applyHmrUpdate, runtime, [
|
|
165
|
+
patch.code,
|
|
166
|
+
patch.changedIds,
|
|
167
|
+
patch.seq,
|
|
168
|
+
])
|
|
169
|
+
).toBe(true)
|
|
101
170
|
expect((globalThis as any).__vxrnHmrBody).toBe('v2')
|
|
102
171
|
expect((globalThis as any).__vxrnHmrAccepted).toBe('v2')
|
|
103
172
|
} finally {
|
|
@@ -109,6 +178,181 @@ if (import.meta.hot) {
|
|
|
109
178
|
}
|
|
110
179
|
}
|
|
111
180
|
)
|
|
181
|
+
|
|
182
|
+
it(
|
|
183
|
+
'propagates a non-component update to a React Refresh boundary',
|
|
184
|
+
{ timeout: 30_000 },
|
|
185
|
+
async () => {
|
|
186
|
+
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-refresh-boundary-'))
|
|
187
|
+
const entry = join(testRoot, 'entry.js')
|
|
188
|
+
const leaf = join(testRoot, 'leaf.js')
|
|
189
|
+
await writeFile(
|
|
190
|
+
entry,
|
|
191
|
+
`
|
|
192
|
+
import { value } from './leaf.js'
|
|
193
|
+
globalThis.__vxrnRefreshBoundaryValue = value
|
|
194
|
+
export function App() {}
|
|
195
|
+
if (import.meta.hot) {
|
|
196
|
+
import.meta.hot.acceptReactRefresh(() => {
|
|
197
|
+
globalThis.__vxrnRefreshBoundaryAccepted = true
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
`
|
|
201
|
+
)
|
|
202
|
+
await writeFile(leaf, `export const value = 'v1'`)
|
|
203
|
+
|
|
204
|
+
let resolveInitialOutput!: (output: any) => void
|
|
205
|
+
const initialOutput = new Promise<any>((resolve) => {
|
|
206
|
+
resolveInitialOutput = resolve
|
|
207
|
+
})
|
|
208
|
+
let resolveHmrUpdate!: (output: any) => void
|
|
209
|
+
const hmrUpdate = new Promise<any>((resolve) => {
|
|
210
|
+
resolveHmrUpdate = resolve
|
|
211
|
+
})
|
|
212
|
+
let registeredClientId: string | undefined
|
|
213
|
+
const engine = await dev(
|
|
214
|
+
{
|
|
215
|
+
cwd: testRoot,
|
|
216
|
+
input: entry,
|
|
217
|
+
experimental: { devMode: { implement: getHmrRuntimeSource() } },
|
|
218
|
+
},
|
|
219
|
+
{ format: 'esm' },
|
|
220
|
+
{
|
|
221
|
+
onOutput(result) {
|
|
222
|
+
resolveInitialOutput(result)
|
|
223
|
+
},
|
|
224
|
+
onHmrUpdates(result) {
|
|
225
|
+
if (
|
|
226
|
+
!(result instanceof Error) &&
|
|
227
|
+
result.updates.some(
|
|
228
|
+
(item) =>
|
|
229
|
+
item.clientId === registeredClientId && item.update.type === 'Patch'
|
|
230
|
+
)
|
|
231
|
+
) {
|
|
232
|
+
resolveHmrUpdate(result)
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
const previousRefreshRuntime = Reflect.get(globalThis, '__ReactRefresh')
|
|
239
|
+
Reflect.set(globalThis, '__ReactRefresh', {
|
|
240
|
+
isLikelyComponentType(value: unknown) {
|
|
241
|
+
return typeof value === 'function'
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
await engine.run()
|
|
247
|
+
const initial = await initialOutput
|
|
248
|
+
if (initial instanceof Error) throw initial
|
|
249
|
+
const chunk = initial.output.find(
|
|
250
|
+
(item: any) => item.type === 'chunk' && item.isEntry
|
|
251
|
+
)
|
|
252
|
+
expect(chunk).toBeTruthy()
|
|
253
|
+
|
|
254
|
+
Reflect.deleteProperty(globalThis, '__rolldown_runtime__')
|
|
255
|
+
await import(
|
|
256
|
+
`data:text/javascript;base64,${Buffer.from(chunk.code).toString('base64')}`
|
|
257
|
+
)
|
|
258
|
+
const runtime = Reflect.get(globalThis, '__rolldown_runtime__')
|
|
259
|
+
registeredClientId = runtime.clientId
|
|
260
|
+
await engine.registerClient(runtime.clientId)
|
|
261
|
+
|
|
262
|
+
await writeFile(leaf, `export const value = 'v2'`)
|
|
263
|
+
const result = await Promise.race([
|
|
264
|
+
hmrUpdate,
|
|
265
|
+
new Promise((_, reject) =>
|
|
266
|
+
setTimeout(() => reject(new Error('timed out waiting for HMR patch')), 10_000)
|
|
267
|
+
),
|
|
268
|
+
])
|
|
269
|
+
const patch = result.updates.find(
|
|
270
|
+
(item: any) =>
|
|
271
|
+
item.clientId === runtime.clientId && item.update.type === 'Patch'
|
|
272
|
+
)?.update
|
|
273
|
+
expect(patch).toBeTruthy()
|
|
274
|
+
|
|
275
|
+
const applyHmrUpdate = Reflect.get(runtime, 'applyHmrUpdate')
|
|
276
|
+
expect(
|
|
277
|
+
Reflect.apply(applyHmrUpdate, runtime, [
|
|
278
|
+
patch.code,
|
|
279
|
+
patch.changedIds,
|
|
280
|
+
patch.seq,
|
|
281
|
+
])
|
|
282
|
+
).toBe(true)
|
|
283
|
+
expect(Reflect.get(globalThis, '__vxrnRefreshBoundaryValue')).toBe('v2')
|
|
284
|
+
expect(Reflect.get(globalThis, '__vxrnRefreshBoundaryAccepted')).toBe(true)
|
|
285
|
+
} finally {
|
|
286
|
+
await engine.close()
|
|
287
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
288
|
+
Reflect.deleteProperty(globalThis, '__rolldown_runtime__')
|
|
289
|
+
Reflect.deleteProperty(globalThis, '__vxrnRefreshBoundaryValue')
|
|
290
|
+
Reflect.deleteProperty(globalThis, '__vxrnRefreshBoundaryAccepted')
|
|
291
|
+
if (previousRefreshRuntime === undefined) {
|
|
292
|
+
Reflect.deleteProperty(globalThis, '__ReactRefresh')
|
|
293
|
+
} else {
|
|
294
|
+
Reflect.set(globalThis, '__ReactRefresh', previousRefreshRuntime)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
describe('native production import.meta lowering', () => {
|
|
302
|
+
it('emits a Hermes-compatible bundle for guarded import.meta.env reads', async () => {
|
|
303
|
+
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-import-meta-'))
|
|
304
|
+
await writeFile(
|
|
305
|
+
join(testRoot, 'entry.js'),
|
|
306
|
+
`globalThis.__vxrnNativeImportMetaProbe = typeof import.meta !== 'undefined' && import.meta.env.DEV`
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const result = await buildNativeBundle({
|
|
311
|
+
root: testRoot,
|
|
312
|
+
platform: 'ios',
|
|
313
|
+
entryFile: 'entry.js',
|
|
314
|
+
})
|
|
315
|
+
expect(result.code).not.toContain('typeof import.meta')
|
|
316
|
+
|
|
317
|
+
const context = { globalThis: {}, process: { env: {} } }
|
|
318
|
+
runInNewContext(result.code, context)
|
|
319
|
+
expect(Reflect.get(context.globalThis, '__vxrnNativeImportMetaProbe')).toBe(false)
|
|
320
|
+
} finally {
|
|
321
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
describe('native animated guard transform', () => {
|
|
327
|
+
it('preserves line count and returns a composable source map', async () => {
|
|
328
|
+
const plugin = nativeAnimatedGuardPlugin()
|
|
329
|
+
if (typeof plugin.transform !== 'function') {
|
|
330
|
+
throw new Error('native animated guard transform hook is not callable')
|
|
331
|
+
}
|
|
332
|
+
const source = [
|
|
333
|
+
'function call(methodName) {',
|
|
334
|
+
' const method = nullthrows(NativeAnimatedModule)[methodName];',
|
|
335
|
+
' method();',
|
|
336
|
+
'}',
|
|
337
|
+
].join('\n')
|
|
338
|
+
|
|
339
|
+
const result = await Reflect.apply(plugin.transform, undefined, [
|
|
340
|
+
source,
|
|
341
|
+
'/project/node_modules/react-native/src/private/animated/NativeAnimatedHelper.js',
|
|
342
|
+
])
|
|
343
|
+
|
|
344
|
+
expect(result.code.split('\n')).toHaveLength(source.split('\n').length)
|
|
345
|
+
expect(result.code).toContain("if (typeof method !== 'function') return")
|
|
346
|
+
expect(result.map).toEqual({
|
|
347
|
+
version: 3,
|
|
348
|
+
sources: [
|
|
349
|
+
'/project/node_modules/react-native/src/private/animated/NativeAnimatedHelper.js',
|
|
350
|
+
],
|
|
351
|
+
sourcesContent: [source],
|
|
352
|
+
names: [],
|
|
353
|
+
mappings: 'AAAA;AACA;AACA;AACA',
|
|
354
|
+
})
|
|
355
|
+
})
|
|
112
356
|
})
|
|
113
357
|
|
|
114
358
|
// use a root with no .env files so only the platform defines are present
|
|
@@ -140,6 +384,24 @@ describe('getNativeTransformConfig platform env defines', () => {
|
|
|
140
384
|
})
|
|
141
385
|
}
|
|
142
386
|
}
|
|
387
|
+
|
|
388
|
+
it('inlines EXPO_PUBLIC values supplied by the native build environment', () => {
|
|
389
|
+
const key = 'EXPO_PUBLIC_VXRN_NATIVE_ENV_PROBE'
|
|
390
|
+
const previous = process.env[key]
|
|
391
|
+
process.env[key] = 'native-env-value'
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const { define } = getNativeTransformConfig('ios', false, root)
|
|
395
|
+
expect(define[`process.env.${key}`]).toBe('"native-env-value"')
|
|
396
|
+
expect(define[`import.meta.env.${key}`]).toBe('"native-env-value"')
|
|
397
|
+
expect(JSON.parse(define['import.meta.env'] as string)[key]).toBe(
|
|
398
|
+
'native-env-value'
|
|
399
|
+
)
|
|
400
|
+
} finally {
|
|
401
|
+
if (previous === undefined) delete process.env[key]
|
|
402
|
+
else process.env[key] = previous
|
|
403
|
+
}
|
|
404
|
+
})
|
|
143
405
|
})
|
|
144
406
|
|
|
145
407
|
describe('wrapNativeBundleModuleScope', () => {
|
|
@@ -174,6 +436,7 @@ describe('getHermesSWCIncludes', () => {
|
|
|
174
436
|
const CLASS_SET = [
|
|
175
437
|
'transform-classes',
|
|
176
438
|
'transform-parameters',
|
|
439
|
+
'transform-block-scoping',
|
|
177
440
|
'transform-class-properties',
|
|
178
441
|
'transform-class-static-block',
|
|
179
442
|
'transform-private-methods',
|
|
@@ -187,11 +450,62 @@ describe('getHermesSWCIncludes', () => {
|
|
|
187
450
|
expect(getHermesSWCIncludes(false)).toEqual(expect.arrayContaining(CLASS_SET))
|
|
188
451
|
})
|
|
189
452
|
|
|
190
|
-
it('adds transform-async-to-generator
|
|
191
|
-
expect(getHermesSWCIncludes(true)).
|
|
453
|
+
it('adds transform-async-to-generator in development and production', () => {
|
|
454
|
+
expect(getHermesSWCIncludes(true)).toContain('transform-async-to-generator')
|
|
192
455
|
expect(getHermesSWCIncludes(false)).toContain('transform-async-to-generator')
|
|
193
456
|
})
|
|
194
457
|
|
|
458
|
+
it('lowers async generators for the Hermes development interpreter', async () => {
|
|
459
|
+
const plugin = hermesCompatSWCPlugin(true)
|
|
460
|
+
if (typeof plugin.transform !== 'function') {
|
|
461
|
+
throw new Error('Hermes compatibility transform hook is not callable')
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const result = await Reflect.apply(plugin.transform, undefined, [
|
|
465
|
+
'export async function* values() { yield await Promise.resolve(1) }',
|
|
466
|
+
'/project/async-generator.ts',
|
|
467
|
+
])
|
|
468
|
+
expect(result.code).not.toContain('async function*')
|
|
469
|
+
expect(result.code).not.toContain('async function *')
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('preserves per-iteration bindings used by lazy method getters', async () => {
|
|
473
|
+
const plugin = hermesCompatSWCPlugin(true)
|
|
474
|
+
if (typeof plugin.transform !== 'function') {
|
|
475
|
+
throw new Error('Hermes compatibility transform hook is not callable')
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const result = await Reflect.apply(plugin.transform, undefined, [
|
|
479
|
+
`
|
|
480
|
+
const installedGroups = new WeakMap()
|
|
481
|
+
function install(inst, methods) {
|
|
482
|
+
const proto = Object.getPrototypeOf(inst)
|
|
483
|
+
for (const key in methods) {
|
|
484
|
+
const fn = methods[key]
|
|
485
|
+
Object.defineProperty(proto, key, {
|
|
486
|
+
get() { return fn.bind(this) }
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function Schema() {
|
|
491
|
+
install(this, {
|
|
492
|
+
nullish() { return 'nullish' },
|
|
493
|
+
apply(fn) { return fn(this) }
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
globalThis.__vxrnBlockScopeProbe = new Schema().nullish()
|
|
497
|
+
`,
|
|
498
|
+
'/project/block-scope-loop.ts',
|
|
499
|
+
])
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
new Function(result.code)()
|
|
503
|
+
expect(Reflect.get(globalThis, '__vxrnBlockScopeProbe')).toBe('nullish')
|
|
504
|
+
} finally {
|
|
505
|
+
Reflect.deleteProperty(globalThis, '__vxrnBlockScopeProbe')
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
|
|
195
509
|
it('bundles lowered classes whose constructors use default and rest parameters', async () => {
|
|
196
510
|
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-hermes-parameters-'))
|
|
197
511
|
const entry = join(testRoot, 'entry.js')
|
|
@@ -310,8 +624,371 @@ describe('vxrnCompilerPlugin React Refresh registration', () => {
|
|
|
310
624
|
expect(code).toContain('var __vxrnRefreshReg = globalThis.$RefreshReg$')
|
|
311
625
|
expect(code).toContain('__vxrnRefreshReg(')
|
|
312
626
|
expect(code).toContain('"$RefreshReg$("')
|
|
627
|
+
expect(code).toContain('import.meta.hot.acceptReactRefresh(')
|
|
313
628
|
} finally {
|
|
314
629
|
process.env.NODE_ENV = previousNodeEnv
|
|
315
630
|
}
|
|
316
631
|
})
|
|
317
632
|
})
|
|
633
|
+
|
|
634
|
+
describe('native required transform failures', () => {
|
|
635
|
+
it.each([true, false])(
|
|
636
|
+
'rejects valid worklet source when its required compiler transform fails (dev=%s)',
|
|
637
|
+
async (dev) => {
|
|
638
|
+
const testRoot = await createWorkletsProject()
|
|
639
|
+
const compiler = await import('@vxrn/compiler')
|
|
640
|
+
compiler.configureVXRNCompilerPlugin({ enableReanimated: true })
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
const plugin = vxrnCompilerPlugin('ios', dev, testRoot)
|
|
644
|
+
if (typeof plugin.transform !== 'function') {
|
|
645
|
+
throw new Error('vxrn compiler transform hook is not callable')
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
await expect(
|
|
649
|
+
Reflect.apply(plugin.transform, undefined, [
|
|
650
|
+
nativeTransformProbe,
|
|
651
|
+
join(testRoot, 'entry.ts'),
|
|
652
|
+
])
|
|
653
|
+
).rejects.toThrow('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
|
|
654
|
+
} finally {
|
|
655
|
+
compiler.configureVXRNCompilerPlugin({ enableReanimated: false })
|
|
656
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
it('returns a dev build error and no output when the required compiler transform fails', async () => {
|
|
662
|
+
const testRoot = await createWorkletsProject()
|
|
663
|
+
const compiler = await import('@vxrn/compiler')
|
|
664
|
+
compiler.configureVXRNCompilerPlugin({ enableReanimated: true })
|
|
665
|
+
let resolveOutput!: (output: unknown) => void
|
|
666
|
+
const output = new Promise<unknown>((resolve) => {
|
|
667
|
+
resolveOutput = resolve
|
|
668
|
+
})
|
|
669
|
+
const engine = await dev(
|
|
670
|
+
{
|
|
671
|
+
cwd: testRoot,
|
|
672
|
+
input: join(testRoot, 'entry.ts'),
|
|
673
|
+
plugins: [vxrnCompilerPlugin('ios', true, testRoot)],
|
|
674
|
+
},
|
|
675
|
+
{ format: 'esm' },
|
|
676
|
+
{ onOutput: resolveOutput }
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
await engine.run()
|
|
681
|
+
const result = await output
|
|
682
|
+
expect(result).toBeInstanceOf(Error)
|
|
683
|
+
expect(String(result)).toContain('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
|
|
684
|
+
} finally {
|
|
685
|
+
await engine.close()
|
|
686
|
+
compiler.configureVXRNCompilerPlugin({ enableReanimated: false })
|
|
687
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
688
|
+
}
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
it('emits no production bundle when the required compiler transform fails', async () => {
|
|
692
|
+
const testRoot = await createWorkletsProject()
|
|
693
|
+
const compiler = await import('@vxrn/compiler')
|
|
694
|
+
compiler.configureVXRNCompilerPlugin({ enableReanimated: true })
|
|
695
|
+
|
|
696
|
+
try {
|
|
697
|
+
await expect(
|
|
698
|
+
buildNativeBundle({
|
|
699
|
+
root: testRoot,
|
|
700
|
+
platform: 'ios',
|
|
701
|
+
entryFile: 'entry.ts',
|
|
702
|
+
})
|
|
703
|
+
).rejects.toThrow('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
|
|
704
|
+
} finally {
|
|
705
|
+
compiler.configureVXRNCompilerPlugin({ enableReanimated: false })
|
|
706
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
707
|
+
}
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
it('rejects Hermes compatibility transform errors instead of returning source', async () => {
|
|
711
|
+
const plugin = hermesCompatSWCPlugin(true)
|
|
712
|
+
if (typeof plugin.transform !== 'function') {
|
|
713
|
+
throw new Error('Hermes compatibility transform hook is not callable')
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
await expect(
|
|
717
|
+
Reflect.apply(plugin.transform, undefined, [
|
|
718
|
+
'class TransformProbe { value = ; }',
|
|
719
|
+
'/project/TransformProbe.ts',
|
|
720
|
+
])
|
|
721
|
+
).rejects.toBeTruthy()
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
it('returns maps for every required production transform', async () => {
|
|
725
|
+
const testRoot = await createWorkletsProject(false)
|
|
726
|
+
const compiler = await import('@vxrn/compiler')
|
|
727
|
+
compiler.configureVXRNCompilerPlugin({ enableReanimated: true })
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
const compilerPlugin = vxrnCompilerPlugin('ios', false, testRoot, true)
|
|
731
|
+
if (typeof compilerPlugin.transform !== 'function') {
|
|
732
|
+
throw new Error('vxrn compiler transform hook is not callable')
|
|
733
|
+
}
|
|
734
|
+
const compilerResult = await Reflect.apply(compilerPlugin.transform, undefined, [
|
|
735
|
+
nativeTransformProbe,
|
|
736
|
+
join(testRoot, 'entry.ts'),
|
|
737
|
+
])
|
|
738
|
+
expect(compilerResult.map).toBeTruthy()
|
|
739
|
+
|
|
740
|
+
const hermesPlugin = hermesCompatSWCPlugin(false, true)
|
|
741
|
+
if (typeof hermesPlugin.transform !== 'function') {
|
|
742
|
+
throw new Error('Hermes compatibility transform hook is not callable')
|
|
743
|
+
}
|
|
744
|
+
const hermesResult = await Reflect.apply(hermesPlugin.transform, undefined, [
|
|
745
|
+
'export class TransformProbe { value = 1 }',
|
|
746
|
+
join(testRoot, 'TransformProbe.ts'),
|
|
747
|
+
])
|
|
748
|
+
expect(hermesResult.map).toBeTruthy()
|
|
749
|
+
} finally {
|
|
750
|
+
compiler.configureVXRNCompilerPlugin({ enableReanimated: false })
|
|
751
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
752
|
+
}
|
|
753
|
+
})
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
describe('native production assets', () => {
|
|
757
|
+
it('registers scale siblings and keeps monorepo assets inside assetsDest', async () => {
|
|
758
|
+
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-assets-'))
|
|
759
|
+
const appRoot = join(testRoot, 'workspace/apps/native-app')
|
|
760
|
+
const appAssets = join(appRoot, 'assets')
|
|
761
|
+
const packageAssets = join(testRoot, 'workspace/node_modules/example/assets')
|
|
762
|
+
const assetsDest = join(testRoot, 'output')
|
|
763
|
+
await mkdir(appAssets, { recursive: true })
|
|
764
|
+
await mkdir(packageAssets, { recursive: true })
|
|
765
|
+
await writeFile(join(appAssets, 'icon.png'), 'icon-1x')
|
|
766
|
+
await writeFile(join(appAssets, 'icon@2x.png'), 'icon-2x')
|
|
767
|
+
await writeFile(join(appAssets, 'icon@3x.png'), 'icon-3x')
|
|
768
|
+
await writeFile(join(packageAssets, 'back.png'), 'back-1x')
|
|
769
|
+
await writeFile(join(packageAssets, 'back@2x.png'), 'back-2x')
|
|
770
|
+
await writeFile(
|
|
771
|
+
join(appRoot, 'entry.js'),
|
|
772
|
+
`
|
|
773
|
+
import icon from './assets/icon.png'
|
|
774
|
+
import back from '../../node_modules/example/assets/back.png'
|
|
775
|
+
globalThis.__nativeAssetProbe = [icon, back]
|
|
776
|
+
`
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
const assetData = await getNativeAssetData(
|
|
781
|
+
join(appAssets, 'icon.png'),
|
|
782
|
+
appRoot,
|
|
783
|
+
'ios'
|
|
784
|
+
)
|
|
785
|
+
expect(assetData.scales).toEqual([1, 2, 3])
|
|
786
|
+
expect(assetData.files.map((file) => file.slice(appAssets.length + 1))).toEqual([
|
|
787
|
+
'icon.png',
|
|
788
|
+
'icon@2x.png',
|
|
789
|
+
'icon@3x.png',
|
|
790
|
+
])
|
|
791
|
+
expect(assetData.hash).not.toBe('')
|
|
792
|
+
|
|
793
|
+
const registry = createNativeDevAssetRegistry()
|
|
794
|
+
registry.register(assetData)
|
|
795
|
+
expect(
|
|
796
|
+
registry.resolve('/assets/assets/icon@2x.png', assetData.hash)?.filePath
|
|
797
|
+
).toBe(join(appAssets, 'icon@2x.png'))
|
|
798
|
+
expect(
|
|
799
|
+
registry.resolve('/assets/assets/icon.png', 'stale-content-hash')
|
|
800
|
+
).toBeUndefined()
|
|
801
|
+
expect(
|
|
802
|
+
registry.resolve('/assets/../../package.json', assetData.hash)
|
|
803
|
+
).toBeUndefined()
|
|
804
|
+
|
|
805
|
+
const result = await buildNativeBundle({
|
|
806
|
+
root: appRoot,
|
|
807
|
+
platform: 'ios',
|
|
808
|
+
entryFile: 'entry.js',
|
|
809
|
+
assetsDest,
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
expect(result.code).toMatch(/"scales":\s*\[\s*1,\s*2,\s*3\s*\]/)
|
|
813
|
+
for (const file of ['icon.png', 'icon@2x.png', 'icon@3x.png']) {
|
|
814
|
+
expect(existsSync(join(assetsDest, 'assets/assets', file))).toBe(true)
|
|
815
|
+
}
|
|
816
|
+
for (const file of ['back.png', 'back@2x.png']) {
|
|
817
|
+
expect(
|
|
818
|
+
existsSync(join(assetsDest, 'assets/_/_/node_modules/example/assets', file))
|
|
819
|
+
).toBe(true)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// the old path join normalized ../../ out of assetsDest.
|
|
823
|
+
expect(existsSync(join(testRoot, 'node_modules/example/assets/back.png'))).toBe(
|
|
824
|
+
false
|
|
825
|
+
)
|
|
826
|
+
} finally {
|
|
827
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
828
|
+
}
|
|
829
|
+
})
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
describe('native conditional exports', () => {
|
|
833
|
+
it('uses the require condition for CommonJS calls', async () => {
|
|
834
|
+
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-conditions-'))
|
|
835
|
+
const packageRoot = join(testRoot, 'node_modules/conditional-helper')
|
|
836
|
+
await mkdir(packageRoot, { recursive: true })
|
|
837
|
+
await writeFile(
|
|
838
|
+
join(packageRoot, 'package.json'),
|
|
839
|
+
JSON.stringify({
|
|
840
|
+
name: 'conditional-helper',
|
|
841
|
+
exports: {
|
|
842
|
+
'.': {
|
|
843
|
+
import: './esm.js',
|
|
844
|
+
require: './cjs.cjs',
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
type: 'module',
|
|
848
|
+
})
|
|
849
|
+
)
|
|
850
|
+
await writeFile(
|
|
851
|
+
join(packageRoot, 'esm.js'),
|
|
852
|
+
`export default function helper() { return 'import' }`
|
|
853
|
+
)
|
|
854
|
+
await writeFile(
|
|
855
|
+
join(packageRoot, 'cjs.cjs'),
|
|
856
|
+
`module.exports = function helper() { return 'require' }`
|
|
857
|
+
)
|
|
858
|
+
await writeFile(
|
|
859
|
+
join(testRoot, 'entry.cjs'),
|
|
860
|
+
`
|
|
861
|
+
const helper = require('conditional-helper')
|
|
862
|
+
globalThis.__vxrnConditionalExportProbe = helper()
|
|
863
|
+
`
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
try {
|
|
867
|
+
const result = await buildNativeBundle({
|
|
868
|
+
root: testRoot,
|
|
869
|
+
platform: 'ios',
|
|
870
|
+
entryFile: 'entry.cjs',
|
|
871
|
+
dev: true,
|
|
872
|
+
})
|
|
873
|
+
const context = {
|
|
874
|
+
clearTimeout,
|
|
875
|
+
console,
|
|
876
|
+
process: { env: {} },
|
|
877
|
+
setTimeout,
|
|
878
|
+
}
|
|
879
|
+
runInNewContext(result.code, context)
|
|
880
|
+
expect(Reflect.get(context, '__vxrnConditionalExportProbe')).toBe('require')
|
|
881
|
+
} finally {
|
|
882
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
883
|
+
}
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
it(
|
|
887
|
+
'unwraps the default export of Babel CommonJS modules in dev output',
|
|
888
|
+
{ timeout: 30_000 },
|
|
889
|
+
async () => {
|
|
890
|
+
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-cjs-default-'))
|
|
891
|
+
const packageRoot = join(testRoot, 'node_modules/default-export-helper')
|
|
892
|
+
await mkdir(packageRoot, { recursive: true })
|
|
893
|
+
await writeFile(
|
|
894
|
+
join(packageRoot, 'package.json'),
|
|
895
|
+
JSON.stringify({ name: 'default-export-helper', main: './index.js' })
|
|
896
|
+
)
|
|
897
|
+
await writeFile(
|
|
898
|
+
join(packageRoot, 'index.js'),
|
|
899
|
+
`Object.defineProperty(exports, '__esModule', { value: true }); exports.default = function Component() {}`
|
|
900
|
+
)
|
|
901
|
+
await writeFile(
|
|
902
|
+
join(testRoot, 'entry.js'),
|
|
903
|
+
`import Component from 'default-export-helper'; globalThis.__vxrnCjsDefaultProbe = typeof Component`
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
let resolveOutput!: (output: Error | RolldownOutput) => void
|
|
907
|
+
const output = new Promise<Error | RolldownOutput>((resolve) => {
|
|
908
|
+
resolveOutput = resolve
|
|
909
|
+
})
|
|
910
|
+
const engine = await dev(
|
|
911
|
+
{
|
|
912
|
+
cwd: testRoot,
|
|
913
|
+
input: join(testRoot, 'entry.js'),
|
|
914
|
+
experimental: { devMode: { implement: getHmrRuntimeSource() } },
|
|
915
|
+
},
|
|
916
|
+
{ format: 'esm', codeSplitting: false, strictExecutionOrder: true },
|
|
917
|
+
{ onOutput: resolveOutput }
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
try {
|
|
921
|
+
await engine.run()
|
|
922
|
+
const result = await output
|
|
923
|
+
if (result instanceof Error) throw result
|
|
924
|
+
const chunk = result.output.find((item) => item.type === 'chunk' && item.isEntry)
|
|
925
|
+
if (!chunk || chunk.type !== 'chunk') {
|
|
926
|
+
throw new Error('Rolldown did not emit a native dev entry chunk')
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Calibrate the exact Rolldown node-mode form observed in the app: it
|
|
930
|
+
// must expose the Babel exports object before the integration fix.
|
|
931
|
+
const nodeModeCode = chunk.code.replace(
|
|
932
|
+
/(\b__toESM(?:\$\d+)?\(\s*require[\w$]*\(\)\s*)\)/,
|
|
933
|
+
'$1, 1)'
|
|
934
|
+
)
|
|
935
|
+
expect(nodeModeCode).not.toBe(chunk.code)
|
|
936
|
+
|
|
937
|
+
Reflect.deleteProperty(globalThis, '__rolldown_runtime__')
|
|
938
|
+
await import(
|
|
939
|
+
`data:text/javascript;base64,${Buffer.from(nodeModeCode).toString('base64')}`
|
|
940
|
+
)
|
|
941
|
+
expect(Reflect.get(globalThis, '__vxrnCjsDefaultProbe')).toBe('object')
|
|
942
|
+
|
|
943
|
+
Reflect.deleteProperty(globalThis, '__rolldown_runtime__')
|
|
944
|
+
Reflect.deleteProperty(globalThis, '__vxrnCjsDefaultProbe')
|
|
945
|
+
const normalized = normalizeNativeCommonJSInterop(nodeModeCode)
|
|
946
|
+
await import(
|
|
947
|
+
`data:text/javascript;base64,${Buffer.from(normalized).toString('base64')}`
|
|
948
|
+
)
|
|
949
|
+
expect(Reflect.get(globalThis, '__vxrnCjsDefaultProbe')).toBe('function')
|
|
950
|
+
} finally {
|
|
951
|
+
await engine.close()
|
|
952
|
+
Reflect.deleteProperty(globalThis, '__rolldown_runtime__')
|
|
953
|
+
Reflect.deleteProperty(globalThis, '__vxrnCjsDefaultProbe')
|
|
954
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
it('unwraps the default export of Babel CommonJS modules in production output', async () => {
|
|
960
|
+
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-prod-cjs-default-'))
|
|
961
|
+
const packageRoot = join(testRoot, 'node_modules/default-export-helper')
|
|
962
|
+
await mkdir(packageRoot, { recursive: true })
|
|
963
|
+
await writeFile(
|
|
964
|
+
join(packageRoot, 'package.json'),
|
|
965
|
+
JSON.stringify({ name: 'default-export-helper', main: './index.js' })
|
|
966
|
+
)
|
|
967
|
+
await writeFile(
|
|
968
|
+
join(packageRoot, 'index.js'),
|
|
969
|
+
`Object.defineProperty(exports, '__esModule', { value: true }); exports.default = function Component() {}`
|
|
970
|
+
)
|
|
971
|
+
await writeFile(
|
|
972
|
+
join(testRoot, 'entry.js'),
|
|
973
|
+
`import Component from 'default-export-helper'; globalThis.__vxrnProdCjsDefaultProbe = typeof Component`
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
const result = await buildNativeBundle({
|
|
978
|
+
root: testRoot,
|
|
979
|
+
platform: 'ios',
|
|
980
|
+
entryFile: 'entry.js',
|
|
981
|
+
})
|
|
982
|
+
const context = {
|
|
983
|
+
clearTimeout,
|
|
984
|
+
console,
|
|
985
|
+
process: { env: {} },
|
|
986
|
+
setTimeout,
|
|
987
|
+
}
|
|
988
|
+
runInNewContext(result.code, context)
|
|
989
|
+
expect(Reflect.get(context, '__vxrnProdCjsDefaultProbe')).toBe('function')
|
|
990
|
+
} finally {
|
|
991
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
992
|
+
}
|
|
993
|
+
})
|
|
994
|
+
})
|