vxrn 2.7.6 → 2.8.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 (53) hide show
  1. package/dist/config/getBaseViteConfigOnly.mjs +1 -1
  2. package/dist/config/getBaseViteConfigOnly.mjs.map +1 -1
  3. package/dist/config/getBaseViteConfigOnly.native.js +1 -1
  4. package/dist/config/getBaseViteConfigOnly.native.js.map +1 -1
  5. package/dist/config/getOptimizeDeps.mjs +1 -1
  6. package/dist/config/getOptimizeDeps.mjs.map +1 -1
  7. package/dist/config/getOptimizeDeps.native.js +3 -1
  8. package/dist/config/getOptimizeDeps.native.js.map +1 -1
  9. package/dist/config/getReactNativePlugins.mjs +1 -2
  10. package/dist/config/getReactNativePlugins.mjs.map +1 -1
  11. package/dist/config/getReactNativePlugins.native.js +4 -2
  12. package/dist/config/getReactNativePlugins.native.js.map +1 -1
  13. package/dist/exports/prebuildWithoutExpo.mjs +26 -97
  14. package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
  15. package/dist/exports/prebuildWithoutExpo.native.js +47 -133
  16. package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
  17. package/dist/plugins/autoDepOptimizePlugin.mjs +5 -2
  18. package/dist/plugins/autoDepOptimizePlugin.mjs.map +1 -1
  19. package/dist/plugins/autoDepOptimizePlugin.native.js +10 -7
  20. package/dist/plugins/autoDepOptimizePlugin.native.js.map +1 -1
  21. package/dist/rn-commands/bundle/buildBundle.mjs +5 -4
  22. package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
  23. package/dist/rn-commands/bundle/buildBundle.native.js +5 -4
  24. package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
  25. package/dist/utils/createNativeDevEngine.mjs +30 -7
  26. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  27. package/dist/utils/createNativeDevEngine.native.js +64 -10
  28. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  29. package/package.json +27 -20
  30. package/src/config/getBaseViteConfigOnly.ts +1 -1
  31. package/src/config/getOptimizeDeps.ts +3 -1
  32. package/src/config/getReactNativePlugins.ts +8 -4
  33. package/src/config/ssrVendorOptimization.test.ts +115 -0
  34. package/src/exports/prebuildWithoutExpo.test.ts +62 -157
  35. package/src/exports/prebuildWithoutExpo.ts +31 -125
  36. package/src/plugins/autoDepOptimizeExternal.test.ts +101 -0
  37. package/src/plugins/autoDepOptimizePlugin.ts +12 -6
  38. package/src/plugins/rnCodegenPlugin.ts +1 -2
  39. package/src/rn-commands/bundle/buildBundle.ts +8 -7
  40. package/src/utils/createNativeDevEngine.test.ts +308 -9
  41. package/src/utils/createNativeDevEngine.ts +71 -29
  42. package/types/config/getOptimizeDeps.d.ts.map +1 -1
  43. package/types/config/getReactNativePlugins.d.ts +3 -2
  44. package/types/config/getReactNativePlugins.d.ts.map +1 -1
  45. package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
  46. package/types/plugins/autoDepOptimizePlugin.d.ts +1 -0
  47. package/types/plugins/autoDepOptimizePlugin.d.ts.map +1 -1
  48. package/types/plugins/rnCodegenPlugin.d.ts.map +1 -1
  49. package/types/rn-commands/bundle/buildBundle.d.ts +1 -1
  50. package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
  51. package/types/utils/createNativeDevEngine.d.ts +3 -4
  52. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  53. package/src/plugins/autoDepOptimizePlugin.test.ts +0 -42
@@ -151,14 +151,9 @@ function escapeXml(value: string): string {
151
151
  return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
152
152
  }
153
153
 
