@vxrn/vite-plugin-metro 2.0.0-beta.51.1 → 2.0.0-beta.56.1

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 (142) hide show
  1. package/dist/cjs/env/platformEnv.cjs +2 -0
  2. package/dist/cjs/env/platformEnv.native.cjs +2 -0
  3. package/dist/cjs/env/platformEnv.native.js +2 -0
  4. package/dist/cjs/env/platformEnv.native.js.map +1 -1
  5. package/dist/cjs/index.cjs +0 -2
  6. package/dist/cjs/index.native.cjs +0 -2
  7. package/dist/cjs/index.native.js +0 -2
  8. package/dist/cjs/index.native.js.map +1 -1
  9. package/dist/cjs/metro-config/getMetroBabelConfigFromViteConfig.cjs +10 -15
  10. package/dist/cjs/metro-config/getMetroBabelConfigFromViteConfig.native.cjs +27 -48
  11. package/dist/cjs/metro-config/getMetroBabelConfigFromViteConfig.native.js +27 -48
  12. package/dist/cjs/metro-config/getMetroBabelConfigFromViteConfig.native.js.map +1 -1
  13. package/dist/cjs/metro-config/getMetroConfigFromViteConfig.cjs +71 -38
  14. package/dist/cjs/metro-config/getMetroConfigFromViteConfig.native.cjs +86 -42
  15. package/dist/cjs/metro-config/getMetroConfigFromViteConfig.native.js +86 -42
  16. package/dist/cjs/metro-config/getMetroConfigFromViteConfig.native.js.map +1 -1
  17. package/dist/cjs/transformer/babel-transformer.cjs +53 -4
  18. package/dist/cjs/transformer/babel-transformer.native.cjs +65 -6
  19. package/dist/cjs/transformer/babel-transformer.native.js +65 -6
  20. package/dist/cjs/transformer/babel-transformer.native.js.map +1 -1
  21. package/dist/cjs/transformer/expoVirtualEnv.cjs +92 -0
  22. package/dist/cjs/transformer/expoVirtualEnv.native.cjs +113 -0
  23. package/dist/cjs/transformer/expoVirtualEnv.native.js +115 -0
  24. package/dist/cjs/transformer/expoVirtualEnv.native.js.map +1 -0
  25. package/dist/cjs/transformer/loadBabelConfig.cjs +1 -1
  26. package/dist/cjs/transformer/loadBabelConfig.native.cjs +1 -1
  27. package/dist/cjs/transformer/loadBabelConfig.native.js +1 -1
  28. package/dist/cjs/transformer/loadBabelConfig.native.js.map +1 -1
  29. package/dist/cjs/transformer/metroNativeWorker.cjs +20 -29
  30. package/dist/cjs/transformer/metroNativeWorker.native.cjs +24 -30
  31. package/dist/cjs/transformer/metroNativeWorker.native.js +24 -30
  32. package/dist/cjs/transformer/metroNativeWorker.native.js.map +1 -1
  33. package/dist/cjs/transformer/transformSync.cjs +3 -3
  34. package/dist/cjs/transformer/transformSync.native.cjs +3 -3
  35. package/dist/cjs/transformer/transformSync.native.js +3 -3
  36. package/dist/cjs/transformer/transformSync.native.js.map +1 -1
  37. package/dist/cjs/utils/exit.cjs +1 -1
  38. package/dist/cjs/utils/exit.native.cjs +1 -1
  39. package/dist/cjs/utils/exit.native.js +1 -1
  40. package/dist/cjs/utils/exit.native.js.map +1 -1
  41. package/dist/esm/env/platformEnv.mjs +2 -0
  42. package/dist/esm/env/platformEnv.mjs.map +1 -1
  43. package/dist/esm/env/platformEnv.native.js +2 -0
  44. package/dist/esm/env/platformEnv.native.js.map +1 -1
  45. package/dist/esm/index.js +1 -2
  46. package/dist/esm/index.mjs +1 -2
  47. package/dist/esm/index.native.js +1 -2
  48. package/dist/esm/metro-config/getMetroBabelConfigFromViteConfig.mjs +11 -15
  49. package/dist/esm/metro-config/getMetroBabelConfigFromViteConfig.mjs.map +1 -1
  50. package/dist/esm/metro-config/getMetroBabelConfigFromViteConfig.native.js +28 -48
  51. package/dist/esm/metro-config/getMetroBabelConfigFromViteConfig.native.js.map +1 -1
  52. package/dist/esm/metro-config/getMetroConfigFromViteConfig.mjs +71 -38
  53. package/dist/esm/metro-config/getMetroConfigFromViteConfig.mjs.map +1 -1
  54. package/dist/esm/metro-config/getMetroConfigFromViteConfig.native.js +86 -42
  55. package/dist/esm/metro-config/getMetroConfigFromViteConfig.native.js.map +1 -1
  56. package/dist/esm/transformer/babel-transformer.mjs +133 -96
  57. package/dist/esm/transformer/babel-transformer.mjs.map +1 -1
  58. package/dist/esm/transformer/babel-transformer.native.js +147 -103
  59. package/dist/esm/transformer/babel-transformer.native.js.map +1 -1
  60. package/dist/esm/transformer/expoVirtualEnv.mjs +67 -0
  61. package/dist/esm/transformer/expoVirtualEnv.mjs.map +1 -0
  62. package/dist/esm/transformer/expoVirtualEnv.native.js +86 -0
  63. package/dist/esm/transformer/expoVirtualEnv.native.js.map +1 -0
  64. package/dist/esm/transformer/loadBabelConfig.mjs +1 -1
  65. package/dist/esm/transformer/loadBabelConfig.mjs.map +1 -1
  66. package/dist/esm/transformer/loadBabelConfig.native.js +1 -1
  67. package/dist/esm/transformer/loadBabelConfig.native.js.map +1 -1
  68. package/dist/esm/transformer/metroNativeWorker.mjs +20 -29
  69. package/dist/esm/transformer/metroNativeWorker.mjs.map +1 -1
  70. package/dist/esm/transformer/metroNativeWorker.native.js +24 -30
  71. package/dist/esm/transformer/metroNativeWorker.native.js.map +1 -1
  72. package/dist/esm/transformer/transformSync.mjs +3 -3
  73. package/dist/esm/transformer/transformSync.mjs.map +1 -1
  74. package/dist/esm/transformer/transformSync.native.js +3 -3
  75. package/dist/esm/transformer/transformSync.native.js.map +1 -1
  76. package/dist/esm/utils/exit.mjs +1 -1
  77. package/dist/esm/utils/exit.mjs.map +1 -1
  78. package/dist/esm/utils/exit.native.js +1 -1
  79. package/dist/esm/utils/exit.native.js.map +1 -1
  80. package/package.json +7 -9
  81. package/src/babel-plugins/import-meta-env-plugin.ts +1 -1
  82. package/src/env/platformEnv.test.ts +12 -2
  83. package/src/env/platformEnv.ts +3 -0
  84. package/src/index.ts +0 -2
  85. package/src/metro-config/getMetroBabelConfigFromViteConfig.test.ts +19 -25
  86. package/src/metro-config/getMetroBabelConfigFromViteConfig.ts +8 -27
  87. package/src/metro-config/getMetroConfigFromViteConfig.test.ts +60 -0
  88. package/src/metro-config/getMetroConfigFromViteConfig.ts +119 -96
  89. package/src/plugins/metroPlugin.ts +3 -6
  90. package/src/transformer/babel-transformer.test.ts +118 -0
  91. package/src/transformer/babel-transformer.ts +82 -15
  92. package/src/transformer/expoVirtualEnv.integration.test.ts +106 -0
  93. package/src/transformer/expoVirtualEnv.test.ts +72 -0
  94. package/src/transformer/expoVirtualEnv.ts +115 -0
  95. package/src/transformer/loadBabelConfig.ts +1 -1
  96. package/src/transformer/metroNativeWorker.test.ts +134 -17
  97. package/src/transformer/metroNativeWorker.ts +43 -51
  98. package/src/transformer/transformSync.test.ts +22 -0
  99. package/src/transformer/transformSync.ts +4 -4
  100. package/src/utils/exit.ts +2 -2
  101. package/src/utils/getTerminalReporter.ts +1 -1
  102. package/src/utils/projectImport.ts +2 -2
  103. package/types/babel-plugins/import-meta-env-plugin.d.ts +1 -1
  104. package/types/env/platformEnv.d.ts +1 -0
  105. package/types/env/platformEnv.d.ts.map +1 -1
  106. package/types/index.d.ts +0 -2
  107. package/types/index.d.ts.map +1 -1
  108. package/types/metro-config/getMetroBabelConfigFromViteConfig.d.ts +1 -1
  109. package/types/metro-config/getMetroBabelConfigFromViteConfig.d.ts.map +1 -1
  110. package/types/metro-config/getMetroConfigFromViteConfig.d.ts +2 -2
  111. package/types/metro-config/getMetroConfigFromViteConfig.d.ts.map +1 -1
  112. package/types/plugins/metroPlugin.d.ts +2 -4
  113. package/types/plugins/metroPlugin.d.ts.map +1 -1
  114. package/types/transformer/babel-transformer.d.ts +10 -1
  115. package/types/transformer/babel-transformer.d.ts.map +1 -1
  116. package/types/transformer/expoVirtualEnv.d.ts +8 -0
  117. package/types/transformer/expoVirtualEnv.d.ts.map +1 -0
  118. package/types/transformer/metroNativeWorker.d.ts +3 -5
  119. package/types/transformer/metroNativeWorker.d.ts.map +1 -1
  120. package/types/utils/projectImport.d.ts +2 -2
  121. package/dist/cjs/plugins/expoManifestRequestHandlerPlugin.cjs +0 -59
  122. package/dist/cjs/plugins/expoManifestRequestHandlerPlugin.native.cjs +0 -62
  123. package/dist/cjs/plugins/expoManifestRequestHandlerPlugin.native.js +0 -64
  124. package/dist/cjs/plugins/expoManifestRequestHandlerPlugin.native.js.map +0 -1
  125. package/dist/cjs/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.cjs +0 -35
  126. package/dist/cjs/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.native.cjs +0 -37
  127. package/dist/cjs/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.native.js +0 -39
  128. package/dist/cjs/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.native.js.map +0 -1
  129. package/dist/esm/plugins/expoManifestRequestHandlerPlugin.mjs +0 -40
  130. package/dist/esm/plugins/expoManifestRequestHandlerPlugin.mjs.map +0 -1
  131. package/dist/esm/plugins/expoManifestRequestHandlerPlugin.native.js +0 -41
  132. package/dist/esm/plugins/expoManifestRequestHandlerPlugin.native.js.map +0 -1
  133. package/dist/esm/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.mjs +0 -15
  134. package/dist/esm/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.mjs.map +0 -1
  135. package/dist/esm/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.native.js +0 -15
  136. package/dist/esm/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.native.js.map +0 -1
  137. package/src/plugins/expoManifestRequestHandlerPlugin.ts +0 -88
  138. package/src/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.ts +0 -28
  139. package/types/plugins/expoManifestRequestHandlerPlugin.d.ts +0 -14
  140. package/types/plugins/expoManifestRequestHandlerPlugin.d.ts.map +0 -1
  141. package/types/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.d.ts +0 -5
  142. package/types/utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName.d.ts.map +0 -1
