vxrn 2.8.0 → 2.9.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.
Files changed (52) hide show
  1. package/dist/exports/clean.mjs +128 -19
  2. package/dist/exports/clean.mjs.map +1 -1
  3. package/dist/exports/clean.native.js +147 -21
  4. package/dist/exports/clean.native.js.map +1 -1
  5. package/dist/exports/prebuild.mjs +2 -1
  6. package/dist/exports/prebuild.mjs.map +1 -1
  7. package/dist/exports/prebuild.native.js +2 -1
  8. package/dist/exports/prebuild.native.js.map +1 -1
  9. package/dist/exports/prebuildWithoutExpo.mjs +196 -7
  10. package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
  11. package/dist/exports/prebuildWithoutExpo.native.js +280 -42
  12. package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
  13. package/dist/plugins/reactNativeDevServer.mjs +1 -1
  14. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  15. package/dist/plugins/reactNativeDevServer.native.js +1 -1
  16. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  17. package/dist/user-interface/index.mjs +31 -24
  18. package/dist/user-interface/index.mjs.map +1 -1
  19. package/dist/user-interface/index.native.js +32 -25
  20. package/dist/user-interface/index.native.js.map +1 -1
  21. package/dist/utils/createNativeDevEngine.mjs +111 -14
  22. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  23. package/dist/utils/createNativeDevEngine.native.js +143 -14
  24. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  25. package/dist/utils/scanDepsToOptimize.mjs +10 -6
  26. package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
  27. package/dist/utils/scanDepsToOptimize.native.js +12 -6
  28. package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
  29. package/expo-plugin.cjs +71 -8
  30. package/package.json +13 -13
  31. package/src/exports/clean.test.ts +502 -5
  32. package/src/exports/clean.ts +176 -19
  33. package/src/exports/expoPlugin.test.mjs +44 -1
  34. package/src/exports/prebuild.ts +6 -0
  35. package/src/exports/prebuildWithoutExpo.test.ts +134 -2
  36. package/src/exports/prebuildWithoutExpo.ts +235 -10
  37. package/src/plugins/reactNativeDevServer.ts +1 -1
  38. package/src/user-interface/index.ts +50 -42
  39. package/src/utils/createNativeDevEngine.test.ts +486 -7
  40. package/src/utils/createNativeDevEngine.ts +220 -17
  41. package/src/utils/imageSizePatch.test.ts +3 -3
  42. package/src/utils/scanDepsToOptimize.test.ts +35 -2
  43. package/src/utils/scanDepsToOptimize.ts +25 -8
  44. package/types/exports/clean.d.ts.map +1 -1
  45. package/types/exports/prebuild.d.ts +2 -1
  46. package/types/exports/prebuild.d.ts.map +1 -1
  47. package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
  48. package/types/user-interface/index.d.ts.map +1 -1
  49. package/types/utils/createNativeDevEngine.d.ts +1 -1
  50. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  51. package/types/utils/scanDepsToOptimize.d.ts +25 -0
  52. package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
@@ -1,5 +1,13 @@
1
1
  import { existsSync, realpathSync } from 'node:fs'