154
- // uiscene adoption for the xcode 27 sdk: an app built with sdk 27 must
155
- // declare a scene manifest or it fails to launch on ios 27 ("UIScene life
156
- // cycle is required for apps built with this SDK"). prebuild therefore
157
- // always renders the scene manifest, a SceneDelegate that owns the window
158
- // and the RN root, and a trimmed AppDelegate. no toggle: the scene path is
159
- // the only path, on every ios version prebuild supports.
160
- const SCENE_DELEGATE_FILE_REF_ID = '1A2B3C4D5E6F7A8B9C0D1E2F'
161
- const SCENE_DELEGATE_BUILD_FILE_ID = '2B3C4D5E6F7A8B9C0D1E2F1A'
154
+ // the rn template supplies the scene manifest and xcode source references.
155
+ // keep factory ownership at app scope so background tasks and app intents can
156
+ // start react native before a scene connects; each scene owns its window.
162
157
  const PUSH_ENTITLEMENTS_FILE_REF_ID = '3C4D5E6F7A8B9C0D1E2F1A2B'
163
158
 
164
159
  export function renderSceneDelegateSwift(appName: string): string {
@@ -185,40 +180,19 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
185
180
  let window = UIWindow(windowScene: windowScene)
186
181
  self.window = window
187
182
 
188
- // cold-start links arrive in connectionOptions under scenes, never in
189
- // the app launchOptions, so they are translated into the launchOptions
190
- // shape RCTLinkingManager.getInitialURL reads.
191
- var launchOptions: [UIApplication.LaunchOptionsKey: Any] = [:]
192
- if let url = connectionOptions.urlContexts.first?.url {
193
- launchOptions[.url] = url
194
- }
195
- if let activity = connectionOptions.userActivities.first(where: {
196
- $0.activityType == NSUserActivityTypeBrowsingWeb
197
- }) {
198
- launchOptions[.userActivityDictionary] = [
199
- "UIApplicationLaunchOptionsUserActivityTypeKey": activity.activityType,
200
- "UIApplicationLaunchOptionsUserActivityKey": activity,
201
- ]
202
- }
203
-
204
183
  factory.startReactNative(
205
184
  withModuleName: "${appName}",
206
185
  in: window,
207
- launchOptions: launchOptions
186
+ connectionOptions: connectionOptions
208
187
  )
209
188
  }
210
189
 
211
190
  func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
212
- guard let url = URLContexts.first?.url else { return }
213
- RCTLinkingManager.application(UIApplication.shared, open: url, options: [:])
191
+ RCTLinkingManager.scene(scene, openURLContexts: URLContexts)
214
192
  }
215
193
 
216
194
  func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
217
- RCTLinkingManager.application(
218
- UIApplication.shared,
219
- continue: userActivity,
220
- restorationHandler: { _ in }
221
- )
195
+ RCTLinkingManager.scene(scene, continue: userActivity)
222
196
  }
223
197
  }
224
198
  `
@@ -232,79 +206,32 @@ function insertAfterLine(haystack: string, anchor: string, insertion: string): s
232
206
  return `${haystack.slice(0, lineEnd + 1)}${insertion}\n${haystack.slice(lineEnd + 1)}`
233
207
  }
234
208
 
235
- function patchIosInfoPlistSceneManifest(rendered: string): string {
236
- const anchor = '\t<key>LSRequiresIPhoneOS</key>'
237
- if (!rendered.includes(anchor)) {
238
- throw new Error(
239
- '[vxrn] prebuild template Info.plist lost its LSRequiresIPhoneOS anchor'
240
- )
241
- }
242
- return rendered.replace(
243
- anchor,
244
- `\t<key>UIApplicationSceneManifest</key>
245
- \t<dict>
246
- \t\t<key>UIApplicationSupportsMultipleScenes</key>
247
- \t\t<false/>
248
- \t\t<key>UISceneConfigurations</key>
249
- \t\t<dict>
250
- \t\t\t<key>UIWindowSceneSessionRoleApplication</key>
251
- \t\t\t<array>
252
- \t\t\t\t<dict>
253
- \t\t\t\t\t<key>UISceneConfigurationName</key>
254
- \t\t\t\t\t<string>Default Configuration</string>
255
- \t\t\t\t\t<key>UISceneDelegateClassName</key>
256
- \t\t\t\t\t<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
257
- \t\t\t\t</dict>
258
- \t\t\t</array>
259
- \t\t</dict>
260
- \t</dict>
261
- ${anchor}`
262
- )
263
- }
264
-
265
209
  function patchIosAppDelegateSceneLifecycle(
266
210
  rendered: string,
267
- appName: string,
268
211
  backgroundTasks: boolean,
269
212
  appIntents: boolean
270
213
  ): string {
271
214
  const anchor = `@main
