vxrn 2.0.0-beta.98.1 → 2.6.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 (210) hide show
  1. package/dist/backgroundComputation.mjs +145 -0
  2. package/dist/backgroundComputation.mjs.map +1 -0
  3. package/dist/backgroundComputation.native.js +163 -0
  4. package/dist/backgroundComputation.native.js.map +1 -0
  5. package/dist/backgroundComputationNative.mjs +16 -0
  6. package/dist/backgroundComputationNative.mjs.map +1 -0
  7. package/dist/backgroundComputationNative.native.js +18 -0
  8. package/dist/backgroundComputationNative.native.js.map +1 -0
  9. package/dist/config/getBaseViteConfigOnly.mjs +6 -10
  10. package/dist/config/getBaseViteConfigOnly.mjs.map +1 -1
  11. package/dist/config/getBaseViteConfigOnly.native.js +6 -10
  12. package/dist/config/getBaseViteConfigOnly.native.js.map +1 -1
  13. package/dist/config/getOptimizeDeps.mjs +2 -1
  14. package/dist/config/getOptimizeDeps.mjs.map +1 -1
  15. package/dist/config/getOptimizeDeps.native.js +2 -1
  16. package/dist/config/getOptimizeDeps.native.js.map +1 -1
  17. package/dist/config/getViteServerConfig.mjs +13 -0
  18. package/dist/config/getViteServerConfig.mjs.map +1 -1
  19. package/dist/config/getViteServerConfig.native.js +15 -1
  20. package/dist/config/getViteServerConfig.native.js.map +1 -1
  21. package/dist/constants/defaults.mjs +4 -0
  22. package/dist/constants/defaults.mjs.map +1 -1
  23. package/dist/constants/defaults.native.js +4 -0
  24. package/dist/constants/defaults.native.js.map +1 -1
  25. package/dist/exports/build.native.js +1 -1
  26. package/dist/exports/build.native.js.map +1 -1
  27. package/dist/exports/clean.native.js +1 -1
  28. package/dist/exports/clean.native.js.map +1 -1
  29. package/dist/exports/createServer.native.js +2 -2
  30. package/dist/exports/createServer.native.js.map +1 -1
  31. package/dist/exports/dev.mjs +3 -0
  32. package/dist/exports/dev.mjs.map +1 -1
  33. package/dist/exports/dev.native.js +6 -3
  34. package/dist/exports/dev.native.js.map +1 -1
  35. package/dist/exports/loadEnv.native.js +1 -1
  36. package/dist/exports/loadEnv.native.js.map +1 -1
  37. package/dist/exports/patch.native.js +1 -1
  38. package/dist/exports/patch.native.js.map +1 -1
  39. package/dist/exports/prebuild.mjs +0 -22
  40. package/dist/exports/prebuild.mjs.map +1 -1
  41. package/dist/exports/prebuild.native.js +1 -23
  42. package/dist/exports/prebuild.native.js.map +1 -1
  43. package/dist/exports/prebuildWithoutExpo.mjs +1133 -182
  44. package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
  45. package/dist/exports/prebuildWithoutExpo.native.js +1606 -244
  46. package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
  47. package/dist/exports/runAndroid.native.js +1 -1
  48. package/dist/exports/runAndroid.native.js.map +1 -1
  49. package/dist/exports/runIos.native.js +1 -1
  50. package/dist/exports/runIos.native.js.map +1 -1
  51. package/dist/exports/serve.native.js +1 -1
  52. package/dist/exports/serve.native.js.map +1 -1
  53. package/dist/exports/serveStaticAssets.native.js +1 -1
  54. package/dist/exports/serveStaticAssets.native.js.map +1 -1
  55. package/dist/index.js +4 -1
  56. package/dist/index.mjs +4 -1
  57. package/dist/index.native.js +4 -1
  58. package/dist/native-source-contract.mjs +3 -0
  59. package/dist/native-source-contract.native.js +3 -0
  60. package/dist/patches/builtInDepPatches.mjs +0 -13
  61. package/dist/patches/builtInDepPatches.mjs.map +1 -1
  62. package/dist/patches/builtInDepPatches.native.js +0 -13
  63. package/dist/patches/builtInDepPatches.native.js.map +1 -1
  64. package/dist/plugins/autoDepOptimizePlugin.mjs +3 -2
  65. package/dist/plugins/autoDepOptimizePlugin.mjs.map +1 -1
  66. package/dist/plugins/autoDepOptimizePlugin.native.js +6 -3
  67. package/dist/plugins/autoDepOptimizePlugin.native.js.map +1 -1
  68. package/dist/plugins/backgroundComputationPlugin.mjs +27 -0
  69. package/dist/plugins/backgroundComputationPlugin.mjs.map +1 -0
  70. package/dist/plugins/backgroundComputationPlugin.native.js +32 -0
  71. package/dist/plugins/backgroundComputationPlugin.native.js.map +1 -0
  72. package/dist/plugins/reactNativeCommonJsPlugin.native.js +3 -3
  73. package/dist/plugins/reactNativeCommonJsPlugin.native.js.map +1 -1
  74. package/dist/plugins/reactNativeDevAssetPlugin.mjs +2 -1
  75. package/dist/plugins/reactNativeDevAssetPlugin.mjs.map +1 -1
  76. package/dist/plugins/reactNativeDevAssetPlugin.native.js +3 -2
  77. package/dist/plugins/reactNativeDevAssetPlugin.native.js.map +1 -1
  78. package/dist/plugins/reactNativeDevServer.mjs +87 -24
  79. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  80. package/dist/plugins/reactNativeDevServer.native.js +162 -45
  81. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  82. package/dist/plugins/workletImportsPlugin.mjs +9 -30
  83. package/dist/plugins/workletImportsPlugin.mjs.map +1 -1
  84. package/dist/plugins/workletImportsPlugin.native.js +10 -35
  85. package/dist/plugins/workletImportsPlugin.native.js.map +1 -1
  86. package/dist/rn-commands/bundle/buildBundle.mjs +3 -3
  87. package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
  88. package/dist/rn-commands/bundle/buildBundle.native.js +3 -3
  89. package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
  90. package/dist/runtime/hmr-client.native.js +3 -7
  91. package/dist/runtime/hmr-client.native.js.map +1 -1
  92. package/dist/runtime/hmr-runtime.native.js +32 -40
  93. package/dist/runtime/hmr-runtime.native.js.map +1 -1
  94. package/dist/serve/node.native.js +2 -2
  95. package/dist/serve/node.native.js.map +1 -1
  96. package/dist/swift-package-id.mjs +3 -0
  97. package/dist/swift-package-id.native.js +3 -0
  98. package/dist/user-interface/index.native.js +6 -6
  99. package/dist/user-interface/index.native.js.map +1 -1
  100. package/dist/utils/compileNativeDevBytecode.mjs +93 -0
  101. package/dist/utils/compileNativeDevBytecode.mjs.map +1 -0
  102. package/dist/utils/compileNativeDevBytecode.native.js +98 -0
  103. package/dist/utils/compileNativeDevBytecode.native.js.map +1 -0
  104. package/dist/utils/createNativeDevEngine.mjs +227 -20
  105. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  106. package/dist/utils/createNativeDevEngine.native.js +249 -37
  107. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  108. package/dist/utils/execSync.native.js +1 -1
  109. package/dist/utils/execSync.native.js.map +1 -1
  110. package/dist/utils/getServerEntry.native.js +2 -2
  111. package/dist/utils/getServerEntry.native.js.map +1 -1
  112. package/dist/utils/nativeRun.mjs +32 -16
  113. package/dist/utils/nativeRun.mjs.map +1 -1
  114. package/dist/utils/nativeRun.native.js +43 -18
  115. package/dist/utils/nativeRun.native.js.map +1 -1
  116. package/dist/utils/nativeSourceContract.mjs +1823 -0
  117. package/dist/utils/nativeSourceContract.mjs.map +1 -0
  118. package/dist/utils/nativeSourceContract.native.js +2425 -0
  119. package/dist/utils/nativeSourceContract.native.js.map +1 -0
  120. package/dist/utils/patches.native.js +15 -24
  121. package/dist/utils/patches.native.js.map +1 -1
  122. package/dist/utils/printServerUrls.native.js +1 -1
  123. package/dist/utils/printServerUrls.native.js.map +1 -1
  124. package/dist/utils/scanDepsToOptimize.mjs +7 -1
  125. package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
  126. package/dist/utils/scanDepsToOptimize.native.js +27 -2
  127. package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
  128. package/dist/utils/swiftPackageId.mjs +31 -2
  129. package/dist/utils/swiftPackageId.mjs.map +1 -1
  130. package/dist/utils/swiftPackageId.native.js +50 -2
  131. package/dist/utils/swiftPackageId.native.js.map +1 -1
  132. package/dist/vxrn-vite-plugin.native.js +1 -1
  133. package/dist/vxrn-vite-plugin.native.js.map +1 -1
  134. package/expo-plugin.cjs +792 -2
  135. package/native-project-patches.cjs +201 -10
  136. package/package.json +34 -15
  137. package/src/backgroundComputation.ts +220 -0
  138. package/src/backgroundComputationNative.ts +23 -0
  139. package/src/config/getBaseViteConfigOnly.ts +8 -9
  140. package/src/config/getOptimizeDeps.ts +3 -1
  141. package/src/config/getViteServerConfig.ts +16 -0
  142. package/src/config/glbAssets.test.ts +228 -0
  143. package/src/constants/defaults.ts +6 -0
  144. package/src/exports/dev.ts +5 -1
  145. package/src/exports/expoPlugin.test.mjs +337 -15
  146. package/src/exports/prebuild.ts +0 -31
  147. package/src/exports/prebuildWithoutExpo.test.ts +1623 -46
  148. package/src/exports/prebuildWithoutExpo.ts +1606 -216
  149. package/src/index.ts +8 -0
  150. package/src/native-source-contract.ts +16 -0
  151. package/src/patches/builtInDepPatches.ts +0 -21
  152. package/src/plugins/autoDepOptimizePlugin.ts +7 -2
  153. package/src/plugins/backgroundComputationPlugin.test.ts +77 -0
  154. package/src/plugins/backgroundComputationPlugin.ts +29 -0
  155. package/src/plugins/reactNativeDevAssetPlugin.ts +5 -1
  156. package/src/plugins/reactNativeDevServer.test.ts +74 -0
  157. package/src/plugins/reactNativeDevServer.ts +145 -28
  158. package/src/plugins/rnCodegenPlugin.test.ts +30 -0
  159. package/src/plugins/workletImportsPlugin.ts +19 -37
  160. package/src/rn-commands/bundle/buildBundle.ts +5 -3
  161. package/src/swift-package-id.ts +1 -0
  162. package/src/utils/compileNativeDevBytecode.test.ts +168 -0
  163. package/src/utils/compileNativeDevBytecode.ts +117 -0
  164. package/src/utils/createNativeDevEngine.test.ts +643 -18
  165. package/src/utils/createNativeDevEngine.ts +278 -48
  166. package/src/utils/nativeRun.test.ts +125 -23
  167. package/src/utils/nativeRun.ts +51 -23
  168. package/src/utils/nativeSourceArtifacts.test.ts +39 -0
  169. package/src/utils/nativeSourceContract.test.ts +600 -0
  170. package/src/utils/nativeSourceContract.ts +1897 -0
  171. package/src/utils/scanDepsToOptimize.test.ts +80 -0
  172. package/src/utils/scanDepsToOptimize.ts +18 -7
  173. package/src/utils/swiftPackageId.ts +25 -1
  174. package/types/backgroundComputation.d.ts +28 -0
  175. package/types/backgroundComputation.d.ts.map +1 -0
  176. package/types/backgroundComputationNative.d.ts +2 -0
  177. package/types/backgroundComputationNative.d.ts.map +1 -0
  178. package/types/config/getBaseViteConfigOnly.d.ts.map +1 -1
  179. package/types/config/getOptimizeDeps.d.ts.map +1 -1
  180. package/types/config/getViteServerConfig.d.ts.map +1 -1
  181. package/types/constants/defaults.d.ts.map +1 -1
  182. package/types/exports/dev.d.ts.map +1 -1
  183. package/types/exports/prebuild.d.ts.map +1 -1
  184. package/types/exports/prebuildWithoutExpo.d.ts +6 -0
  185. package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
  186. package/types/index.d.ts +2 -0
  187. package/types/index.d.ts.map +1 -1
  188. package/types/native-source-contract.d.ts +2 -0
  189. package/types/native-source-contract.d.ts.map +1 -0
  190. package/types/patches/builtInDepPatches.d.ts.map +1 -1
  191. package/types/plugins/autoDepOptimizePlugin.d.ts.map +1 -1
  192. package/types/plugins/backgroundComputationPlugin.d.ts +3 -0
  193. package/types/plugins/backgroundComputationPlugin.d.ts.map +1 -0
  194. package/types/plugins/reactNativeDevAssetPlugin.d.ts.map +1 -1
  195. package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
  196. package/types/plugins/workletImportsPlugin.d.ts.map +1 -1
  197. package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
  198. package/types/swift-package-id.d.ts +2 -0
  199. package/types/swift-package-id.d.ts.map +1 -0
  200. package/types/utils/compileNativeDevBytecode.d.ts +3 -0
  201. package/types/utils/compileNativeDevBytecode.d.ts.map +1 -0
  202. package/types/utils/createNativeDevEngine.d.ts +7 -22
  203. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  204. package/types/utils/nativeRun.d.ts.map +1 -1
  205. package/types/utils/nativeSourceContract.d.ts +121 -0
  206. package/types/utils/nativeSourceContract.d.ts.map +1 -0
  207. package/types/utils/scanDepsToOptimize.d.ts +1 -0
  208. package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
  209. package/types/utils/swiftPackageId.d.ts +1 -0
  210. package/types/utils/swiftPackageId.d.ts.map +1 -1