2
- import { mkdir, mkdtemp, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises'
2
+ import {
3
+ mkdir,
4
+ mkdtemp,
5
+ readFile,
6
+ rename,
7
+ rm,
8
+ symlink,
9
+ writeFile,
10
+ } from 'node:fs/promises'
3
11
  import { tmpdir } from 'node:os'
4
12
  import { join } from 'node:path'
5
13
  import { createContext, runInContext, runInNewContext } from 'node:vm'
@@ -111,7 +119,7 @@ ${accept}`
111
119
  const pending = nextUpdate()
112
120
  if (change === 'edit') {
113
121
  expect(context.routeEdit).toBe('index')
114
- await writeFile(
122
+ await saveFile(
115
123
  join(root, 'app/index.tsx'),
116
124
  "export default 'edited'; globalThis.routeEdit = 'edited'; if (import.meta.hot) import.meta.hot.accept()"
117
125
  )
@@ -119,7 +127,7 @@ ${accept}`
119
127
  expect(context.routeEdit).toBe('edited')
120
128
  } else if (change === 'create') {
121
129
  expect(context.routeMap['/app/[id].tsx']).toBeUndefined()
122
- await writeFile(join(root, 'app/[id].tsx'), "export default 'dynamic'")
130
+ await saveFile(join(root, 'app/[id].tsx'), "export default 'dynamic'")
123
131
  await apply(pending)
124
132
  expect((await context.routeMap['/app/[id].tsx']()).default).toBe('dynamic')
125
133
  } else {
@@ -825,7 +833,7 @@ if (import.meta.hot) import.meta.hot.accept(() => {})
825
833
 
826
834
  const runtime = rn.__rolldown_runtime__
827
835
  await native.engine.registerClient(runtime.clientId)
828
- await writeFile(dependency, 'export const amount = 3')
836
+ await saveFile(dependency, 'export const amount = 3')
829
837
  const update = await updated
830
838
  expect(update.type).toBe('hmr:update')
831
839
  expect(runtime.applyHmrUpdate(update.code, update.changedIds, update.seq)).toBe(
@@ -1078,7 +1086,7 @@ if (import.meta.hot) import.meta.hot.accept(() => {})
1078
1086
  expect(context.renderComponent()).toBe(42)
1079
1087
  const runtime = context.__rolldown_runtime__
1080
1088
  await native.engine.registerClient(runtime.clientId)
1081
- await writeFile(entry, source(2))
1089
+ await saveFile(entry, source(2))
1082
1090
  const update = await updateReceived
1083
1091
  expect(update.type).toBe('hmr:update')
1084
1092
  expect(update.clientId).toBe(runtime.clientId)
@@ -1275,7 +1283,7 @@ if (import.meta.hot) {
1275
1283
  registeredClientId = runtime.clientId
1276
1284
  await engine.registerClient(runtime.clientId)
1277
1285
 
1278
- await writeFile(entry, source('v2'))
1286
+ await saveFile(entry, source('v2'))
1279
1287
  const result = await Promise.race([
1280
1288
  hmrUpdate,
1281
1289
  new Promise((_, reject) =>
@@ -1388,7 +1396,7 @@ if (import.meta.hot) {
1388
1396
  registeredClientId = runtime.clientId
1389
1397
  await engine.registerClient(runtime.clientId)
1390
1398
 
1391
- await writeFile(leaf, `export const value = 'v2'`)
1399
+ await saveFile(leaf, `export const value = 'v2'`)
1392
1400
  const result = await Promise.race([
1393
1401
  hmrUpdate,
1394
1402
  new Promise((_, reject) =>
@@ -1452,6 +1460,78 @@ describe('native production import.meta lowering', () => {
1452
1460
  })
1453
1461
  })
1454
1462
 
1463
+ describe('native function and class names', () => {
1464
+ it('keeps colliding names in an unminified bundle, the way metro does', async () => {
1465
+ const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-names-'))
1466
+ for (const name of ['a', 'b']) {
1467
+ await writeFile(
1468
+ join(testRoot, `${name}.js`),
1469
+ `export class UnsupportedFeature extends Error {}\nexport function read() {}`
1470
+ )
1471
+ }
1472
+ await writeFile(
1473
+ join(testRoot, 'entry.js'),
1474
+ `import * as a from './a.js'\nimport * as b from './b.js'\nglobalThis.names = [a.UnsupportedFeature.name, b.UnsupportedFeature.name, a.read.name, b.read.name]`
1475
+ )
1476
+
1477
+ try {
1478
+ for (const dev of [true, false]) {
1479
+ const result = await buildNativeBundle({
1480
+ root: testRoot,
1481
+ platform: 'ios',
1482
+ dev,
1483
+ minify: false,
1484
+ entryFile: 'entry.js',
1485
+ })
1486
+ const context: any = { process: { env: {} } }
1487
+ context.globalThis = context
1488
+ runInNewContext(result.code, context)
1489
+ expect(context.names).toEqual(['UnsupportedFeature', 'UnsupportedFeature', 'read', 'read'])
1490
+ }
1491
+ } finally {
1492
+ await rm(testRoot, { recursive: true, force: true })
1493
+ }
1494
+ })
1495
+
1496
+ it('keeps lowered class names in a module that also lowers async', async () => {
1497
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-native-dev-names-'))
1498
+ const packageRoot = join(root, 'node_modules', 'errors-pkg')
1499
+ await mkdir(packageRoot, { recursive: true })
1500
+ await writeFile(join(packageRoot, 'package.json'), JSON.stringify({ main: './index.js' }))
1501
+ // async lowering reprints the lowered classes, renaming each inner
1502
+ // constructor that shadows its top-level export.
1503
+ await writeFile(
1504
+ join(packageRoot, 'index.js'),
1505
+ `class ErrorBase extends Error {}\nexport class UnsupportedFeature extends ErrorBase {}\nexport async function check() {}`
1506
+ )
1507
+ await writeFile(
1508
+ join(root, 'entry.js'),
1509
+ `const { UnsupportedFeature } = require('errors-pkg')\nglobalThis.names = [UnsupportedFeature.name, new UnsupportedFeature().constructor.name]`
1510
+ )
1511
+ const native = await createNativeDevEngine({
1512
+ root,
1513
+ port: 52279,
1514
+ platform: 'ios',
1515
+ plugins: [
1516
+ {
1517
+ name: 'native-names-entry',
1518
+ transform(_code, id) {
1519
+ if (id.endsWith('/__virtual-native-entry.tsx')) return `import './entry.js'`
1520
+ },
1521
+ },
1522
+ ],
1523
+ })
1524
+ try {
1525
+ const context: any = { console, setTimeout, clearTimeout }
1526
+ runInNewContext((await native.getBundle()).code, context)
1527
+ expect(context.names).toEqual(['UnsupportedFeature', 'UnsupportedFeature'])
1528
+ } finally {
1529
+ await native.close()
1530
+ await rm(root, { recursive: true, force: true })
1531
+ }
1532
+ })
1533
+ })
1534
+
1455
1535
  describe('native production react-native deep imports', () => {
1456
1536
  it('resolves react native subpaths that are missing from its exports map', async () => {
1457
1537
  const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-rn-deep-import-'))
@@ -3032,6 +3112,74 @@ globalThis.externalDefaults = [Component, marker, Scoped, Plain, require('extern
3032
3112
  })
3033
3113
  })
3034
3114
 
3115
+ describe('native interop string literals', () => {
3116
+ it('rewrites real __toESM calls while leaving string data alone', () => {
3117
+ const literal = '__toESM(externalModule, 1)'
3118
+ const code = [
3119
+ `const stringData = ${JSON.stringify(literal)}`,
3120
+ 'const templateData = `' + literal + '`',
3121
+ `// ${literal}`,
3122
+ `/* ${literal} */`,
3123
+ 'const viaRequire = __toESM(require_helper(), 1)',
3124
+ 'const viaBinding = __toESM(externalModule, 1)',
3125
+ 'const viaLookup = __toESM(__rolldown_runtime__.loadExports("helper"), 1)',
3126
+ 'const viaRuntime = __rolldown_runtime__.__toESM(__rolldown_runtime__.loadExports("helper"), 1)',
3127
+ 'const renamed = __toESM$2(require_helper(), 1)',
3128
+ 'const spaced = __toESM( require_helper() , 1 )',
3129
+ 'const interpolated = `${__toESM(externalModule, 1)}`',
3130
+ ].join('\n')
3131
+ const normalized = normalizeNativeCommonJSInterop(code)
3132
+ expect(normalized).toContain(`const stringData = ${JSON.stringify(literal)}`)
3133
+ expect(normalized).toContain('const templateData = `' + literal + '`')
3134
+ expect(normalized).toContain(`// ${literal}`)
3135
+ expect(normalized).toContain(`/* ${literal} */`)
3136
+ expect(normalized).toContain('const viaRequire = __toESM(require_helper())')
3137
+ expect(normalized).toContain('const viaBinding = __toESM(externalModule)')
3138
+ expect(normalized).toContain(
3139
+ 'const viaLookup = __toESM(__rolldown_runtime__.loadExports("helper"))'
3140
+ )
3141
+ expect(normalized).toContain(
3142
+ 'const viaRuntime = __rolldown_runtime__.__toESM(__rolldown_runtime__.loadExports("helper"))'
3143
+ )
3144
+ expect(normalized).toContain('const renamed = __toESM$2(require_helper())')
3145
+ expect(normalized).toContain('const spaced = __toESM( require_helper() )')
3146
+ expect(normalized).toContain('const interpolated = `${__toESM(externalModule)}`')
3147
+ })
3148
+
3149
+ it.each([false, true])(
3150
+ 'keeps __toESM string data through buildNativeBundle (minify=%s)',
3151
+ async (minify) => {
3152
+ const literal = '__toESM(externalModule, 1)'
3153
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-interop-literal-'))
3154
+ try {
3155
+ await writeFile(join(root, 'package.json'), JSON.stringify({ type: 'module' }))
3156
+ await writeFile(
3157
+ join(root, 'entry.js'),
3158
+ `globalThis.__interopLiteralProbe = ${JSON.stringify(literal)}\n`
3159
+ )
3160
+ const { code } = await buildNativeBundle({
3161
+ root,
3162
+ platform: 'ios',
3163
+ entryFile: 'entry.js',
3164
+ dev: true,
3165
+ minify,
3166
+ })
3167
+ expect(code).toContain(literal)
3168
+ const context = {
3169
+ clearTimeout,
3170
+ console,
3171
+ process: { env: {} },
3172
+ setTimeout,
3173
+ }
3174
+ runInNewContext(code, context)
3175
+ expect(Reflect.get(context, '__interopLiteralProbe')).toBe(literal)
3176
+ } finally {
3177
+ await rm(root, { recursive: true, force: true })
3178
+ }
3179
+ }
3180
+ )
3181
+ })
3182
+
3035
3183
  describe('native Flow sources', () => {
3036
3184
  it.each([true, false])(
3037
3185
  'preserves codegen, worklets and maps with dev=%s',
@@ -3335,6 +3483,56 @@ describe('postProcessNativeBundle', () => {
3335
3483
  })
3336
3484
  })
3337
3485
 
3486
+ describe('native dev bundle post-processing failures', () => {
3487
+ it('rejects the bundle request with the failure instead of timing out', async () => {
3488
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-native-postprocess-error-'))
3489
+ await writeFile(join(root, 'package.json'), JSON.stringify({ type: 'module' }))
3490
+ await writeFile(join(root, 'entry.js'), `globalThis.__postprocessProbe = 1\n`)
3491
+ const native = await createNativeDevEngine({
3492
+ root,
3493
+ port: 0,
3494
+ platform: 'ios',
3495
+ plugins: [
3496
+ {
3497
+ name: 'postprocess-error-entry',
3498
+ transform(code, id) {
3499
+ if (id.endsWith('/__virtual-native-entry.tsx')) return `import './entry.js'`
3500
+ },
3501
+ },
3502
+ {
3503
+ name: 'postprocess-error-poison',
3504
+ renderChunk() {
3505
+ // valid javascript, so rolldown delivers it: the single-line
3506
+ // class fields inside the runtime region gain lines when
3507
+ // downleveled, which throws inside handleOutput.
3508
+ return {
3509
+ code: [
3510
+ '//#region \\0rolldown/runtime.js',
3511
+ 'class Poison { x = 1; y = 2; z = 3; }',
3512
+ '//#endregion',
3513
+ 'globalThis.__postprocessProbe = new Poison();',
3514
+ '',
3515
+ ].join('\n'),
3516
+ }
3517
+ },
3518
+ },
3519
+ ],
3520
+ })
3521
+ try {
3522
+ await expect(native.getBundle()).rejects.toThrow(
3523
+ 'Hermes runtime transform added lines'
3524
+ )
3525
+ // the failure is recorded for later callers, not just the waiter
3526
+ await expect(native.getBundle()).rejects.toThrow(
3527
+ 'Hermes runtime transform added lines'
3528
+ )
3529
+ } finally {
3530
+ await native.close()
3531
+ await rm(root, { recursive: true, force: true })
3532
+ }
3533
+ })
3534
+ })
3535
+
3338
3536
  describe('native package context identity', () => {
3339
3537
  it.each(['@tamagui/web', '@tamagui/adapt'])(
3340
3538
  'shares %s contexts with dependencies containing a second package copy',
@@ -3470,3 +3668,284 @@ describe('native zero-client edit recoverability', () => {
3470
3668
  }
3471
3669
  )
3472
3670
  })
3671
+
3672
+ describe('native mixed ESM and CommonJS exports', () => {
3673
+ it.each([false, true])(
3674
+ 'preserves replacement exports for require and import (dev=%s)',
3675
+ async (dev) => {
3676
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-native-mixed-'))
3677
+ const dep = join(root, 'node_modules/mixed-native')
3678
+ await mkdir(dep, { recursive: true })
3679
+ await writeFile(
3680
+ join(dep, 'package.json'),
3681
+ JSON.stringify({ name: 'mixed-native', 'react-native': 'index.ts' })
3682
+ )
3683
+ await writeFile(
3684
+ join(dep, 'api.ts'),
3685
+ "export const value = 'native'; export const discarded = 'esm-only'"
3686
+ )
3687
+ await writeFile(
3688
+ join(dep, 'index.ts'),
3689
+ `
3690
+ import { value } from './api'
3691
+ export * from './api'
3692
+ export const install = () => value
3693
+ const api: { value: string; default?: unknown; install?: unknown } = { value }
3694
+ export default api
3695
+ module.exports = api
3696
+ module.exports.default = api
3697
+ module.exports.install = install
3698
+ `
3699
+ )
3700
+ await writeFile(
3701
+ join(root, 'entry.js'),
3702
+ `
3703
+ import api, { install, value, discarded } from 'mixed-native'
3704
+ const required = require('mixed-native')
3705
+ globalThis.mixedProbe = {
3706
+ same: api === required,
3707
+ selfDefault: required.default === required,
3708
+ installed: install(),
3709
+ value,
3710
+ discarded,
3711
+ keys: Object.keys(required).sort(),
3712
+ }
3713
+ `
3714
+ )
3715
+ try {
3716
+ const bundle = await buildNativeBundle({
3717
+ root,
3718
+ platform: 'ios',
3719
+ entryFile: 'entry.js',
3720
+ dev,
3721
+ })
3722
+ const context: any = { console }
3723
+ runInNewContext(bundle.code, context)
3724
+
3725
+ // metro's react native preset lowers all imports and exports before cjs runs.
3726
+ const { transformAsync } = await import('@babel/core')
3727
+ const metroContext: any = { console }
3728
+ const modules = new Map<string, any>()
3729
+ const sources = new Map<string, string>()
3730
+ for (const [name, file] of [
3731
+ ['mixed-native', join(dep, 'index.ts')],
3732
+ ['./api', join(dep, 'api.ts')],
3733
+ ['entry', join(root, 'entry.js')],
3734
+ ]) {
3735
+ const transformed = await transformAsync(await readFile(file!, 'utf8'), {
3736
+ filename: file,
3737
+ babelrc: false,
3738
+ configFile: false,
3739
+ presets: ['module:@react-native/babel-preset'],
3740
+ })
3741
+ sources.set(name!, transformed!.code!)
3742
+ }
3743
+ const metroRequire = (name: string): any => {
3744
+ if (modules.has(name)) return modules.get(name).exports
3745
+ const module = { exports: {} }
3746
+ modules.set(name, module)
3747
+ runInNewContext(
3748
+ '(function(module, exports, require) {' + sources.get(name) + '\n})',
3749
+ metroContext
3750
+ )(module, module.exports, metroRequire)
3751
+ return module.exports
3752
+ }
3753
+ metroRequire('entry')
3754
+ expect(context.mixedProbe).toEqual(metroContext.mixedProbe)
3755
+ expect(context.mixedProbe).toEqual({
3756
+ same: true,
3757
+ selfDefault: true,
3758
+ installed: 'native',
3759
+ value: 'native',
3760
+ discarded: undefined,
3761
+ keys: ['default', 'install', 'value'],
3762
+ })
3763
+ } finally {
3764
+ await rm(root, { recursive: true, force: true })
3765
+ }
3766
+ }
3767
+ )
3768
+ })
3769
+
3770
+ it.each([
3771
+ 'export const value = "module.exports = ignored"',
3772
+ 'export const module = { exports: 1 }; module.exports = 2',
3773
+ 'export function local(module) { module.exports = 1 }',
3774
+ 'export function local(exports) { exports.value = 1 }',
3775
+ 'module.exports = 1',
3776
+ ])('leaves non-mixed sources alone: %s', async (source) => {
3777
+ const { transformMixedNativeModule } = await import('@vxrn/compiler')
3778
+ expect(await transformMixedNativeModule(source, 'probe.js')).toBeUndefined()
3779
+ })
3780
+
3781
+ it('preserves computed CommonJS assignments and their source map', async () => {
3782
+ const { transformMixedNativeModule } = await import('@vxrn/compiler')
3783
+ const source = "export const dropped = true;\nmodule['exports'] = { value: 42 };\n"
3784
+ const result = await transformMixedNativeModule(source, 'probe.js', true)
3785
+ const module = { exports: {} }
3786
+ runInNewContext(result!.code, { module, exports: module.exports })
3787
+ expect(module.exports).toEqual({ value: 42 })
3788
+ const lines = result!.code.split('\n')
3789
+ const line = lines.findIndex((line) => line.includes('42'))
3790
+ expect(
3791
+ originalPositionFor(new TraceMap(result!.map), {
3792
+ line: line + 1,
3793
+ column: lines[line]!.indexOf('42'),
3794
+ })
3795
+ ).toMatchObject({ source: 'probe.js', line: 2 })
3796
+ })
3797
+
3798
+ it('keeps Babel export properties writable in mixed modules', async () => {
3799
+ const { transformMixedNativeModule } = await import('@vxrn/compiler')
3800
+ const result = await transformMixedNativeModule(
3801
+ 'export let value = 1; exports.value = 2; export const lexical = () => value',
3802
+ 'probe.js'
3803
+ )
3804
+ const module = { exports: {} as any }
3805
+ runInNewContext(result!.code, { module, exports: module.exports })
3806
+ expect(module.exports.value).toBe(2)
3807
+ expect(module.exports.lexical()).toBe(1)
3808
+ })
3809
+
3810
+ it.each([
3811
+ 'export const value = this; module.exports = { valueIsObject: typeof value === "object", strict: (function () { return this === undefined })() };',
3812
+ '"use strict"; export const value = this; module.exports = { valueIsObject: typeof value === "object", strict: (function () { return this === undefined })() };',
3813
+ 'export const value = 42; Object.assign(exports, { attached: value });',
3814
+ 'export const value = 42; const out = exports; out.attached = value;',
3815
+ 'export const value = 42; const out = module; out.exports.attached = value;',
3816
+ 'export let value = 1; exports.value = 10; value++;',
3817
+ 'export let value = 1; export { value as alias }; exports.value = 10; value++;',
3818
+ 'export let value = 1, next = exports.value; exports.attached = next;',
3819
+ 'export let value = 1; exports.value = 10; value ||= 2;',
3820
+ 'export default function () {}; exports.default = 42;',
3821
+ 'export default function named() {}; exports.default = 42;',
3822
+ 'export default class Named {}; exports.default = 42;',
3823
+ 'export default 1; exports.default = 42;',
3824
+ 'export let value = 1; export const nested = () => { let value = 10; value++; return value }; exports.attached = exports.nested();',
3825
+ 'export let value = 1; export const previous = value++; exports.attached = previous;',
3826
+ 'export let value = 1; ({ value } = { value: 42 }); exports.attached = value;',
3827
+ 'export let value = 1; export const increment = () => value++; exports.increment();',
3828
+ 'let value = 1; export { value as renamed }; value += 2; exports.attached = value;',
3829
+ ])('matches Metro wrapper semantics: %s', async (source) => {
3830
+ const { transformMixedNativeModule } = await import('@vxrn/compiler')
3831
+ const { transformAsync } = await import('@babel/core')
3832
+ const actual = await transformMixedNativeModule(source, 'probe.js')
3833
+ const expected = await transformAsync(source, {
3834
+ filename: 'probe.js',
3835
+ babelrc: false,
3836
+ configFile: false,
3837
+ presets: ['module:@react-native/babel-preset'],
3838
+ })
3839
+ const evaluate = (code: string) => {
3840
+ const module = { exports: {} as any }
3841
+ runInNewContext(
3842
+ '(function(module, exports) {' + code + '\n})(module, exports)',
3843
+ {
3844
+ module,
3845
+ exports: module.exports,
3846
+ }
3847
+ )
3848
+ return JSON.parse(JSON.stringify(module.exports))
3849
+ }
3850
+ expect(actual).toBeDefined()
3851
+ expect(evaluate(actual!.code)).toEqual(evaluate(expected!.code!))
3852
+ })
3853
+
3854
+ it.each([
3855
+ 'export function local(exports) { return Object.assign(exports, { value: 1 }) }',
3856
+ 'export function local(module) { return module }',
3857
+ 'export const obj = { exports: 1, module: 2 }',
3858
+ 'export function local() { module.exports = 1; var module }',
3859
+ 'export function local({ exports }) { return exports }',
3860
+ 'export function local() { try {} catch (exports) { exports.value = 1 } }',
3861
+ 'export function local() { for (const exports of []) { exports.value = 1 } }',
3862
+ ])('keeps lexical CommonJS shadows unchanged: %s', async (source) => {
3863
+ const { transformMixedNativeModule } = await import('@vxrn/compiler')
3864
+ expect(await transformMixedNativeModule(source, 'probe.js')).toBeUndefined()
3865
+ })
3866
+ it.each([
3867
+ [
3868
+ 'probe.js',
3869
+ 'export let value=1; export function increment(exports){value++;} increment({}); module.exports={published:exports.value,lexical:value};',
3870
+ ],
3871
+ [
3872
+ 'probe.js',
3873
+ 'export let value=1; for(value of [2,3]) {} module.exports={published:exports.value,lexical:value};',
3874
+ ],
3875
+ [
3876
+ 'probe.js',
3877
+ 'export let value=""; for(value in {a:1,b:2}) {} module.exports={published:exports.value,lexical:value};',
3878
+ ],
3879
+ [
3880
+ 'probe.js',
3881
+ 'export let value=1; for(value of [2,3]) if(value===2) continue; module.exports={published:exports.value,lexical:value};',
3882
+ ],
3883
+ [
3884
+ 'probe.js',
3885
+ 'export let value=1; for(const value of [2,3]) {} module.exports={published:exports.value,lexical:value};',
3886
+ ],
3887
+ [
3888
+ 'probe.js',
3889
+ 'export let value=1; export function increment(module, exports){value++;} increment({},{}); module.exports={published:exports.value,lexical:value};',
3890
+ ],
3891
+ [
3892
+ 'probe.js',
3893
+ 'export let value=1; exports={}; value++; module.exports={published:exports.value,lexical:value};',
3894
+ ],
3895
+ [
3896
+ 'probe.ts',
3897
+ 'export enum Color { Red }; exports.Color=42; module.exports={value:exports.Color};',
3898
+ ],
3899
+ [
3900
+ 'probe.ts',
3901
+ 'export namespace Color { export const Red = 1 }; exports.Color=42; module.exports={value:exports.Color};',
3902
+ ],
3903
+ [
3904
+ 'probe.ts',
3905
+ 'export const value: unknown = this; module.exports={valueIsObject:typeof value === "object",strict:(function(){return this === undefined})()};',
3906
+ ],
3907
+ [
3908
+ 'probe.tsx',
3909
+ 'export enum Color { Red }; exports.Color=42; module.exports={value:exports.Color};',
3910
+ ],
3911
+ ])(
3912
+ 'matches Metro writable binding semantics in %s: %s',
3913
+ async (filename, source) => {
3914
+ const { transformMixedNativeModule } = await import('@vxrn/compiler')
3915
+ const { transformAsync } = await import('@babel/core')
3916
+ const actual = await transformMixedNativeModule(source, filename)
3917
+ const expected = await transformAsync(source, {
3918
+ filename,
3919
+ babelrc: false,
3920
+ configFile: false,
3921
+ presets: ['module:@react-native/babel-preset'],
3922
+ })
3923
+ const evaluate = (code: string) => {
3924
+ const module = { exports: {} }
3925
+ runInNewContext(
3926
+ '(function(module, exports) {' + code + '\n})(module, exports)',
3927
+ {
3928
+ module,
3929
+ exports: module.exports,
3930
+ }
3931
+ )
3932
+ return JSON.parse(JSON.stringify(module.exports))
3933
+ }
3934
+ expect(actual).toBeDefined()
3935
+ expect(evaluate(actual!.code)).toEqual(evaluate(expected!.code!))
3936
+ }
3937
+ )
3938
+
3939
+ it('composes TypeScript runtime declaration maps to the original source', async () => {
3940
+ const { transformMixedNativeModule } = await import('@vxrn/compiler')
3941
+ const source = 'export enum Color { Red };\nexports.Color = 42;\n'
3942
+ const result = await transformMixedNativeModule(source, 'probe.ts', true)
3943
+ const lines = result!.code.split('\n')
3944
+ const line = lines.findIndex((line) => line.includes('42'))
3945
+ expect(
3946
+ originalPositionFor(new TraceMap(result!.map), {
3947
+ line: line + 1,
3948
+ column: lines[line]!.indexOf('42'),
3949
+ })
3950
+ ).toMatchObject({ source: 'probe.ts', line: 2 })
3951
+ })