272
215
  class AppDelegate: UIResponder, UIApplicationDelegate {
273
- var window: UIWindow?
274
-
275
- var reactNativeDelegate: ReactNativeDelegate?
276
- var reactNativeFactory: RCTReactNativeFactory?
277
-
278
216
  func application(
279
217
  _ application: UIApplication,
280
218
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
281
219
  ) -> Bool {
282
- let delegate = ReactNativeDelegate()
283
- let factory = RCTReactNativeFactory(delegate: delegate)
284
- delegate.dependencyProvider = RCTAppDependencyProvider()
285
-
286
- reactNativeDelegate = delegate
287
- reactNativeFactory = factory
288
-
289
- window = UIWindow(frame: UIScreen.main.bounds)
290
-
291
- factory.startReactNative(
292
- withModuleName: "${appName}",
293
- in: window,
294
- launchOptions: launchOptions
295
- )
296
-
297
- return true
220
+ true
298
221
  }
299
222
  }`
300
223
  if (!rendered.includes(anchor)) {
301
224
  throw new Error(
302
- '[vxrn] prebuild template AppDelegate.swift changed shape: cannot move the RN root to the scene delegate'
225
+ '[vxrn] prebuild template AppDelegate.swift changed shape: cannot register the app-level React Native factory'
303
226
  )
304
227
  }
305
228
  return rendered.replace(
306
229
  anchor,
307
- `@main
230
+ `import React
231
+ import React_RCTAppDelegate
232
+ import ReactAppDependencyProvider
233
+
234
+ @main
308
235
  class AppDelegate: UIResponder, UIApplicationDelegate {
309
236
  var reactNativeDelegate: ReactNativeDelegate?
310
237
  var reactNativeFactory: RCTReactNativeFactory?
@@ -322,6 +249,16 @@ ${backgroundTasks ? ` OneBackgroundTasksRegisterHost(factory, launchOptions)
322
249
  ` : ''}${appIntents ? ` OneAppIntentsRegisterHost(factory, launchOptions)
323
250
  ` : ''} return true
324
251
  }
252
+ }
253
+
254
+ class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
255
+ override func bundleURL() -> URL? {
256
+ #if DEBUG
257
+ RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
258
+ #else
259
+ Bundle.main.url(forResource: "main", withExtension: "jsbundle")
260
+ #endif
261
+ }
325
262
  }`
326
263
  )
327
264
  }
@@ -362,38 +299,6 @@ function patchIosPbxprojOneBridgingHeader(project: string, appName: string): str
362
299
  .join(`${anchor}\n\t\t\t\t${setting}`)
363
300
  }
364
301
 
365
- function patchIosPbxprojSceneDelegate(rendered: string, appName: string): string {
366
- const edits: Array<[string, string]> = [
367
- [
368
- '/* AppDelegate.swift in Sources */ = {isa = PBXBuildFile;',
369
- `\t\t${SCENE_DELEGATE_BUILD_FILE_ID} /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ${SCENE_DELEGATE_FILE_REF_ID} /* SceneDelegate.swift */; };`,
370
- ],
371
- [
372
- '/* AppDelegate.swift */ = {isa = PBXFileReference;',
373
- `\t\t${SCENE_DELEGATE_FILE_REF_ID} /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = ${appName}/SceneDelegate.swift; sourceTree = "<group>"; };`,
374
- ],
375
- [
376
- '/* AppDelegate.swift */,',
377
- `\t\t\t\t${SCENE_DELEGATE_FILE_REF_ID} /* SceneDelegate.swift */,`,
378
- ],
379
- [
380
- '/* AppDelegate.swift in Sources */,',
381
- `\t\t\t\t${SCENE_DELEGATE_BUILD_FILE_ID} /* SceneDelegate.swift in Sources */,`,
382
- ],
383
- ]
384
- let patched = rendered
385
- for (const [anchor, insertion] of edits) {
386
- const next = insertAfterLine(patched, anchor, insertion)
387
- if (next === patched) {
388
- throw new Error(
389
- `[vxrn] prebuild template project.pbxproj lost its AppDelegate.swift anchor (${anchor})`
390
- )
391
- }
392
- patched = next
393
- }
394
- return patched
395
- }
396
-
397
302
  function patchIosPbxprojAppIntents(rendered: string, appName: string): string {
398
303
  const name = 'OneAppIntents.swift'
399
304
  const fileRef = pbxprojId('one-app-intents-file')
@@ -2703,12 +2608,10 @@ ${schemes.map((scheme) => ` <data android:scheme="${scheme}" />`).joi
2703
2608
  /\t<key>NS\w+UsageDescription<\/key>\s*<string><\/string>\n/g,
2704
2609
  ''
2705
2610
  )
2706
- rendered = patchIosInfoPlistSceneManifest(rendered)
2707
2611
  }