package/src/index.ts CHANGED
@@ -27,5 +27,13 @@ export * from './plugins/rollupRemoveUnusedImports'
27
27
  export * from './plugins/autoDepOptimizePlugin'
28
28
  export * from './plugins/rnCodegenPlugin'
29
29
  export * from './plugins/workletImportsPlugin'
30
+ export * from './plugins/backgroundComputationPlugin'
30
31
 
31
32
  export * from './types'
33
+
34
+ export {
35
+ transformBackgroundComputations,
36
+ loadBackgroundComputationModule,
37
+ createBackgroundWorkletModule,
38
+ assertBackgroundDependency,
39
+ } from './backgroundComputation'
@@ -0,0 +1,16 @@
1
+ export {
2
+ nativeSourceContract,
3
+ writeNativeSourceDeclaration,
4
+ writeNativeSourceDeclarations,
5
+ renderSwiftSourceGlue,
6
+ kotlinSourceId,
7
+ renderKotlinSourceGlue,
8
+ swiftPodManifest,
9
+ writeSwiftPackageArtifacts,
10
+ type NativeSourceContract,
11
+ type NativeSourceMethod,
12
+ type NativeSourceModule,
13
+ type NativeSourceView,
14
+ type NativeSourceViewProp,
15
+ type SwiftPackageArtifacts,
16
+ } from './utils/nativeSourceContract'
@@ -469,25 +469,4 @@ ${contents}
469
469
  },
