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.
Files changed (45) hide show
  1. package/dist/config/getViteServerConfig.mjs +6 -0
  2. package/dist/config/getViteServerConfig.mjs.map +1 -1
  3. package/dist/config/getViteServerConfig.native.js +6 -0
  4. package/dist/config/getViteServerConfig.native.js.map +1 -1
  5. package/dist/config/getViteServerConfig.test.mjs +26 -0
  6. package/dist/config/getViteServerConfig.test.mjs.map +1 -0
  7. package/dist/config/getViteServerConfig.test.native.js +26 -0
  8. package/dist/config/getViteServerConfig.test.native.js.map +1 -0
  9. package/dist/exports/dev.mjs +26 -1
  10. package/dist/exports/dev.mjs.map +1 -1
  11. package/dist/exports/dev.native.js +26 -1
  12. package/dist/exports/dev.native.js.map +1 -1
  13. package/dist/plugins/reactNativeDevServer.mjs +80 -7
  14. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  15. package/dist/plugins/reactNativeDevServer.native.js +80 -7
  16. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  17. package/dist/runtime/native-prelude.mjs +7 -0
  18. package/dist/runtime/native-prelude.mjs.map +1 -1
  19. package/dist/runtime/native-prelude.native.js +7 -0
  20. package/dist/runtime/native-prelude.native.js.map +1 -1
  21. package/dist/utils/createNativeDevEngine.mjs +302 -197
  22. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  23. package/dist/utils/createNativeDevEngine.native.js +410 -221
  24. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  25. package/dist/utils/createNativeDevEngine.test.mjs +536 -6
  26. package/dist/utils/createNativeDevEngine.test.mjs.map +1 -1
  27. package/dist/utils/createNativeDevEngine.test.native.js +559 -6
  28. package/dist/utils/createNativeDevEngine.test.native.js.map +1 -1
  29. package/package.json +11 -11
  30. package/src/config/getViteServerConfig.test.ts +29 -0
  31. package/src/config/getViteServerConfig.ts +11 -0
  32. package/src/exports/dev.ts +34 -3
  33. package/src/plugins/reactNativeDevServer.ts +89 -12
  34. package/src/runtime/native-prelude.ts +7 -0
  35. package/src/utils/createNativeDevEngine.test.ts +682 -5
  36. package/src/utils/createNativeDevEngine.ts +516 -229
  37. package/types/config/getViteServerConfig.d.ts.map +1 -1
  38. package/types/config/getViteServerConfig.test.d.ts +2 -0
  39. package/types/config/getViteServerConfig.test.d.ts.map +1 -0
  40. package/types/exports/dev.d.ts.map +1 -1
  41. package/types/plugins/reactNativeDevServer.d.ts +1 -0
  42. package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
  43. package/types/runtime/native-prelude.d.ts.map +1 -1
  44. package/types/utils/createNativeDevEngine.d.ts +40 -2
  45. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
@@ -1,19 +1,81 @@
1
- import { mkdtemp, rm, writeFile } from 'node:fs/promises'
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 { rolldown } from 'rolldown'
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
- expect(runtime.applyHmrUpdate(patch.code, patch.changedIds, patch.seq)).toBe(true)
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 only in production', () => {
191
- expect(getHermesSWCIncludes(true)).not.toContain('transform-async-to-generator')
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
+ })