2708
2612
  if (platform === 'ios' && relativePath.endsWith('/AppDelegate.swift')) {
2709
2613
  rendered = patchIosAppDelegateSceneLifecycle(
2710
2614
  rendered,
2711
- appName,
2712
2615
  !!app.ios?.backgroundTasks,
2713
2616
  !!app.ios?.appIntents
2714
2617
  )
@@ -2734,15 +2637,19 @@ ${schemes.map((scheme) => ` <data android:scheme="${scheme}" />`).joi
2734
2637
  rendered = nativeProjectPatches.pointReleaseBundleURLAtOneUpdates(rendered)
2735
2638
  }
2736
2639
  }
2737
- if (platform === 'ios' && app.ios?.deploymentTarget) {
2640
+ if (platform === 'ios') {
2641
+ // one requires ios 17 (see the schema minimumVersion and
2642
+ // One.podspec); the template's react native 15.1 default fails pod
2643
+ // install, so an unset target falls back to one's floor.
2644
+ const deploymentTarget = app.ios?.deploymentTarget ?? '17.0'
2738
2645
  rendered = rendered
2739
2646
  .replace(
2740
2647
  'platform :ios, min_ios_version_supported',
2741
- `platform :ios, '${app.ios.deploymentTarget}'`
2648
+ `platform :ios, '${deploymentTarget}'`
2742
2649
  )
2743
2650
  .replace(
2744
2651
  /IPHONEOS_DEPLOYMENT_TARGET = \d+(?:\.\d+)?;/g,
2745
- `IPHONEOS_DEPLOYMENT_TARGET = ${app.ios.deploymentTarget};`
2652
+ `IPHONEOS_DEPLOYMENT_TARGET = ${deploymentTarget};`
2746
2653
  )
2747
2654
  if (relativePath === 'Podfile') {
2748
2655
  rendered = rendered.replace(
@@ -2751,7 +2658,7 @@ ${schemes.map((scheme) => ` <data android:scheme="${scheme}" />`).joi
2751
2658
 
2752
2659
  installer.pods_project.targets.each do |target|
2753
2660
  target.build_configurations.each do |config|
2754
- config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '${app.ios.deploymentTarget}'
2661
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '${deploymentTarget}'
2755
2662
  end
2756
2663
  end
2757
2664
  end
@@ -2761,7 +2668,6 @@ end`
2761
2668
  }
2762
2669
  if (platform === 'ios' && relativePath.endsWith('.xcodeproj/project.pbxproj')) {
2763
2670
  rendered = patchIosBundlePhase(rendered, app.updates?.runtimeVersion)
2764
- rendered = patchIosPbxprojSceneDelegate(rendered, appName)
2765
2671
  if (app.ios?.appIntents) rendered = patchIosPbxprojAppIntents(rendered, appName)
2766
2672
  rendered = patchIosPbxprojOneBridgingHeader(rendered, appName)
2767
2673
  if (Object.keys(appEntitlements(app)).length && !app.ios?.widgets) {
@@ -0,0 +1,101 @@
1
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
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 { autoDepOptimizePlugin } from './autoDepOptimizePlugin'
7
+
8
+ it.each([
9
+ { external: 'external-dep', rootBundled: false, exclude: [] },
10
+ { external: 'external-dep/feature', rootBundled: true, exclude: [] },
11
+ { external: 'external-dep', rootBundled: false, exclude: ['internal-dep'] },
12
+ {
13
+ external: 'external-dep',
14
+ rootBundled: false,
15
+ exclude: [/some-pattern/, 'internal-dep'],
16
+ },
17
+ ])(
18
+ 'keeps SSR external $external and exclusions $exclude outside prebundling',
19
+ async ({ external, rootBundled, exclude }) => {
20
+ const root = realpathSync(mkdtempSync(join(tmpdir(), 'vxrn-ssr-external-')))
21
+ const bundled = new Set<string>()
22
+ const names = ['external-dep', 'external-dep-extra', 'internal-dep']
23
+ writeFileSync(
24
+ join(root, 'package.json'),
25
+ JSON.stringify({
26
+ private: true,
27
+ dependencies: Object.fromEntries(names.map((name) => [name, '1.0.0'])),
28
+ })
29
+ )
30
+ for (const name of names) {
31
+ const folder = join(root, 'node_modules', name)
32
+ mkdirSync(folder, { recursive: true })
33
+ writeFileSync(
34
+ join(folder, 'package.json'),
35
+ JSON.stringify({
36
+ name,
37
+ version: '1.0.0',
38
+ type: 'module',
39
+ peerDependencies: { react: '*' },
40
+ exports: {
41
+ '.': { import: './index.js' },
42
+ './feature': { import: './feature.js' },
43
+ },
44
+ })
45
+ )
46
+ writeFileSync(join(folder, 'index.js'), 'export const value = 1')
47
+ writeFileSync(join(folder, 'feature.js'), 'export const value = 2')
48
+ }
49
+ writeFileSync(
50
+ join(root, 'entry.js'),
51
+ `import { value as external } from 'external-dep'
52
+ import { value as feature } from 'external-dep/feature'
53
+ import { value as sibling } from 'external-dep-extra'
54
+ import { value as internal } from 'internal-dep'
55
+ export const value = external + feature + sibling + internal
56
+ `
57
+ )
58
+ const server = await createServer({
59
+ root,
60
+ configFile: false,
61
+ server: { middlewareMode: true },
62
+ optimizeDeps: { noDiscovery: true },
63
+ ssr: { external: [external] },
64
+ plugins: [autoDepOptimizePlugin({ root, exclude })],
65
+ environments: {
66
+ ssr: {
67
+ optimizeDeps: {
68
+ noDiscovery: true,
69
+ rolldownOptions: {
70
+ plugins: [
71
+ {
72
+ name: 'record-prebundled-packages',
73
+ load(id) {
74
+ if (id.startsWith(join(root, 'node_modules'))) bundled.add(id)
75
+ },
76
+ },
77
+ ],
78
+ },
79
+ },
80
+ },
81
+ },
82
+ })
83
+ try {
84
+ await server.environments.ssr.listen(server)
85
+ expect((await server.ssrLoadModule(join(root, 'entry.js'))).value).toBe(5)
86
+ expect(bundled.has(join(root, 'node_modules/internal-dep/index.js'))).toBe(
87
+ !exclude.includes('internal-dep')
88
+ )
89
+ expect(bundled.has(join(root, 'node_modules/external-dep-extra/index.js'))).toBe(
90
+ true
91
+ )
92
+ expect(bundled.has(join(root, 'node_modules/external-dep/index.js'))).toBe(
93
+ rootBundled
94
+ )
95
+ expect(bundled.has(join(root, 'node_modules/external-dep/feature.js'))).toBe(false)
96
+ } finally {
97
+ await server.close()
98
+ rmSync(root, { recursive: true, force: true })
99
+ }
100
+ }
101
+ )
@@ -47,6 +47,7 @@ export function autoDepOptimizePlugin(props: FindDepsOptions): Plugin {
47
47
  const finalConfig = await getScannedOptimizeDepsConfig({
48
48
  ...props,
49
49
  mode: env.mode,
50
+ external: cfg.ssr?.external,
50
51
  exclude: [
51
52
  ...exclude,
52
53
  ...userExcludes,
@@ -68,7 +69,10 @@ export const getSSRExternalsCachePath = (root: string) => {
68
69
  return path.join(getCacheDir(root), 'deps-to-pre-bundle-for-ssr-cache.json')
69
70
  }
70
71
 
71
- type FindDepsOptionsByMode = FindDepsOptions & { mode: string }
72
+ type FindDepsOptionsByMode = FindDepsOptions & {
73
+ mode: string
74
+ external?: string[] | true
75
+ }
72
76
 
73
77
  export async function getScannedOptimizeDepsConfig(props: FindDepsOptionsByMode) {
74
78
  const result = await findDepsToOptimize(props)
@@ -80,8 +84,13 @@ export async function getScannedOptimizeDepsConfig(props: FindDepsOptionsByMode)
80
84
  const excludeArray = Array.isArray(props.exclude) ? props.exclude : [props.exclude]
81
85
  const excludeStrings = excludeArray.filter((e): e is string => typeof e === 'string')
82
86
  const excludeSet = new Set(excludeStrings)
87
+ const external = props.external
88
+ const shouldOptimize = (dep: string) =>
89
+ !excludeSet.has(dep) &&
90
+ external !== true &&
91
+ !external?.some((name) => dep === name || dep.startsWith(`${name}/`))
83
92
 
84
- const filteredDeps = result.prebundleDeps.filter((dep) => !excludeSet.has(dep))
93
+ const filteredDeps = result.prebundleDeps.filter(shouldOptimize)
85
94
 
86
95
  return {
87
96
  ssr: {
@@ -89,10 +98,7 @@ export async function getScannedOptimizeDepsConfig(props: FindDepsOptionsByMode)
89
98
  include: filteredDeps,
90
99
  exclude: EXCLUDE_LIST,
91
100
  },
92
- noExternal: [
93
- ...filteredDeps,
94
- ...result.noExternalDeps.filter((dep) => !excludeSet.has(dep)),
95
- ],
101
+ noExternal: [...filteredDeps, ...result.noExternalDeps.filter(shouldOptimize)],
96
102
  },
97
103
  }
98
104
  }
@@ -2,8 +2,7 @@ import type { Plugin } from 'vite'
2
2
  import { transformReactNativeCodegen } from '@vxrn/compiler'
3
3
 
4
4
  // the transform itself lives in @vxrn/compiler so the metro transformer worker
5
- // can run it too. vxrn depends on @vxrn/vite-plugin-metro, so the worker cannot
6
- // reach back into this package.
5
+ // can run it too without loading vxrn.
7
6
  export { transformReactNativeCodegen } from '@vxrn/compiler'
8
7
 
9
8
  export function rnCodegenPlugin(options?: { projectRoot?: string }): Plugin {
@@ -1,9 +1,7 @@
1
1
  import FSExtra from 'fs-extra'
2
2
  import { clearTransformCache } from '@vxrn/compiler'
3
- import {
4
- bundle as metroBundle,
5
- type BundleCommandArgs,
6
- } from '@vxrn/vite-plugin-metro/rn-commands'
3
+ import type { BundleCommandArgs } from '@vxrn/vite-plugin-metro/rn-commands'
4
+ import { resolveConfig } from 'vite'
7
5
  import { loadEnv } from '../../exports/loadEnv'
8
6
  import { buildNativeBundle } from '../../utils/createNativeDevEngine'
9
7
 
@@ -48,11 +46,14 @@ export async function buildBundle(
48
46
  throw new Error(`Expected ctx.root to be a string, but got ${typeof root}`)
49
47
  }
50
48
 
51
- const metroBuildBundleFn = await metroBundle.getBuildBundleFn()
49
+ const viteConfig = await resolveConfig({}, 'build', 'production')
52
50
 
53
- if (metroBuildBundleFn) {
51
+ // resolving the config runs the metro plugin only in metro mode, and metro is
52
+ // an optional peer, so only that mode loads it
53
+ if (globalThis['__viteMetroPluginOptions__']) {
54
54
  console.info('Using @vxrn/vite-plugin-metro to build the bundle.')
55
- await metroBuildBundleFn(_argv, ctx, args, bundleImpl)
55
+ const { bundle } = await import('@vxrn/vite-plugin-metro/rn-commands')
56
+ await bundle.getBuildBundleFn(viteConfig)(_argv, ctx, args, bundleImpl)
56
57
  // metroBuildBundleFn calls ensureProcessExitsAfterDelay internally,
57
58
  // which kills lingering child handles (esbuild --service) and lets
58
59
  // the event loop drain naturally. no extra setTimeout needed —