470
470
  },
471
471
  },
472
- // react native 0.87 made ReactContextBaseJavaModule a Kotlin class, so the
473
- // bare `currentActivity` property in @react-navigation/native's MaterialSymbolModule
474
- // no longer resolves and the Android build fails to compile. upstream alpha.45
475
- // changed exactly this line; the monorepo stays on alpha.44 because alpha.45
476
- // also pulls a @react-navigation/core alpha that renames Tabs APIs.
477
- {
478
- module: '@react-navigation/native',
479
- patchFiles: {
480
- version: '8.0.0-alpha.44',
481
- 'android/src/main/java/org/reactnavigation/MaterialSymbolModule.kt': (contents) => {
482
- assertString(contents)
483
- bailIfExists(contents, 'reactApplicationContext.currentActivity')
484
- const patched = contents.replace(
485
- 'colorValue, currentActivity ?: reactApplicationContext',
486
- 'colorValue, reactApplicationContext.currentActivity ?: reactApplicationContext'
487
- )
488
- bailIfUnchanged(patched, contents)
489
- return patched
490
- },
491
- },
492
- },
493
472
  ]
@@ -89,7 +89,10 @@ export async function getScannedOptimizeDepsConfig(props: FindDepsOptionsByMode)
89
89
  include: filteredDeps,