@@ -1,24 +1,23 @@
1
- import type { ResolvedConfig } from 'vite'
1
+ import { searchForWorkspaceRoot, type ResolvedConfig } from 'vite'
2
2
  import { spawn } from 'node:child_process'
3
3
  import { resolve } from 'node:path'
4
4
  import micromatch from 'micromatch'
5
+ import { getDefaultConfig } from '@react-native/metro-config'
5
6
 
6
- // For Metro and Expo, we only import types here.
7
- // We use `projectImport` to dynamically import the actual modules
8
- // at runtime to ensure they are loaded from the user's project root.
7
+ // metro itself is loaded from the app so its cli and config stay aligned.
8
+ // the base config belongs to this package and matches its react-native peer.
9
9
  import type { loadConfig as loadConfigT } from 'metro'
10
- import type { getDefaultConfig as getDefaultConfigT } from '@expo/metro-config'
11
10
 
12
11
  import { findUserBabelConfig } from '@vxrn/compiler'
13
12
  import { projectImport, projectResolve } from '../utils/projectImport'
14
13
  import { getTerminalReporter } from '../utils/getTerminalReporter'
15
- import { patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName } from '../utils/patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName'
16
14
  import type { MetroPluginOptions } from '../plugins/metroPlugin'
17
15
  import type { ExtraConfig, MetroConfigExtended } from './types'