90
90
  exclude: EXCLUDE_LIST,
91
91
  },
92
- noExternal: filteredDeps,
92
+ noExternal: [
93
+ ...filteredDeps,
94
+ ...result.noExternalDeps.filter((dep) => !excludeSet.has(dep)),
95
+ ],
93
96
  },
94
97
  }
95
98
  }
@@ -119,6 +122,7 @@ export async function findDepsToOptimize({
119
122
  'yarn.lock',
120
123
  'package-lock.json',
121
124
  'pnpm-lock.yaml',
125
+ 'bun.lock',
122
126
  'bun.lockb',
123
127
  ])
124
128
 
@@ -136,7 +140,8 @@ export async function findDepsToOptimize({
136
140
  if (
137
141
  lockFileHash === cachedLockFileHash &&
138
142
  !!cachedDepsToPreBundle &&
139
- 'prebundleDeps' in cachedDepsToPreBundle
143
+ 'prebundleDeps' in cachedDepsToPreBundle &&
144
+ Array.isArray(cachedDepsToPreBundle.noExternalDeps)
140
145
  ) {
141
146
  value = cachedDepsToPreBundle
142
147
  debug?.(`Using cached scan results from ${cacheFilePath}`)
@@ -0,0 +1,77 @@
1
+ import { expect, test } from 'vitest'
2
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { runInNewContext } from 'node:vm'
6
+ import { rolldown } from 'rolldown'
7
+ import { backgroundComputationPlugin } from './backgroundComputationPlugin'
8
+ import { workletImportsPlugin } from './workletImportsPlugin'
9
+
10
+ async function bundle(source: string) {
11
+ const root = mkdtempSync(join(tmpdir(), 'one-background-definition-'))
12
+ writeFileSync(join(root, 'entry.ts'), source)
13
+ writeFileSync(
14
+ join(root, 'calculate.ts'),
15
+ `import { multiplier } from './helper'; export function calculate(input: number) { return input * multiplier }`
16
+ )
17
+ writeFileSync(join(root, 'helper.ts'), 'export const multiplier = 3')
18
+ let build: Awaited<ReturnType<typeof rolldown>> | undefined
19
+ try {
20
+ build = await rolldown({
21
+ input: join(root, 'entry.ts'),
22
+ plugins: [
23
+ backgroundComputationPlugin('native'),
24
+ workletImportsPlugin({}),
25
+ {
26
+ name: 'definition-contract',
27
+ resolveId(id) {
28
+ if (id === 'one/background') return '\0definition-contract'
29
+ },
30
+ load(id) {
31
+ if (id === '\0definition-contract')
32
+ return 'export function defineBackgroundComputation(calculate) { return calculate }'
33
+ },
34
+ },
35
+ ],
36
+ })
37
+ const result = await build.generate({ format: 'cjs' })
38
+ const chunk = result.output[0]
39
+ if (chunk.type !== 'chunk') throw new Error('expected executable output')
40
+ const exports: Record<string, unknown> = {}
41
+ runInNewContext(chunk.code, { exports })
42
+ return exports
43
+ } finally {
44
+ await build?.close()
45
+ rmSync(root, { recursive: true, force: true })
46
+ }
47
+ }
48
+
49
+ test('bundles an imported pure graph into the selected native calculation', async () => {
50
+ const result = await bundle(
51
+ `import { defineBackgroundComputation as define } from 'one/background'; import { calculate } from './calculate'; const task = define(calculate); export const answer = task(7);`
52
+ )
53
+ expect(result.answer).toBe(21)
54
+ })
55
+
56
+ test('unused definition imports do not reject a module', async () => {
57
+ const result = await bundle(
58
+ `import { defineBackgroundComputation } from 'one/background'; export const answer = 21;`
59
+ )
60
+ expect(result.answer).toBe(21)
61
+ })
62
+
63
+ test('rejects component definitions even beside a valid definition', async () => {
64
+ await expect(
65
+ bundle(
66
+ `import { defineBackgroundComputation } from 'one/background'; import { calculate } from './calculate'; export const good = defineBackgroundComputation(calculate); export function Component() { return defineBackgroundComputation(calculate) }`
67
+ )
68
+ ).rejects.toThrow('definitions must be module-scope variables')
69
+ })
70
+
71
+ test('rejects inline calculations that can capture component state', async () => {
72
+ await expect(
73
+ bundle(
74
+ `import { defineBackgroundComputation } from 'one/background'; export const bad = defineBackgroundComputation((input) => input);`
75
+ )
76
+ ).rejects.toThrow('one named import from a relative pure module')
77
+ })
@@ -0,0 +1,29 @@
1
+ import { dirname, resolve, relative } from 'node:path'
2
+ import {
3
+ transformBackgroundComputations,
4
+ loadBackgroundComputationModule,
5
+ } from '../backgroundComputation'
6
+ import type { Plugin } from 'rolldown'
7
+
8
+ export function backgroundComputationPlugin(platform: 'web' | 'native'): Plugin {
9
+ return {
10
+ name: `one:background-computation:${platform}`,
11
+ transform: {
12
+ order: 'pre',
13
+ handler(code, id) {
14
+ return transformBackgroundComputations(code, id, {
15
+ platform,
16
+ resolve: (source, importer) => resolve(dirname(importer), source),
17
+ workerFactory: (module) =>
18
+ `() => new Worker(new URL(${JSON.stringify(`./${relative(dirname(id), module.id).replaceAll('\\', '/')}`)}, import.meta.url), { type: 'module' })`,
19
+ })
20
+ },
21
+ },
22
+ resolveId(source) {
23
+ if (platform === 'web' && loadBackgroundComputationModule(source)) return source
24
+ },
25
+ load(id) {
26
+ if (platform === 'web') return loadBackgroundComputationModule(id)?.source
27
+ },
28
+ }
29
+ }
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path'
2
2
  import FSExtra from 'fs-extra'
3
+ import { getMimeType } from 'hono/utils/mime'
3
4
 
4
5
  import type { Plugin, ResolvedConfig } from 'vite'
5
6
  import colors from 'picocolors'
@@ -138,7 +139,10 @@ export default asset;
138
139
  try {
139
140
  const asset = await FSExtra.readFile(assetPath)
140
141
 
141
- res.setHeader('content-type', 'image/png')
142
+ res.setHeader(
143
+ 'content-type',
144
+ getMimeType(assetPath) || 'application/octet-stream'
145
+ )
142
146
  res.write(asset)
143
147
  res.end()
144
148
  } catch (e) {
@@ -0,0 +1,74 @@
1
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { createServer } from 'vite'
5
+ import { expect, it } from 'vitest'
6
+ import { createReactNativeDevServerPlugin } from './reactNativeDevServer'
7
+
8
+ it('serves native media with byte ranges and preview content types', async () => {
9
+ const root = await mkdtemp(join(tmpdir(), 'native-media-http-'))
10
+ const bytes = Buffer.from([0, 1, 2, 3, 4, 5, 6, 7])
11
+ await mkdir(join(root, 'assets'))
12
+ await writeFile(join(root, 'assets/clip.mp4'), bytes)
13
+ await writeFile(join(root, 'assets/preview.txt'), 'native preview')
14
+ const server = await createServer({
15
+ root,
16
+ configFile: false,
17
+ plugins: [createReactNativeDevServerPlugin()],
18
+ server: { host: '127.0.0.1', port: 0 },
19
+ })
20
+ try {
21
+ await server.listen()
22
+ const { port } = server.httpServer!.address() as { port: number }
23
+ const origin = `http://127.0.0.1:${port}/assets/assets`
24
+ const full = await fetch(`${origin}/clip.mp4?platform=ios`)
25
+ expect(full.status).toBe(200)
26
+ expect(full.headers.get('content-type')).toBe('video/mp4')
27
+ expect(full.headers.get('accept-ranges')).toBe('bytes')
28
+ expect(Buffer.from(await full.arrayBuffer())).toEqual(bytes)
29
+ for (const [range, start, end] of [
30
+ ['bytes=0-1', 0, 1],
31
+ ['bytes=3-', 3, 7],
32
+ ['bytes=-2', 6, 7],
33
+ ['bytes=5-99', 5, 7],
34
+ ['bytes=-99', 0, 7],
35
+ ] as const) {
36
+ const response = await fetch(`${origin}/clip.mp4`, { headers: { range } })
37
+ expect(response.status).toBe(206)
38
+ expect(response.headers.get('content-range')).toBe(`bytes ${start}-${end}/8`)
39
+ expect(response.headers.get('content-length')).toBe(String(end - start + 1))
40
+ expect(Buffer.from(await response.arrayBuffer())).toEqual(
41
+ bytes.subarray(start, end + 1)
42
+ )
43
+ }
44
+ for (const range of ['bytes=8-', 'bytes=4-2', 'bytes=-0']) {
45
+ const response = await fetch(`${origin}/clip.mp4`, { headers: { range } })
46
+ expect(response.status).toBe(416)
47
+ expect(response.headers.get('content-range')).toBe('bytes */8')
48
+ expect(await response.text()).toBe('')
49
+ }
50
+ const fullResponseHeaders: Record<string, string>[] = [
51
+ { range: 'items=0-1' },
52
+ { range: 'bytes=0-1,4-5' },
53
+ { range: 'bytes=0-1', 'if-range': '"old"' },
54
+ ]
55
+ for (const headers of fullResponseHeaders) {
56
+ const response = await fetch(`${origin}/clip.mp4`, { headers })
57
+ expect(response.status).toBe(200)
58
+ expect(Buffer.from(await response.arrayBuffer())).toEqual(bytes)
59
+ }
60
+ const head = await fetch(`${origin}/clip.mp4`, {
61
+ method: 'HEAD',
62
+ headers: { range: 'bytes=0-1' },
63
+ })
64
+ expect(head.status).toBe(200)
65
+ expect(head.headers.get('content-length')).toBe('8')
66
+ expect(await head.text()).toBe('')
67
+ const preview = await fetch(`${origin}/preview.txt`)
68
+ expect(preview.headers.get('content-type')).toBe('text/plain')
69
+ expect(await preview.text()).toBe('native preview')
70
+ } finally {
71
+ await server.close()
72
+ await rm(root, { recursive: true, force: true })
73
+ }
74
+ })
@@ -1,6 +1,10 @@
1
1
  import type { Connect, Plugin, ViteDevServer } from 'vite'
2
2
  import { WebSocketServer, type WebSocket } from 'ws'
3
3
  import { createMessageSocket } from '@vxrn/utils'
4
+ import {
5
+ expoClientFromNativeApp,
6
+ type NativeAppManifest,
7
+ } from '@vxrn/utils/nativeAppManifest'
4
8
  import {
5
9
  addConnectedNativeClient,
6
10
  removeConnectedNativeClient,
@@ -8,11 +12,13 @@ import {
8
12
  import type { VXRNOptionsFilled } from '../config/getOptionsFilled'
9
13
  import { URL } from 'node:url'
10
14
  import { createRequire } from 'node:module'
11
- import { join } from 'node:path'
15
+ import { extname, join, resolve, sep } from 'node:path'
12
16
  import { randomUUID } from 'node:crypto'
17
+ import { existsSync, readFileSync, statSync } from 'node:fs'
13
18
  import { readFile } from 'node:fs/promises'
14
19
  import { createDevMiddleware } from '@react-native/dev-middleware'
15
20
  import { createNativeDevEngine } from '../utils/createNativeDevEngine'
21
+ import { nativeDevBytecodeCompiler } from '../utils/compileNativeDevBytecode'
16
22
  import { getBoundPort } from '../utils/getBoundPort'
17
23
  import {
18
24
  getNativeFramePlatform,
@@ -32,17 +38,35 @@ type NativeHmrSocket = WebSocket & {
32
38
  vxrnPlatform: 'ios' | 'android'
33
39
  }
34
40
 
41
+ // metro's asset server answers `/assets/<path>` for any asset file inside the
42
+ // project root, registered by the bundle or not.
43
+ function getProjectAssetFile(
44
+ root: string,
45
+ pathname: string
46
+ ): { filePath: string; type: string } | undefined {
47
+ const filePath = resolve(root, pathname.slice('/assets/'.length))
48
+ if (!filePath.startsWith(resolve(root) + sep)) return
49
+ const type = extname(filePath).slice(1)
50
+ if (getNativeAssetContentType(type) === 'application/octet-stream') return
51
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) return
52
+ return { filePath, type }
53
+ }
54
+
35
55
  export function getNativeAssetContentType(type: string): string {
36
56
  switch (type.toLowerCase()) {
37
57
  case 'bmp':
38
58
  return 'image/bmp'
39
59
  case 'gif':
40
60
  return 'image/gif'
61
+ case 'glb':
62
+ return 'model/gltf-binary'
41
63
  case 'jpeg':
42
64
  case 'jpg':
43
65
  return 'image/jpeg'
44
66
  case 'json':
45
67
  return 'application/json'
68
+ case 'mp4':
69
+ return 'video/mp4'
46
70
  case 'otf':
47
71
  return 'font/otf'
48
72
  case 'png':
@@ -51,6 +75,8 @@ export function getNativeAssetContentType(type: string): string {
51
75
  return 'image/svg+xml'
52
76
  case 'ttf':
53
77
  return 'font/ttf'
78
+ case 'txt':
79
+ return 'text/plain'
54
80
  case 'webp':
55
81
  return 'image/webp'
56
82
  case 'woff':
@@ -87,6 +113,7 @@ export function createReactNativeDevServerPlugin(
87
113
  const devEngineCreating: Record<string, Promise<unknown> | null> = {}
88
114
  const warnedProdBundleRequest = new Set<string>()
89
115
  const pendingReloadPlatforms = new Set<'ios' | 'android'>()
116
+ let compileBytecode: ReturnType<typeof nativeDevBytecodeCompiler> | undefined
90
117
 
91
118
  const devToolsSocketEndpoints = ['/inspector/device', '/inspector/debug']
92
119
  const reactNativeDevToolsUrl = `http://${host}:${getBoundPort(server)}`
@@ -101,14 +128,32 @@ export function createReactNativeDevServerPlugin(
101
128
  // an expo project's clients (dev launcher, expo-updates, peach) ask `/`,
102
129
  // `/manifest` or `/index.exp` for the evaluated app config with an
103
130
  // `expo-platform` header or `?platform=`. answer with the expo updates
104
- // manifest expo cli's ExpoGoManifestHandlerMiddleware serves. a project
105
- // without @expo/config gets none, like a bare react native metro.
131
+ // manifest expo cli's ExpoGoManifestHandlerMiddleware serves. only apps
132
+ // that declare expo use expo prebuild; a hoisted @expo/config alone does
133
+ // not make a one native app an expo app.
106
134
  const projectRequire = createRequire(join(root, 'package.json'))
135
+ const packageJsonPath = join(root, 'package.json')
136
+ const packageJson = existsSync(packageJsonPath)
137
+ ? JSON.parse(readFileSync(packageJsonPath, 'utf8'))
138
+ : {}
139
+ const hasExpo =
140
+ Object.hasOwn(packageJson.dependencies ?? {}, 'expo') ||
141
+ Object.hasOwn(packageJson.devDependencies ?? {}, 'expo')
142
+ // a one app that declares no expo answers from its native.app manifest,
143
+ // mapped to the same expo config shape.
144
+ const nativeApp: NativeAppManifest | undefined = hasExpo
145
+ ? undefined
146
+ : (globalThis as any).__vxrnNativeEntryConfig?.app
107
147
  const anonymousScopeId = randomUUID()
108
148
  server.middlewares.use(async (req, res, next) => {
149
+ if (!hasExpo && !nativeApp) return next()
109
150
  if (req.method !== 'GET' && req.method !== 'HEAD') return next()
110
151
  const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
111
- if (url.pathname !== '/' && url.pathname !== '/manifest' && url.pathname !== '/index.exp') {
152
+ if (
153
+ url.pathname !== '/' &&
154
+ url.pathname !== '/manifest' &&
155
+ url.pathname !== '/index.exp'
156
+ ) {
112
157
  return next()
113
158
  }
114
159
  const header = req.headers['expo-platform'] || req.headers['exponent-platform']
@@ -116,26 +161,47 @@ export function createReactNativeDevServerPlugin(
116
161
  validPlatforms[url.searchParams.get('platform') || String(header || '')]
117
162
  if (!platform) return next()
118
163
 
119
- let expoConfigPath: string
120
- try {
121
- expoConfigPath = projectRequire.resolve('@expo/config')
122
- } catch {
123
- return next()
164
+ let expoConfigPath: string | undefined
165
+ if (!nativeApp) {
166
+ try {
167
+ expoConfigPath = projectRequire.resolve('@expo/config')
168
+ } catch {
169
+ return next()
170
+ }
124
171
  }
125
- const expoRequire = createRequire(expoConfigPath)
126
- const { getConfig } = expoRequire('@expo/config')
127
- const { resolveRelativeEntryPoint } = expoRequire('@expo/config/paths')
128
- const { Updates } = expoRequire('@expo/config-plugins')
129
172
 
130
173
  try {
131
- const { exp, pkg } = getConfig(root)
132
174
  const hostUri = req.headers.host || `localhost:${getBoundPort(server)}`
133
- const mainModuleName: string = resolveRelativeEntryPoint(root, { platform, pkg })
134
- const runtimeVersion = await Updates.getRuntimeVersionAsync(
135
- root,
136
- { ...exp, runtimeVersion: exp.runtimeVersion ?? { policy: 'sdkVersion' } },
137
- platform
138
- )
175
+ const { exp, mainModuleName, runtimeVersion } = nativeApp
176
+ ? {
177
+ exp: expoClientFromNativeApp(nativeApp),
178
+ // the vite native server answers any `.bundle` path with the
179
+ // app entry, so `index` is what a one app's native build loads.
180
+ mainModuleName: 'index',
181
+ runtimeVersion: nativeApp.updates?.runtimeVersion ?? null,
182
+ }
183
+ : await (async () => {
184
+ const expoRequire = createRequire(expoConfigPath!)
185
+ const { getConfig } = expoRequire('@expo/config')
186
+ const { resolveRelativeEntryPoint } = expoRequire('@expo/config/paths')
187
+ const { Updates } = expoRequire('@expo/config-plugins')
188
+ const { exp, pkg } = getConfig(root)
189
+ return {
190
+ exp,
191
+ mainModuleName: resolveRelativeEntryPoint(root, {
192
+ platform,
193
+ pkg,
194
+ }) as string,
195
+ runtimeVersion: await Updates.getRuntimeVersionAsync(
196
+ root,
197
+ {
198
+ ...exp,
199
+ runtimeVersion: exp.runtimeVersion ?? { policy: 'sdkVersion' },
200
+ },
201
+ platform
202
+ ),
203
+ }
204
+ })()
139
205
  const manifest = JSON.stringify({
140
206
  id: randomUUID(),
141
207
  createdAt: new Date().toISOString(),
@@ -146,7 +212,7 @@ export function createReactNativeDevServerPlugin(
146
212
  url: `http://${hostUri}/${encodeURI(mainModuleName.replace(/^\/+/, ''))}.bundle?platform=${platform}&dev=true&hot=false`,
147
213
  },
148
214
  assets: [],
149
- metadata: {},
215
+ metadata: nativeApp ? { oneNativeApp: true } : {},
150
216
  extra: {
151
217
  eas: { projectId: exp.extra?.eas?.projectId ?? undefined },
152
218
  expoClient: { ...exp, hostUri },
@@ -191,8 +257,11 @@ export function createReactNativeDevServerPlugin(
191
257
  })
192
258
 
193
259
  // Native AssetSourceResolver requests the URL registered in the Rolldown
194
- // bundle. Install this before React Native's generic middleware, which
195
- // otherwise terminates unknown /assets requests with an HTML 404.
260
+ // bundle. Like Metro, any other project file is served at
261
+ // `/assets/<project-relative path>`: a splash image or font that only the
262
+ // app config names is never in the bundle. Install this before React
263
+ // Native's generic middleware, which otherwise terminates unknown
264
+ // /assets requests with an HTML 404.
196
265
  server.middlewares.use(async (req, res, next) => {
197
266
  if (req.method !== 'GET' && req.method !== 'HEAD') return next()
198
267
 
@@ -204,7 +273,6 @@ export function createReactNativeDevServerPlugin(
204
273
 
205
274
  const platform = validPlatforms[url.searchParams.get('platform') || '']
206
275
  const engine = platform ? devEngines[platform] : undefined
207
- if (!engine) return next()
208
276
 
209
277
  let pathname: string
210
278
  try {
@@ -215,8 +283,11 @@ export function createReactNativeDevServerPlugin(
215
283
  return
216
284
  }
217
285
 
218
- const asset = engine.getAsset(pathname, url.searchParams.get('hash') || undefined)
286
+ const asset =
287
+ engine?.getAsset(pathname, url.searchParams.get('hash') || undefined) ??
288
+ getProjectAssetFile(root, pathname)
219
289
  if (!asset) {
290
+ if (!engine) return next()
220
291
  res.writeHead(404, { 'Content-Type': 'text/plain' })
221
292
  res.end('Native asset not found')
222
293
  return
@@ -224,11 +295,49 @@ export function createReactNativeDevServerPlugin(
224
295
 
225
296
  try {
226
297
  const contents = await readFile(asset.filePath)
227
- res.writeHead(200, {
298
+ const headers = {
228
299
  'Cache-Control': 'no-cache',
300
+ 'Accept-Ranges': 'bytes',
229
301
  'Content-Length': String(contents.byteLength),
230
302
  'Content-Type': getNativeAssetContentType(asset.type),
231
- })
303
+ }
304
+ // avfoundation probes remote media with byte ranges before loading it.
305
+ // ignore unsupported range units, multipart requests and if-range without
306
+ // a matching validator; these receive the complete representation.
307
+ const range =
308
+ req.method === 'GET' &&
309
+ !req.headers['if-range'] &&
310
+ /^bytes=(\d*)-(\d*)$/.exec(req.headers.range || '')
311
+ if (range && (range[1] || range[2])) {
312
+ const size = contents.byteLength
313
+ const start = range[1]
314
+ ? Number(range[1])
315
+ : Math.max(0, size - Number(range[2]))
316
+ const end =
317
+ range[1] && range[2] ? Math.min(Number(range[2]), size - 1) : size - 1
318
+ if (
319
+ !Number.isSafeInteger(start) ||
320
+ !Number.isSafeInteger(end) ||
321
+ start > end ||
322
+ start >= size
323
+ ) {
324
+ res.writeHead(416, {
325
+ ...headers,
326
+ 'Content-Length': '0',
327
+ 'Content-Range': `bytes */${size}`,
328
+ })
329
+ res.end()
330
+ return
331
+ }
332
+ res.writeHead(206, {
333
+ ...headers,
334
+ 'Content-Length': String(end - start + 1),
335
+ 'Content-Range': `bytes ${start}-${end}/${size}`,
336
+ })
337
+ res.end(contents.subarray(start, end + 1))
338
+ return
339
+ }
340
+ res.writeHead(200, headers)
232
341
  res.end(req.method === 'HEAD' ? undefined : contents)
233
342
  } catch (error) {
234
343
  console.error(
@@ -448,6 +557,14 @@ export function createReactNativeDevServerPlugin(
448
557
 
449
558
  try {
450
559
  const bundle = await (await getDevEngine(platform)).getBundle()
560
+ let body: string | Buffer = bundle.code
561
+ if (platform === 'ios' && url.searchParams.get('bytecode') === 'hermes') {
562
+ compileBytecode ||= nativeDevBytecodeCompiler(root)
563
+ body = await compileBytecode(
564
+ bundle,
565
+ `${url.origin}/index.bundle?platform=${platform}`
566
+ )
567
+ }
451
568
  // a client that connects after this response starts from the current
452
569
  // route map and does not need the pending reload intended for the
453
570
  // previous runtime.
@@ -460,7 +577,7 @@ export function createReactNativeDevServerPlugin(
460
577
  'Cache-Control': 'no-store',
461
578
  'Content-Type': 'text/javascript',
462
579
  })
463
- res.end(bundle.code)
580
+ res.end(body)
464
581
  } catch (err) {
465
582
  console.error(` Error building React Native bundle`)
466
583
  console.error(err)
@@ -101,6 +101,36 @@ export default codegenNativeComponent<NativeProps>('MyCustomView') as HostCompon
101
101
  )
102
102
  })
103
103
 
104
+ it('forwards arguments of commands declared as method signatures', () => {
105
+ const inputCode = `
106
+ import type { HostComponent, ViewProps } from 'react-native';
107
+ import type { Int32 } from 'react-native/Libraries/Types/CodegenTypes';
108
+ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
109
+ import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
110
+
111
+ interface NativeProps extends ViewProps {}
112
+ interface NativeCommands {
113
+ setPage(viewRef: React.ElementRef<HostComponent<NativeProps>>, index: Int32): void
114
+ setScrollEnabledImperatively(viewRef: React.ElementRef<HostComponent<NativeProps>>, enabled: boolean): void
115
+ }
116
+ export const Commands = codegenNativeCommands<NativeCommands>({
117
+ supportedCommands: ['setPage', 'setScrollEnabledImperatively'],
118
+ });
119
+ export default codegenNativeComponent<NativeProps>('MyPager');
120
+ `
121
+
122
+ const result = transformReactNativeCodegen(
123
+ inputCode,
124
+ '/workspace/src/MyPagerNativeComponent.ts'
125
+ )
126
+
127
+ expect(result?.code).toContain('setPage(ref, index) {')
128
+ expect(result?.code).toContain('dispatchCommand(ref, "setPage", [index]);')
129
+ expect(result?.code).toContain(
130
+ 'dispatchCommand(ref, "setScrollEnabledImperatively", [enabled]);'
131
+ )
132
+ })
133
+
104
134
  it('throws an error if native commands are exported with a name other than Commands', () => {
105
135
  const inputCode = `
106
136
  import * as React from 'react';