18
16
 
19
- type MetroInputConfig = Parameters<typeof loadConfigT>[1]
17
+ type MetroInputConfig = NonNullable<Parameters<typeof loadConfigT>[1]>
20
18
 
21
19
  const WATCHMAN_PROBE_TIMEOUT_MS = 2000
20
+ const expoEnvAdditionalExts = ['env', 'local', 'development']
22
21
  const watchmanResponsivePromises = new Map<string, Promise<boolean>>()
23
22
  let didWarnAboutWatchmanFallback = false
24
23
  const rootIndexBundleRequestPattern = /^(https?:\/\/[^/]+)?\/index\.bundle(?=$|[?#])/
@@ -32,6 +31,36 @@ const metroWatchExclusions = [
32
31
  /[/\\]node_modules[/\\]\.vxrn(?:[/\\]|$)/,
33
32
  ]
34
33
 
34
+ function enforceWorkspaceVisibility(
35
+ config: MetroInputConfig,
36
+ projectRoot: string
37
+ ): MetroInputConfig {
38
+ const workspaceRoot = searchForWorkspaceRoot(projectRoot)
39
+ if (workspaceRoot === projectRoot) return config
40
+
41
+ return {
42
+ ...config,
43
+ watchFolders: [...new Set([...(config.watchFolders ?? []), workspaceRoot])],
44
+ resolver: {
45
+ ...config.resolver,
46
+ nodeModulesPaths: [
47
+ ...new Set([
48
+ ...(config.resolver?.nodeModulesPaths ?? []),
49
+ resolve(projectRoot, 'node_modules'),
50
+ resolve(workspaceRoot, 'node_modules'),
51
+ ]),
52
+ ],
53
+ },
54
+ }
55
+ }
56
+
57
+ function getReactNativeDefaultConfig(projectRoot: string): MetroInputConfig {
58
+ return enforceWorkspaceVisibility(
59
+ getDefaultConfig(projectRoot) as MetroInputConfig,
60
+ projectRoot
61
+ )
62
+ }
63
+
35
64
  function getPlatformFromBundleUrl(url: string): 'ios' | 'android' {
36
65
  const platform = url.match(/[?&]platform=(ios|android)(?:&|$)/)?.[1]
37
66
  return platform === 'android' ? 'android' : 'ios'
@@ -45,13 +74,43 @@ function rewriteMainModuleBundleUrl(
45
74
  platform: getPlatformFromBundleUrl(url),
46
75
  })
47
76
 
48
- if (url.includes('/.expo/.virtual-metro-entry.bundle?')) {
49
- return url.replace('.expo/.virtual-metro-entry', resolvedMainModulePath)
50
- }
51
-
52
77
  return url.replace(rootIndexBundleRequestPattern, `$1/${resolvedMainModulePath}.bundle`)
53
78
  }
54
79
 
80
+ function isBareSpecifier(name: string) {
81
+ return name !== '' && !name.startsWith('.') && !name.startsWith('/')
82
+ }
83
+
84
+ // metro parses `/one/metro-entry.bundle` to the entry `./one/metro-entry`
85
+ // relative to the server root, which misses node_modules package lookup.
86
+ function bareMainModuleForRequest(
87
+ moduleName: string,
88
+ mainModuleName: string | undefined
89
+ ) {
90
+ if (!mainModuleName || !isBareSpecifier(mainModuleName)) return undefined
91
+ if (moduleName === `./${mainModuleName}`) return mainModuleName
92
+ return undefined
93
+ }
94
+
95
+ // defaultConfigOverrides and Metro config loading can replace the resolver.
96
+ // enforce package semantics on the final config while leaving every other
97
+ // request on the resolver selected by that composition.
98
+ function enforceBareMainModuleEntry(config: any, mainModuleName: string | undefined) {
99
+ if (!mainModuleName || !isBareSpecifier(mainModuleName)) return config
100
+ const innerResolveRequest = config?.resolver?.resolveRequest
101
+ return {
102
+ ...config,
103
+ resolver: {
104
+ ...config?.resolver,
105
+ resolveRequest: (context: any, moduleName: string, platform: string) => {
106
+ const bareMain = bareMainModuleForRequest(moduleName, mainModuleName)
107
+ const resolveRequest = innerResolveRequest || context.resolveRequest
108
+ return resolveRequest(context, bareMain ?? moduleName, platform)
109
+ },
110
+ },
111
+ }
112
+ }
113
+
55
114
  async function isWatchmanResponsive(projectRoot: string) {
56
115
  let probe = watchmanResponsivePromises.get(projectRoot)
57
116
  if (probe) {
@@ -135,7 +194,7 @@ function resolveNativeTransforms(
135
194
  * Build the Metro config input WITHOUT calling Metro's `loadConfig`. Returns
136
195
  * the same shape Metro `loadConfig` expects as its second argument. Use this
137
196
  * from a project's `metro.config.cjs` so the outer `loadConfig` (driven by
138
- * Expo CLI / Metro CLI) is the only one that runs — avoids infinite
197
+ * the Metro CLI) is the only one that runs. this avoids infinite
139
198
  * recursion that would happen if the inner pipeline also called `loadConfig`
140
199
  * and re-read the same metro.config.cjs.
141
200
  */
@@ -156,53 +215,20 @@ export async function buildMetroConfigInputFromViteConfig(
156
215
  )
157
216
  }
158
217
 
159
- const { getDefaultConfig } = await projectImport<{
160
- getDefaultConfig: typeof getDefaultConfigT
161
- }>(projectRoot, '@expo/metro-config')
162
-
163
- const _defaultConfig: MetroInputConfig = getDefaultConfig(projectRoot) as any
218
+ const _defaultConfig = getReactNativeDefaultConfig(projectRoot)
164
219
 
165
220
  if (mainModuleName) {
166
- const origRewriteRequestUrl = _defaultConfig!.server!.rewriteRequestUrl!
167
-
168
- const resolveMainModuleName: (p: { platform: 'ios' | 'android' }) => string =
169
- await (async () => {
170
- const ExpoGoManifestHandlerMiddleware = (
171
- await projectImport(
172
- projectRoot,
173
- '@expo/cli/build/src/start/server/middleware/ExpoGoManifestHandlerMiddleware.js'
174
- )
175
- ).default.ExpoGoManifestHandlerMiddleware
176
-
177
- const manifestHandlerMiddleware = new ExpoGoManifestHandlerMiddleware(
178
- projectRoot,
179
- {}
180
- )
181
-
182
- patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName(
183
- manifestHandlerMiddleware,
184
- mainModuleName
185
- )
186
-
187
- return (p) => {
188
- return manifestHandlerMiddleware.resolveMainModuleName({
189
- pkg: { main: mainModuleName },
190
- platform: p.platform,
191
- })
192
- }
193
- })()
221
+ const origRewriteRequestUrl = _defaultConfig!.server?.rewriteRequestUrl
222
+ const resolveMainModuleName = () => mainModuleName
194
223
 
195
224
  extraConfig.getResolveMainModuleName = resolveMainModuleName
196
225
 
197
226
  // @ts-expect-error Metro 0.83 made this read-only in types but we need to patch it
198
227
  _defaultConfig!.server!.rewriteRequestUrl = (url) => {
199
- if (
200
- url.includes('/.expo/.virtual-metro-entry.bundle?') ||
201
- rootIndexBundleRequestPattern.test(url)
202
- ) {
228
+ if (rootIndexBundleRequestPattern.test(url)) {
203
229
  return rewriteMainModuleBundleUrl(url, resolveMainModuleName)
204
230
  }
205
- return origRewriteRequestUrl(url)
231
+ return origRewriteRequestUrl?.(url) ?? url
206
232
  }
207
233
  }
208
234
 
@@ -233,6 +259,15 @@ export async function buildMetroConfigInputFromViteConfig(
233
259
 
234
260
  const defaultConfig: MetroInputConfig = {
235
261
  ..._defaultConfig,
262
+ watcher: {
263
+ ..._defaultConfig?.watcher,
264
+ additionalExts: [
265
+ ...new Set([
266
+ ...(_defaultConfig?.watcher?.additionalExts ?? []),
267
+ ...expoEnvAdditionalExts,
268
+ ]),
269
+ ],
270
+ },
236
271
  resolver: {
237
272
  ..._defaultConfig?.resolver,
238
273
  useWatchman,
@@ -269,6 +304,10 @@ export async function buildMetroConfigInputFromViteConfig(
269
304
  },
270
305
  transformer: {
271
306
  ..._defaultConfig?.transformer,
307
+ // one's router and Expo's optional development env module both use
308
+ // require.context. Metro installs a throwing runtime stub unless this
309
+ // graph feature is enabled.
310
+ unstable_allowRequireContext: true,
272
311
  babelTransformerPath: projectResolve(
273
312
  projectRoot,
274
313
  '@vxrn/vite-plugin-metro/babel-transformer'
@@ -287,7 +326,14 @@ export async function buildMetroConfigInputFromViteConfig(
287
326
  : defaultConfigOverrides),
288
327
  }
289
328
 
290
- return { defaultConfig: merged, projectRoot, extraConfig }
329
+ return {
330
+ defaultConfig: enforceBareMainModuleEntry(
331
+ enforceWorkspaceVisibility(merged, projectRoot),
332
+ mainModuleName
333
+ ),
334
+ projectRoot,
335
+ extraConfig,
336
+ }
291
337
  }
292
338
 
293
339
  export async function getMetroConfigFromViteConfig(
@@ -313,60 +359,21 @@ export async function getMetroConfigFromViteConfig(
313
359
  const { loadConfig } = await projectImport<{
314
360
  loadConfig: typeof loadConfigT
315
361
  }>(projectRoot, 'metro')
316
- const { getDefaultConfig } = await projectImport<{
317
- getDefaultConfig: typeof getDefaultConfigT
318
- }>(projectRoot, '@expo/metro-config')
319
-
320
- const _defaultConfig: MetroInputConfig = getDefaultConfig(projectRoot) as any
362
+ const _defaultConfig = getReactNativeDefaultConfig(projectRoot)
321
363
 
322
364
  if (mainModuleName) {
323
- const origRewriteRequestUrl = _defaultConfig!.server!.rewriteRequestUrl!
324
-
325
- // We need to patch Expo's default `config.server.rewriteRequestUrl`
326
- // to change how URLs like '/.expo/.virtual-metro-entry.bundle?' are
327
- // rewritten.
328
- // But since that function is difficult to override, here we borrow
329
- // the ExpoGoManifestHandlerMiddleware and use it to resolve the
330
- // URL to the main module name.
331
- const resolveMainModuleName: (p: { platform: 'ios' | 'android' }) => string =
332
- await (async () => {
333
- const ExpoGoManifestHandlerMiddleware = (
334
- await projectImport(
335
- projectRoot,
336
- '@expo/cli/build/src/start/server/middleware/ExpoGoManifestHandlerMiddleware.js'
337
- )
338
- ).default.ExpoGoManifestHandlerMiddleware
339
-
340
- const manifestHandlerMiddleware = new ExpoGoManifestHandlerMiddleware(
341
- projectRoot,
342
- {}
343
- )
344
-
345
- patchExpoGoManifestHandlerMiddlewareWithCustomMainModuleName(
346
- manifestHandlerMiddleware,
347
- mainModuleName
348
- )
349
-
350
- return (p) => {
351
- return manifestHandlerMiddleware.resolveMainModuleName({
352
- pkg: { main: mainModuleName },
353
- platform: p.platform,
354
- })
355
- }
356
- })()
365
+ const origRewriteRequestUrl = _defaultConfig!.server?.rewriteRequestUrl
366
+ const resolveMainModuleName = () => mainModuleName
357
367
 
358
368
  extraConfig.getResolveMainModuleName = resolveMainModuleName
359
369
 
360
370
  // @ts-expect-error Metro 0.83 made this read-only in types but we need to patch it
361
371
  _defaultConfig!.server!.rewriteRequestUrl = (url) => {
362
- if (
363
- url.includes('/.expo/.virtual-metro-entry.bundle?') ||
364
- rootIndexBundleRequestPattern.test(url)
365
- ) {
372
+ if (rootIndexBundleRequestPattern.test(url)) {
366
373
  return rewriteMainModuleBundleUrl(url, resolveMainModuleName)
367
374
  }
368
375
 
369
- return origRewriteRequestUrl(url)
376
+ return origRewriteRequestUrl?.(url) ?? url
370
377
  }
371
378
  }
372
379
 
@@ -397,6 +404,15 @@ export async function getMetroConfigFromViteConfig(
397
404
 
398
405
  const defaultConfig: MetroInputConfig = {
399
406
  ..._defaultConfig,
407
+ watcher: {
408
+ ..._defaultConfig?.watcher,
409
+ additionalExts: [
410
+ ...new Set([
411
+ ...(_defaultConfig?.watcher?.additionalExts ?? []),
412
+ ...expoEnvAdditionalExts,
413
+ ]),
414
+ ],
415
+ },
400
416
  resolver: {
401
417
  ..._defaultConfig?.resolver,
402
418
  useWatchman,
@@ -441,6 +457,10 @@ export async function getMetroConfigFromViteConfig(
441
457
  },
442
458
  transformer: {
443
459
  ..._defaultConfig?.transformer,
460
+ // one's router and Expo's optional development env module both use
461
+ // require.context. Metro installs a throwing runtime stub unless this
462
+ // graph feature is enabled.
463
+ unstable_allowRequireContext: true,
444
464
  babelTransformerPath: projectResolve(
445
465
  projectRoot,
446
466
  '@vxrn/vite-plugin-metro/babel-transformer'
@@ -468,7 +488,10 @@ export async function getMetroConfigFromViteConfig(
468
488
  )
469
489
 
470
490
  return {
471
- ...metroConfig,
491
+ ...enforceBareMainModuleEntry(
492
+ enforceWorkspaceVisibility(metroConfig, projectRoot),
493
+ mainModuleName
494
+ ),
472
495
  ...extraConfig,
473
496
  } as MetroConfigExtended
474
497
  }
@@ -9,8 +9,7 @@ import { checkAndClearMetroCacheFromVite } from '../utils/metroCacheManager'
9
9
 
10
10
  const { debug } = createDebugger('vite-plugin-metro')
11
11
 
12
- // For Metro and Expo, we only import types here.
13
- // We use `projectImport` to dynamically import the actual modules
12
+ // for Metro, we only import types here. `projectImport` loads the actual modules
14
13
  // at runtime to ensure they are loaded from the user's project root.
15
14
  import type MetroT from 'metro'
16
15
  import type { loadConfig as loadConfigT } from 'metro'
@@ -72,10 +71,8 @@ export type MetroPluginOptions = {
72
71
  /**
73
72
  * Overrides the main module name which is normally defined as the `main` field in `package.json`.
74
73
  *
75
- * This will affect how `/.expo/.virtual-metro-entry.bundle` behaves.
76
- *
77
- * It can be used to change the entry point of the React Native app without the need of using
78
- * the `main` field in `package.json`.
74
+ * Changes the React Native entry point without requiring a `main` field in
75
+ * `package.json`.
79
76
  */
80
77
  mainModuleName?: string
81
78
  /**
@@ -0,0 +1,118 @@
1
+ import path from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import vm from 'node:vm'
4
+ import { describe, expect, it } from 'vitest'
5
+
6
+ import { transformFromAstSync } from './babel-core'
7
+ // @ts-expect-error: module.exports = has no default export in types, runtime interop provides it
8
+ import babelTransformer from './babel-transformer'
9
+
10
+ const fixturesDir = path.join(
11
+ path.dirname(fileURLToPath(import.meta.url)),
12
+ '__fixtures__'
13
+ )
14
+ const virtualFilename = '/proj/node_modules/expo/virtual/env.js'
15
+ const virtualSource = 'export const env = process.env;'
16
+
17
+ function runBabelTransform(src: string, filename: string, dev: boolean): any {
18
+ // the transformer leaks BABEL_ENV=development when it was previously
19
+ // unset; keep that pre-existing leak out of sibling tests.
20
+ const OLD_BABEL_ENV = process.env.BABEL_ENV
21
+ try {
22
+ return (babelTransformer as any).transform({
23
+ filename,
24
+ src,
25
+ options: {
26
+ projectRoot: fixturesDir,
27
+ dev,
28
+ platform: 'ios',
29
+ customTransformOptions: {
30
+ environment: 'client',
31
+ vite: {
32
+ oneViteMetroBabelConfig: true,
33
+ babelConfig: {
34
+ babelrc: false,
35
+ configFile: false,
36
+ presets: [],
37
+ plugins: [],
38
+ },
39
+ },
40
+ },
41
+ },
42
+ plugins: [],
43
+ })
44
+ } finally {
45
+ if (OLD_BABEL_ENV === undefined) {
46
+ delete process.env.BABEL_ENV
47
+ } else {
48
+ process.env.BABEL_ENV = OLD_BABEL_ENV
49
+ }
50
+ }
51
+ }
52
+
53
+ function codegen(ast: any, filename: string): string {
54
+ const out = transformFromAstSync(ast, virtualSource, {
55
+ filename,
56
+ babelrc: false,
57
+ configFile: false,
58
+ code: true,
59
+ ast: false,
60
+ })
61
+ if (!out?.code) {
62
+ throw new Error('codegen produced no code')
63
+ }
64
+ return out.code
65
+ }
66
+
67
+ function runCjs(
68
+ code: string,
69
+ opts: { requireImpl: (...args: any[]) => any; procEnv: Record<string, string> }
70
+ ) {
71
+ const module = { exports: {} as any }
72
+ vm.runInNewContext(code, {
73
+ require: opts.requireImpl,
74
+ module,
75
+ exports: module.exports,
76
+ process: { env: opts.procEnv },
77
+ console,
78
+ })
79
+ return module.exports
80
+ }
81
+
82
+ const noRequire = () => {
83
+ throw new Error('unexpected require call')
84
+ }
85
+
86
+ describe('babel-transformer optional expo virtual env', () => {
87
+ it('merges .env files over process.env in dev like upstream', () => {
88
+ const result = runBabelTransform(virtualSource, virtualFilename, true)
89
+ expect(result.ast).toBeTruthy()
90
+ const code = codegen(result.ast, virtualFilename)
91
+ expect(code).toContain('require.context')
92
+ expect(code).toContain('.env.development.local')
93
+ const contextModule: any = () => ({
94
+ default: { EXPO_PUBLIC_SENTINEL_DOTENV: 'dotenv-sentinel-value' },
95
+ })
96
+ contextModule.keys = () => ['./.env']
97
+ const requireImpl: any = noRequire
98
+ requireImpl.context = () => contextModule
99
+ const mod = runCjs(code, {
100
+ requireImpl,
101
+ procEnv: { EXPO_PUBLIC_SENTINEL_PROC: 'proc-sentinel-value' },
102
+ })
103
+ expect(mod.env.EXPO_PUBLIC_SENTINEL_DOTENV).toBe('dotenv-sentinel-value')
104
+ expect(mod.env.EXPO_PUBLIC_SENTINEL_PROC).toBe('proc-sentinel-value')
105
+ })
106
+
107
+ it('throws a naming error in production like upstream', () => {
108
+ const result = runBabelTransform(virtualSource, virtualFilename, false)
109
+ expect(result.ast).toBeTruthy()
110
+ const mod = runCjs(codegen(result.ast, virtualFilename), {
111
+ requireImpl: noRequire,
112
+ procEnv: {},
113
+ })
114
+ expect(() => mod.env.EXPO_PUBLIC_ANYTHING).toThrow(
115
+ 'not supported in production bundles'
116
+ )
117
+ })
118
+ })
@@ -5,20 +5,27 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- // A fork of the upstream babel-transformer that uses Expo-specific babel defaults
9
- // and adds support for web and Node.js environments via `isServer` on the Babel caller.
8
+ // a fork of the upstream babel-transformer that adds support for web and node
9
+ // environments via `isServer` on the babel caller.
10
10
  // See:
11
11
  // * https://github.com/facebook/metro/blob/main/packages/metro-babel-transformer/src/index.js
12
- // * https://github.com/expo/expo/blob/main/packages/%40expo/metro-config/src/babel-transformer.ts
13
- import type { BabelTransformer, BabelTransformerArgs } from 'metro-babel-transformer'
12
+ import type {
13
+ BabelTransformer,
14
+ BabelTransformerArgs,
15
+ BabelTransformerCacheKeyOptions,
16
+ } from 'metro-babel-transformer'
14
17
  import assert from 'node:assert'
18
+ import { createHash } from 'node:crypto'
19
+ import { readFileSync } from 'node:fs'
20
+ import path from 'node:path'
15
21
 
16
22
  import type { TransformOptions } from './babel-core'
23
+ import { substituteExpoVirtualEnvSource } from './expoVirtualEnv'
17
24
  import { loadBabelConfig } from './loadBabelConfig'
18
25
  import { transformSync } from './transformSync'
19
26
  import type { ViteCustomTransformOptions } from './types'
20
27
 
21
- export type ExpoBabelCaller = TransformOptions['caller'] & {
28
+ export type MetroBabelCaller = TransformOptions['caller'] & {
22
29
  supportsReactCompiler?: boolean
23
30
  isReactServer?: boolean
24
31
  isHMREnabled?: boolean
@@ -37,7 +44,7 @@ export type ExpoBabelCaller = TransformOptions['caller'] & {
37
44
  }
38
45
 
39
46
  const debug = require('debug')(
40
- 'expo:metro-config:babel-transformer'
47
+ 'vxrn:metro-config:babel-transformer'
41
48
  ) as typeof console.log
42
49
 
43
50
  function isCustomTruthy(value: any): boolean {
@@ -67,7 +74,7 @@ function getBabelCaller({
67
74
  oneViteMetroBabelConfig,
68
75
  }: Pick<BabelTransformerArgs, 'filename' | 'options'> & {
69
76
  oneViteMetroBabelConfig: boolean
70
- }): ExpoBabelCaller {
77
+ }): MetroBabelCaller {
71
78
  const isNodeModule = filename.includes('node_modules')
72
79
  const isReactServer = options.customTransformOptions?.environment === 'react-server'
73
80
  const isGenericServer = options.customTransformOptions?.environment === 'node'
@@ -80,7 +87,7 @@ function getBabelCaller({
80
87
 
81
88
  if (routerRoot == null) {
82
89
  memoizeWarning(
83
- 'Warning: Missing transform.routerRoot option in Metro bundling request, falling back to `app` as routes directory. This can occur if you bundle without Expo CLI or expo/metro-config.'
90
+ 'Warning: Missing transform.routerRoot option in Metro bundling request, falling back to `app` as routes directory.'
84
91
  )
85
92
  }
86
93
 
@@ -119,7 +126,7 @@ function getBabelCaller({
119
126
  // target environment.
120
127
  engine: stringOrUndefined(options.customTransformOptions?.engine),
121
128
 
122
- // Provide the project root for accurately reading the Expo config.
129
+ // provide the project root for reading the app babel config
123
130
  projectRoot: options.projectRoot,
124
131
  oneViteMetroBabelConfig,
125
132
 
@@ -146,17 +153,28 @@ function stringOrUndefined(value: unknown): string | undefined {
146
153
 
147
154
  const transform: BabelTransformer['transform'] = ({
148
155
  filename,
149
- src,
156
+ src: originalSrc,
150
157
  options,
151
- // `plugins` is used for `functionMapBabelPlugin` from `metro-source-map`. Could make sense to move this to `babel-preset-expo` too.
158
+ // `plugins` is used for `functionMapBabelPlugin` from `metro-source-map`.
152
159
  plugins,
153
160
  }: BabelTransformerArgs): ReturnType<BabelTransformer['transform']> => {
161
+ // narrow optional Expo compatibility (expo/virtual/env.js and .env files
162
+ // only), shared with the native worker. no-Expo apps pass through byte
163
+ // for byte without resolving any Expo module.
164
+ const src = substituteExpoVirtualEnvSource({
165
+ filename,
166
+ src: originalSrc,
167
+ projectRoot: options.projectRoot,
168
+ dev: options.dev,
169
+ environment: options.customTransformOptions?.environment,
170
+ })
171
+
154
172
  const viteCustomTransformOptions = options.customTransformOptions?.vite
155
173
 
156
174
  const customOptionsFromVite: ViteCustomTransformOptions = (() => {
157
175
  const c: any = viteCustomTransformOptions
158
- // Standalone Metro invocations (expo export, eas update) don't set
159
- // customTransformOptions.vite — the plugins flow entirely through the
176
+ // standalone Metro invocations don't set
177
+ // customTransformOptions.vite. the plugins flow entirely through the
160
178
  // project's babel.config.cjs in that case. Tolerate the missing field
161
179
  // rather than throwing so a single Metro config can serve both the
162
180
  // Vite-driven and standalone paths.
@@ -210,8 +228,8 @@ const transform: BabelTransformer['transform'] = ({
210
228
  // all (most) of the transforms in their local Babel config.
211
229
  // This also helps us keep the transform layers small and focused on a single task. We can also use this to
212
230
  // ensure the Babel config caching is more accurate.
213
- // Additionally, by moving everything Babel-related to the Babel preset, it makes it easier for users to reason
214
- // about the requirements of an Expo project, making it easier to migrate to other transpilers in the future.
231
+ // moving everything babel-related to the preset also keeps the transform
232
+ // requirements explicit for users who opt into babel.
215
233
  caller: getBabelCaller({
216
234
  filename,
217
235
  options,
@@ -238,8 +256,57 @@ const transform: BabelTransformer['transform'] = ({
238
256
  }
239
257
  }
240
258
 
259
+ export function getCacheKey(options?: BabelTransformerCacheKeyOptions): string {
260
+ const hash = createHash('sha256')
261
+ hash.update('vxrn-metro-babel-transformer-v1')
262
+
263
+ const projectRoot = options?.projectRoot
264
+ if (!projectRoot) {
265
+ return hash.digest('hex')
266
+ }
267
+
268
+ hash.update(projectRoot)
269
+
270
+ for (const file of [
271
+ '.env',
272
+ '.env.development',
273
+ '.env.local',
274
+ '.env.development.local',
275
+ ]) {
276
+ const filename = path.join(projectRoot, file)
277
+ hash.update(filename)
278
+ try {
279
+ hash.update(readFileSync(filename))
280
+ } catch (error) {
281
+ if (
282
+ typeof error === 'object' &&
283
+ error !== null &&
284
+ 'code' in error &&
285
+ error.code === 'ENOENT'
286
+ ) {
287
+ continue
288
+ }
289
+ throw error
290
+ }
291
+ }
292
+
293
+ hash.update(
294
+ JSON.stringify(
295
+ Object.entries(process.env)
296
+ .filter(
297
+ ([key]) =>
298
+ key.startsWith('ONE_PUBLIC_') || key.startsWith('EXPO_PUBLIC_')
299
+ )
300
+ .sort(([left], [right]) => left.localeCompare(right))
301
+ )
302
+ )
303
+
304
+ return hash.digest('hex')
305
+ }
306
+
241
307
  const babelTransformer: BabelTransformer = {
242
308
  transform,
309
+ getCacheKey,
243
310
  }
244
311
 
245
312
  module.exports = babelTransformer