one 1.16.12 → 1.17.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 (157) hide show
  1. package/dist/cjs/babel-preset/index.cjs +154 -0
  2. package/dist/cjs/babel-preset/index.native.js +186 -0
  3. package/dist/cjs/babel-preset/index.native.js.map +1 -0
  4. package/dist/cjs/babel-preset/index.test.cjs +143 -0
  5. package/dist/cjs/babel-preset/index.test.native.js +165 -0
  6. package/dist/cjs/babel-preset/index.test.native.js.map +1 -0
  7. package/dist/cjs/babel-preset/integration.test.cjs +94 -0
  8. package/dist/cjs/babel-preset/integration.test.native.js +97 -0
  9. package/dist/cjs/babel-preset/integration.test.native.js.map +1 -0
  10. package/dist/cjs/cli/generateBundlerConfig.cjs +247 -0
  11. package/dist/cjs/cli/generateBundlerConfig.native.js +316 -0
  12. package/dist/cjs/cli/generateBundlerConfig.native.js.map +1 -0
  13. package/dist/cjs/cli/generateBundlerConfig.test.cjs +350 -0
  14. package/dist/cjs/cli/generateBundlerConfig.test.native.js +380 -0
  15. package/dist/cjs/cli/generateBundlerConfig.test.native.js.map +1 -0
  16. package/dist/cjs/cli/patch.cjs +19 -2
  17. package/dist/cjs/cli/patch.native.js +26 -2
  18. package/dist/cjs/cli/patch.native.js.map +1 -1
  19. package/dist/cjs/cli/patch.test.cjs +56 -0
  20. package/dist/cjs/cli/patch.test.native.js +65 -0
  21. package/dist/cjs/cli/patch.test.native.js.map +1 -0
  22. package/dist/cjs/cli.cjs +39 -2
  23. package/dist/cjs/cli.native.js +40 -2
  24. package/dist/cjs/cli.native.js.map +1 -1
  25. package/dist/cjs/metro-config/buildOneMetroResolverOverrides.cjs +102 -0
  26. package/dist/cjs/metro-config/buildOneMetroResolverOverrides.native.js +109 -0
  27. package/dist/cjs/metro-config/buildOneMetroResolverOverrides.native.js.map +1 -0
  28. package/dist/cjs/metro-config/getViteMetroPluginOptions.cjs +17 -150
  29. package/dist/cjs/metro-config/getViteMetroPluginOptions.integration.test.cjs +84 -0
  30. package/dist/cjs/metro-config/getViteMetroPluginOptions.integration.test.native.js +90 -0
  31. package/dist/cjs/metro-config/getViteMetroPluginOptions.integration.test.native.js.map +1 -0
  32. package/dist/cjs/metro-config/getViteMetroPluginOptions.native.js +17 -158
  33. package/dist/cjs/metro-config/getViteMetroPluginOptions.native.js.map +1 -1
  34. package/dist/cjs/metro-config/withOne.cjs +82 -0
  35. package/dist/cjs/metro-config/withOne.native.js +88 -0
  36. package/dist/cjs/metro-config/withOne.native.js.map +1 -0
  37. package/dist/cjs/metro-config/withOne.test.cjs +129 -0
  38. package/dist/cjs/metro-config/withOne.test.native.js +156 -0
  39. package/dist/cjs/metro-config/withOne.test.native.js.map +1 -0
  40. package/dist/cjs/vite/loadConfig.cjs +20 -1
  41. package/dist/cjs/vite/loadConfig.native.js +20 -1
  42. package/dist/cjs/vite/loadConfig.native.js.map +1 -1
  43. package/dist/cjs/vite/plugins/warmRoutesPlugin.cjs +13 -7
  44. package/dist/cjs/vite/plugins/warmRoutesPlugin.native.js +13 -7
  45. package/dist/cjs/vite/plugins/warmRoutesPlugin.native.js.map +1 -1
  46. package/dist/esm/babel-preset/index.mjs +116 -0
  47. package/dist/esm/babel-preset/index.mjs.map +1 -0
  48. package/dist/esm/babel-preset/index.native.js +145 -0
  49. package/dist/esm/babel-preset/index.native.js.map +1 -0
  50. package/dist/esm/babel-preset/index.test.mjs +120 -0
  51. package/dist/esm/babel-preset/index.test.mjs.map +1 -0
  52. package/dist/esm/babel-preset/index.test.native.js +139 -0
  53. package/dist/esm/babel-preset/index.test.native.js.map +1 -0
  54. package/dist/esm/babel-preset/integration.test.mjs +71 -0
  55. package/dist/esm/babel-preset/integration.test.mjs.map +1 -0
  56. package/dist/esm/babel-preset/integration.test.native.js +71 -0
  57. package/dist/esm/babel-preset/integration.test.native.js.map +1 -0
  58. package/dist/esm/cli/generateBundlerConfig.mjs +207 -0
  59. package/dist/esm/cli/generateBundlerConfig.mjs.map +1 -0
  60. package/dist/esm/cli/generateBundlerConfig.native.js +273 -0
  61. package/dist/esm/cli/generateBundlerConfig.native.js.map +1 -0
  62. package/dist/esm/cli/generateBundlerConfig.test.mjs +327 -0
  63. package/dist/esm/cli/generateBundlerConfig.test.mjs.map +1 -0
  64. package/dist/esm/cli/generateBundlerConfig.test.native.js +354 -0
  65. package/dist/esm/cli/generateBundlerConfig.test.native.js.map +1 -0
  66. package/dist/esm/cli/patch.mjs +19 -2
  67. package/dist/esm/cli/patch.mjs.map +1 -1
  68. package/dist/esm/cli/patch.native.js +26 -2
  69. package/dist/esm/cli/patch.native.js.map +1 -1
  70. package/dist/esm/cli/patch.test.mjs +57 -0
  71. package/dist/esm/cli/patch.test.mjs.map +1 -0
  72. package/dist/esm/cli/patch.test.native.js +63 -0
  73. package/dist/esm/cli/patch.test.native.js.map +1 -0
  74. package/dist/esm/cli.mjs +39 -2
  75. package/dist/esm/cli.mjs.map +1 -1
  76. package/dist/esm/cli.native.js +40 -2
  77. package/dist/esm/cli.native.js.map +1 -1
  78. package/dist/esm/metro-config/buildOneMetroResolverOverrides.mjs +66 -0
  79. package/dist/esm/metro-config/buildOneMetroResolverOverrides.mjs.map +1 -0
  80. package/dist/esm/metro-config/buildOneMetroResolverOverrides.native.js +70 -0
  81. package/dist/esm/metro-config/buildOneMetroResolverOverrides.native.js.map +1 -0
  82. package/dist/esm/metro-config/getViteMetroPluginOptions.integration.test.mjs +61 -0
  83. package/dist/esm/metro-config/getViteMetroPluginOptions.integration.test.mjs.map +1 -0
  84. package/dist/esm/metro-config/getViteMetroPluginOptions.integration.test.native.js +64 -0
  85. package/dist/esm/metro-config/getViteMetroPluginOptions.integration.test.native.js.map +1 -0
  86. package/dist/esm/metro-config/getViteMetroPluginOptions.mjs +17 -138
  87. package/dist/esm/metro-config/getViteMetroPluginOptions.mjs.map +1 -1
  88. package/dist/esm/metro-config/getViteMetroPluginOptions.native.js +17 -146
  89. package/dist/esm/metro-config/getViteMetroPluginOptions.native.js.map +1 -1
  90. package/dist/esm/metro-config/withOne.mjs +45 -0
  91. package/dist/esm/metro-config/withOne.mjs.map +1 -0
  92. package/dist/esm/metro-config/withOne.native.js +48 -0
  93. package/dist/esm/metro-config/withOne.native.js.map +1 -0
  94. package/dist/esm/metro-config/withOne.test.mjs +106 -0
  95. package/dist/esm/metro-config/withOne.test.mjs.map +1 -0
  96. package/dist/esm/metro-config/withOne.test.native.js +130 -0
  97. package/dist/esm/metro-config/withOne.test.native.js.map +1 -0
  98. package/dist/esm/vite/loadConfig.mjs +20 -1
  99. package/dist/esm/vite/loadConfig.mjs.map +1 -1
  100. package/dist/esm/vite/loadConfig.native.js +20 -1
  101. package/dist/esm/vite/loadConfig.native.js.map +1 -1
  102. package/dist/esm/vite/plugins/warmRoutesPlugin.mjs +13 -7
  103. package/dist/esm/vite/plugins/warmRoutesPlugin.mjs.map +1 -1
  104. package/dist/esm/vite/plugins/warmRoutesPlugin.native.js +13 -7
  105. package/dist/esm/vite/plugins/warmRoutesPlugin.native.js.map +1 -1
  106. package/package.json +20 -9
  107. package/src/babel-preset/index.test.ts +148 -0
  108. package/src/babel-preset/index.ts +250 -0
  109. package/src/babel-preset/integration.test.ts +91 -0
  110. package/src/cli/generateBundlerConfig.test.ts +343 -0
  111. package/src/cli/generateBundlerConfig.ts +339 -0
  112. package/src/cli/patch.test.ts +65 -0
  113. package/src/cli/patch.ts +30 -2
  114. package/src/cli.ts +31 -0
  115. package/src/metro-config/buildOneMetroResolverOverrides.ts +104 -0
  116. package/src/metro-config/getViteMetroPluginOptions.integration.test.ts +75 -0
  117. package/src/metro-config/getViteMetroPluginOptions.ts +15 -230
  118. package/src/metro-config/withOne.test.ts +120 -0
  119. package/src/metro-config/withOne.ts +112 -0
  120. package/src/vite/loadConfig.ts +22 -0
  121. package/src/vite/plugins/warmRoutesPlugin.ts +22 -6
  122. package/types/babel-preset/index.d.ts +68 -0
  123. package/types/babel-preset/index.d.ts.map +1 -0
  124. package/types/babel-preset/index.test.d.ts +2 -0
  125. package/types/babel-preset/index.test.d.ts.map +1 -0
  126. package/types/babel-preset/integration.test.d.ts +2 -0
  127. package/types/babel-preset/integration.test.d.ts.map +1 -0
  128. package/types/cli/generateBundlerConfig.d.ts +61 -0
  129. package/types/cli/generateBundlerConfig.d.ts.map +1 -0
  130. package/types/cli/generateBundlerConfig.test.d.ts +2 -0
  131. package/types/cli/generateBundlerConfig.test.d.ts.map +1 -0
  132. package/types/cli/patch.d.ts.map +1 -1
  133. package/types/cli/patch.test.d.ts +2 -0
  134. package/types/cli/patch.test.d.ts.map +1 -0
  135. package/types/metro-config/buildOneMetroResolverOverrides.d.ts +20 -0
  136. package/types/metro-config/buildOneMetroResolverOverrides.d.ts.map +1 -0
  137. package/types/metro-config/getViteMetroPluginOptions.d.ts +0 -5
  138. package/types/metro-config/getViteMetroPluginOptions.d.ts.map +1 -1
  139. package/types/metro-config/getViteMetroPluginOptions.integration.test.d.ts +2 -0
  140. package/types/metro-config/getViteMetroPluginOptions.integration.test.d.ts.map +1 -0
  141. package/types/metro-config/withOne.d.ts +44 -0
  142. package/types/metro-config/withOne.d.ts.map +1 -0
  143. package/types/metro-config/withOne.test.d.ts +2 -0
  144. package/types/metro-config/withOne.test.d.ts.map +1 -0
  145. package/types/vite/loadConfig.d.ts +1 -0
  146. package/types/vite/loadConfig.d.ts.map +1 -1
  147. package/types/vite/plugins/warmRoutesPlugin.d.ts.map +1 -1
  148. package/dist/cjs/metro-config/getViteMetroPluginOptions.test.cjs +0 -23
  149. package/dist/cjs/metro-config/getViteMetroPluginOptions.test.native.js +0 -26
  150. package/dist/cjs/metro-config/getViteMetroPluginOptions.test.native.js.map +0 -1
  151. package/dist/esm/metro-config/getViteMetroPluginOptions.test.mjs +0 -24
  152. package/dist/esm/metro-config/getViteMetroPluginOptions.test.mjs.map +0 -1
  153. package/dist/esm/metro-config/getViteMetroPluginOptions.test.native.js +0 -24
  154. package/dist/esm/metro-config/getViteMetroPluginOptions.test.native.js.map +0 -1
  155. package/src/metro-config/getViteMetroPluginOptions.test.ts +0 -34
  156. package/types/metro-config/getViteMetroPluginOptions.test.d.ts +0 -2
  157. package/types/metro-config/getViteMetroPluginOptions.test.d.ts.map +0 -1
@@ -1,20 +1,6 @@
1
- import module from 'node:module'
2
- import path from 'node:path'
3
1
  import type { metroPlugin } from '@vxrn/vite-plugin-metro'
4
- import mm from 'micromatch'
5
- import tsconfigPaths from 'tsconfig-paths'
6
- import {
7
- API_ROUTE_GLOB_PATTERN,
8
- ROUTE_NATIVE_EXCLUSION_GLOB_PATTERNS,
9
- } from '../router/glob-patterns'
10
-
11
- /**
12
- * On Windows, micromatch.makeRe() produces regex patterns with `[\\/]` or `[^\\/]`
13
- * instead of `\/` and `[^/]`. Normalize them so the startsWith check works.
14
- */
15
- export function normalizeReSource(source: string): string {
16
- return source.replace(/\[\\\\\/\]/g, '\\/').replace(/\[\^\\\\\/\]/g, '[^/]')
17
- }
2
+ import { buildOneBabelPlugins } from '../babel-preset'
3
+ import { buildOneMetroResolverOverrides } from './buildOneMetroResolverOverrides'
18
4
 
19
5
  export function getViteMetroPluginOptions({
20
6
  projectRoot,
@@ -33,156 +19,12 @@ export function getViteMetroPluginOptions({
33
19
  >['defaultConfigOverrides']
34
20
  setupFile?: string | { native?: string; ios?: string; android?: string }
35
21
  }): Parameters<typeof metroPlugin>[0] {
36
- const tsconfigPathsConfigLoadResult = tsconfigPaths.loadConfig(projectRoot)
37
-
38
- if (tsconfigPathsConfigLoadResult.resultType === 'failed') {
39
- throw new Error('tsconfigPathsConfigLoadResult.resultType is not success')
40
- }
41
-
42
- const require = module.createRequire(projectRoot)
43
- const emptyPath = require.resolve('@vxrn/vite-plugin-metro/empty', {
44
- paths: [projectRoot],
45
- })
46
-
47
- const metroEntryPath = require.resolve('one/metro-entry', {
48
- paths: [projectRoot],
49
- })
50
-
51
- const routerRequireContextRegexString = (() => {
52
- const excludeRes = [
53
- ...(ignoredRouteFiles || []).map((pattern) => mm.makeRe(pattern)),
54
- ...ROUTE_NATIVE_EXCLUSION_GLOB_PATTERNS.map((pattern) => mm.makeRe(pattern)),
55
- mm.makeRe(API_ROUTE_GLOB_PATTERN),
56
- ]
57
-
58
- const supportedRegexMustStartWith = String.raw`^(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?`
59
- // biome-ignore lint/complexity/noUselessStringRaw: keep original code
60
- const supportedRegexMustEndWith = String.raw`)$`
61
-
62
- const negativeLookaheadGroups = excludeRes.map((re, i) => {
63
- /**
64
- * Example:
65
- * ```
66
- * ^(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\+api\.(ts|tsx))$
67
- * ```
68
- */
69
- const reSource = normalizeReSource(re.source)
70
-
71
- if (
72
- !(
73
- reSource.startsWith(supportedRegexMustStartWith) &&
74
- reSource.endsWith(supportedRegexMustEndWith)
75
- )
76
- ) {
77
- const ignoredRouteFile = ignoredRouteFiles?.[i]
78
-
79
- if (ignoredRouteFile) {
80
- throw new Error(
81
- `[one/metro] ignoredRouteFile pattern "${ignoredRouteFile}" is not supported. We cannot process the corresponding regex "${reSource}" for now.`
82
- )
83
- }
84
-
85
- throw new Error(`Unsupported regex "${reSource}" in "ignoredRouteFiles".`)
86
- }
87
-
88
- const rePart = reSource.slice(
89
- supportedRegexMustStartWith.length,
90
- reSource.length - supportedRegexMustEndWith.length
91
- )
92
-
93
- // biome-ignore lint/complexity/noUselessStringRaw: keep original code
94
- return String.raw`(?:.*${rePart})`
95
- })
96
-
97
- return String.raw`^(?:\.\/)(?!${negativeLookaheadGroups.join('|')}$).*\.tsx?$`
98
- })()
22
+ const applyOneResolverOverrides = buildOneMetroResolverOverrides({ projectRoot })
99
23
 
100
24
  return {
25
+ oneViteMetroBabelConfig: true,
101
26
  defaultConfigOverrides: (defaultConfig) => {
102
- let config: typeof defaultConfig = {
103
- ...defaultConfig,
104
- resolver: {
105
- ...defaultConfig?.resolver,
106
- extraNodeModules: {
107
- ...defaultConfig?.resolver?.extraNodeModules,
108
- // "vite-tsconfig-paths" for Metro
109
- // Commenting out since we are using babel-plugin-module-resolver alias instead
110
- // ...Object.fromEntries(
111
- // Object.entries(tsconfigPathsConfigLoadResult.paths)
112
- // .map(([k, v]) => {
113
- // if (k.endsWith('/*') && v[0]?.endsWith('/*')) {
114
- // const key = k.replace(/\/\*$/, '')
115
- // let value = v[0].replace(/\/\*$/, '')
116
-
117
- // value = path.join(tsconfigPathsConfigLoadResult.absoluteBaseUrl, value)
118
-
119
- // return [key, value]
120
- // }
121
- // })
122
- // .filter((i): i is NonNullable<typeof i> => !!i)
123
- // ),
124
- },
125
- nodeModulesPaths: defaultConfig?.resolver?.nodeModulesPaths,
126
- resolveRequest: (context, moduleName, platform) => {
127
- if (moduleName.endsWith('.css')) {
128
- return {
129
- type: 'sourceFile',
130
- filePath: emptyPath,
131
- }
132
- }
133
-
134
- // On Vite side this is done by excludeAPIAndMiddlewareRoutesPlugin
135
- if (/_middleware.tsx?$/.test(moduleName)) {
136
- return {
137
- type: 'sourceFile',
138
- filePath: emptyPath,
139
- }
140
- }
141
-
142
- // server-only files should never be in the native bundle.
143
- // metro follows dynamic import chains (e.g. zero models →
144
- // server effects → server packages) and tries to resolve
145
- // everything, even though the code only runs on the server.
146
- if (/\.server(\.[jt]sx?)?$/.test(moduleName)) {
147
- return {
148
- type: 'sourceFile',
149
- filePath: emptyPath,
150
- }
151
- }
152
-
153
- // react-native-svg's package.json has "react-native": "src/index.ts"
154
- // which points to TS source that only type-exports Svg/Circle/Path etc.
155
- // force resolution to the compiled JS which has proper named value exports.
156
- if (moduleName === 'react-native-svg') {
157
- const defaultResolveRequest =
158
- defaultConfig?.resolver?.resolveRequest || context.resolveRequest
159
- const res = defaultResolveRequest(context, moduleName, platform)
160
- const svgSrcSuffix = `${path.sep}src${path.sep}index.ts`
161
- if (res && 'filePath' in res && res.filePath.includes(svgSrcSuffix)) {
162
- return {
163
- ...res,
164
- filePath: res.filePath.replace(
165
- svgSrcSuffix,
166
- `${path.sep}lib${path.sep}commonjs${path.sep}index.js`
167
- ),
168
- }
169
- }
170
- return res
171
- }
172
-
173
- const defaultResolveRequest =
174
- defaultConfig?.resolver?.resolveRequest || context.resolveRequest
175
- const res = defaultResolveRequest(context, moduleName, platform)
176
-
177
- // catch .server files that were resolved by path
178
- if (res && 'filePath' in res && /\.server\.[jt]sx?$/.test(res.filePath)) {
179
- return { type: 'sourceFile', filePath: emptyPath }
180
- }
181
-
182
- return res
183
- },
184
- },
185
- }
27
+ let config = applyOneResolverOverrides(defaultConfig)
186
28
 
187
29
  if (typeof userDefaultConfigOverrides === 'function') {
188
30
  config = userDefaultConfigOverrides(config)
@@ -191,73 +33,16 @@ export function getViteMetroPluginOptions({
191
33
  return config
192
34
  },
193
35
  babelConfig: {
194
- plugins: [
195
- // enforce environment guard imports (server-only, client-only, etc.)
196
- 'one/babel-plugin-environment-guard',
197
- // Remove server-only code (loader, generateStaticParams) from route files
198
- // This must run early to prevent server-only imports from being bundled
199
- [
200
- 'one/babel-plugin-remove-server-code',
201
- {
202
- routerRoot: relativeRouterRoot,
203
- },
204
- ],
205
- [
206
- 'babel-plugin-module-resolver',
207
- {
208
- // "vite-tsconfig-paths" for Metro
209
- alias: Object.fromEntries(
210
- Object.entries(tsconfigPathsConfigLoadResult.paths).map(([k, v]) => {
211
- const key = (() => {
212
- if (k.endsWith('/*')) {
213
- return k.replace(/\/\*$/, '')
214
- }
215
-
216
- // If the key does not end with "/*", only alias exact matches.
217
- // Ref: https://www.npmjs.com/package/babel-plugin-module-resolver/v/3.0.0#regular-expression-alias
218
- return `${k}$`
219
- })()
220
-
221
- let value = v[0].replace(/\/\*$/, '')
222
-
223
- if (!value.startsWith('./')) {
224
- value = `./${value}`
225
- }
226
-
227
- return [key, value]
228
- })
229
- ),
230
- },
231
- ],
232
- [
233
- 'one/babel-plugin-one-router-metro',
234
- {
235
- ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY: path.relative(
236
- path.dirname(metroEntryPath),
237
- path.join(projectRoot, relativeRouterRoot)
238
- ),
239
- ONE_ROUTER_ROOT_FOLDER_NAME: relativeRouterRoot,
240
- ONE_ROUTER_REQUIRE_CONTEXT_REGEX_STRING: routerRequireContextRegexString,
241
- ONE_ROUTER_LINKING_CONFIG: linking,
242
- ONE_SETUP_FILE_NATIVE: (() => {
243
- if (!setupFile) return undefined
244
- // Extract native setup file path
245
- const nativeSetupFile =
246
- typeof setupFile === 'string'
247
- ? setupFile
248
- : setupFile.native || setupFile.ios || setupFile.android
249
- if (!nativeSetupFile) return undefined
250
- // Return path relative to metro entry
251
- return path.relative(
252
- path.dirname(metroEntryPath),
253
- path.join(projectRoot, nativeSetupFile)
254
- )
255
- })(),
256
- },
257
- ],
258
- // inline ONE_SERVER_URL so native prod bundles know where to fetch loaders
259
- 'one/babel-plugin-inline-one-server-url',
260
- ],
36
+ plugins: buildOneBabelPlugins({
37
+ projectRoot,
38
+ relativeRouterRoot,
39
+ ignoredRouteFiles,
40
+ linking,
41
+ setupFile,
42
+ // vite path injects import-meta-env-plugin separately via the
43
+ // metro server transformFile hook using the user's vite `define`.
44
+ includeImportMetaEnv: false,
45
+ }),
261
46
  },
262
47
  }
263
48
  }
@@ -0,0 +1,120 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs'
3
+ import { afterAll, describe, expect, it } from 'vitest'
4
+ import { withOne } from './withOne'
5
+
6
+ const projectRoot = path.resolve(__dirname, '../../')
7
+ const tmpDirs: string[] = []
8
+
9
+ function createOneFixtureProject() {
10
+ const workspaceRoot = path.resolve(__dirname, '../../../../')
11
+ const tmpDir = fs.mkdtempSync(path.join(workspaceRoot, '.tmp-with-one-'))
12
+ tmpDirs.push(tmpDir)
13
+
14
+ fs.writeFileSync(
15
+ path.join(tmpDir, 'package.json'),
16
+ JSON.stringify({ name: 'tmp-with-one', private: true })
17
+ )
18
+ fs.writeFileSync(
19
+ path.join(tmpDir, 'tsconfig.json'),
20
+ JSON.stringify({ compilerOptions: { paths: {} } })
21
+ )
22
+ fs.mkdirSync(path.join(tmpDir, 'app'))
23
+ fs.writeFileSync(path.join(tmpDir, 'app', 'index.tsx'), 'export default null\n')
24
+
25
+ fs.writeFileSync(
26
+ path.join(tmpDir, 'vite.config.ts'),
27
+ `
28
+ const defaultConfigOverrides = (config) => ({
29
+ ...config,
30
+ watchFolders: [
31
+ ...(config.watchFolders || []),
32
+ ${JSON.stringify(path.join(tmpDir, 'shared'))},
33
+ ],
34
+ resolver: {
35
+ ...config.resolver,
36
+ extraNodeModules: {
37
+ ...config.resolver?.extraNodeModules,
38
+ 'fixture-singleton': ${JSON.stringify(tmpDir)},
39
+ },
40
+ },
41
+ })
42
+
43
+ globalThis.__oneOptions = {
44
+ setupFile: {
45
+ native: './src/setup-native.ts',
46
+ },
47
+ native: {
48
+ bundler: 'metro',
49
+ bundlerOptions: {
50
+ argv: {
51
+ projectRoot: ${JSON.stringify(tmpDir)},
52
+ },
53
+ defaultConfigOverrides,
54
+ },
55
+ },
56
+ }
57
+ globalThis.__vxrnMetroOptions__ = {
58
+ argv: {
59
+ projectRoot: ${JSON.stringify(tmpDir)},
60
+ },
61
+ defaultConfigOverrides,
62
+ }
63
+
64
+ export default {
65
+ root: ${JSON.stringify(tmpDir)},
66
+ }
67
+ `
68
+ )
69
+
70
+ return tmpDir
71
+ }
72
+
73
+ afterAll(() => {
74
+ for (const tmpDir of tmpDirs) {
75
+ fs.rmSync(tmpDir, { recursive: true, force: true })
76
+ }
77
+ })
78
+
79
+ describe('withOne', () => {
80
+ it('returns a config produced by the production native bundle pipeline', async () => {
81
+ const config = (await withOne(projectRoot, { loadViteConfig: false })) as any
82
+
83
+ // The result is whatever Metro's loadConfig returns. The shape must
84
+ // include the resolver and transformer fields the production pipeline
85
+ // installs.
86
+ expect(config).toBeTruthy()
87
+ expect(config.resolver).toBeTruthy()
88
+ expect(typeof config.resolver.resolveRequest).toBe('function')
89
+ expect(config.transformer).toBeTruthy()
90
+ expect(config.transformer.babelTransformerPath).toMatch(
91
+ /vite-plugin-metro.*babel-transformer/
92
+ )
93
+ })
94
+
95
+ it('orders sourceExts so .js wins over .mjs (the proven One fix)', async () => {
96
+ const config = (await withOne(projectRoot, { loadViteConfig: false })) as any
97
+ const exts: string[] = config.resolver.sourceExts
98
+ expect(exts).toContain('mjs')
99
+ expect(exts).toContain('js')
100
+ // .js must appear before .mjs so platform-aware lookup finds
101
+ // `.native.js` before `.mjs` for one's dist
102
+ expect(exts.indexOf('js')).toBeLessThan(exts.indexOf('mjs'))
103
+ })
104
+
105
+ it('accepts a project root as the first arg', async () => {
106
+ // Mirrors `withOne(__dirname)` usage from a generated metro.config.cjs
107
+ const config = (await withOne(projectRoot, { loadViteConfig: false })) as any
108
+ expect(config).toBeTruthy()
109
+ })
110
+
111
+ it('loads vite.config by default and applies the real native Metro options', async () => {
112
+ const fixtureRoot = createOneFixtureProject()
113
+ const config = (await withOne(fixtureRoot)) as any
114
+
115
+ expect(config.resolver.extraNodeModules['fixture-singleton']).toBe(
116
+ fixtureRoot
117
+ )
118
+ expect(config.watchFolders).toContain(path.join(fixtureRoot, 'shared'))
119
+ })
120
+ })
@@ -0,0 +1,112 @@
1
+ import path from 'node:path'
2
+ import { buildMetroConfigInputFromViteConfig } from '@vxrn/vite-plugin-metro'
3
+ import { getViteMetroPluginOptions } from './getViteMetroPluginOptions'
4
+ import { loadUserOneOptions } from '../vite/loadConfig'
5
+
6
+ export type WithOneOptions = {
7
+ /** Absolute path to the project root. Defaults to `process.cwd()`. */
8
+ projectRoot?: string
9
+ /** Router root folder relative to the project root. Defaults to `'app'`. */
10
+ routerRoot?: string
11
+ /** Patterns to exclude from router file resolution. */
12
+ ignoredRouteFiles?: Array<`**/*${string}`>
13
+ /** Routing linking config — mirrors `one({ router: { linking } })`. */
14
+ linking?: unknown
15
+ /** Native setup file path relative to the project root. */
16
+ setupFile?: string | { native?: string; ios?: string; android?: string }
17
+ /**
18
+ * Load the app's vite.config and use the real One native Metro options.
19
+ * Defaults to true so generated Expo/EAS configs match One's own native path.
20
+ */
21
+ loadViteConfig?: boolean
22
+ }
23
+
24
+ async function loadUserViteMetroOptions(projectRoot: string) {
25
+ const previousCwd = process.cwd()
26
+ const previousIsVxrnCli = process.env.IS_VXRN_CLI
27
+
28
+ try {
29
+ process.chdir(projectRoot)
30
+ process.env.IS_VXRN_CLI = 'true'
31
+ return await loadUserOneOptions('build', true)
32
+ } finally {
33
+ process.chdir(previousCwd)
34
+ if (previousIsVxrnCli === undefined) {
35
+ delete process.env.IS_VXRN_CLI
36
+ } else {
37
+ process.env.IS_VXRN_CLI = previousIsVxrnCli
38
+ }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Produce a Metro config that invokes the EXACT same `getMetroConfigFromViteConfig`
44
+ * pipeline that One's native production builds use. This way `expo export`,
45
+ * `eas update`, and any other Metro-direct workflow produce a bundle that's
46
+ * byte-equivalent to what `react-native bundle` (the iOS build phase) produces.
47
+ *
48
+ * The first argument is ignored — kept only for ergonomic compatibility with
49
+ * the typical `withOne(getDefaultConfig(__dirname))` call shape that Expo
50
+ * users are used to. We discard it because @expo/metro-config's defaults
51
+ * differ from what One needs, and the production pipeline applies its own
52
+ * defaults internally.
53
+ *
54
+ * @example
55
+ * ```js
56
+ * // metro.config.cjs
57
+ * const { withOne } = require('one/metro-config')
58
+ *
59
+ * module.exports = withOne(__dirname)
60
+ * ```
61
+ */
62
+ export async function withOne(
63
+ baseConfigOrProjectRoot: string | object | undefined,
64
+ options: WithOneOptions = {}
65
+ ): Promise<unknown> {
66
+ const projectRoot = path.resolve(
67
+ typeof baseConfigOrProjectRoot === 'string'
68
+ ? baseConfigOrProjectRoot
69
+ : (options.projectRoot ?? process.cwd())
70
+ )
71
+
72
+ const loaded =
73
+ options.loadViteConfig === false
74
+ ? undefined
75
+ : await loadUserViteMetroOptions(projectRoot)
76
+
77
+ // Reuse the proven pipeline rather than re-implementing it: same resolver,
78
+ // same babel transformer path, same sourceExts ordering, same blockList,
79
+ // same @babel/runtime workaround. When possible, load the app's actual
80
+ // vite.config so native.bundlerOptions/defaultConfigOverrides match the
81
+ // One dev/build path.
82
+ const metroPluginOptions =
83
+ loaded?.metroOptions ??
84
+ getViteMetroPluginOptions({
85
+ projectRoot,
86
+ relativeRouterRoot: options.routerRoot ?? 'app',
87
+ ignoredRouteFiles: options.ignoredRouteFiles,
88
+ linking: options.linking,
89
+ setupFile: options.setupFile,
90
+ })
91
+
92
+ // Call `buildMetroConfigInputFromViteConfig`, NOT `getMetroConfigFromViteConfig` —
93
+ // the latter calls Metro's `loadConfig`, which loads the user's metro.config.cjs,
94
+ // which calls `withOne`, infinite recursion. The outer `loadConfig` (driven by
95
+ // Expo CLI / Metro CLI) does the final config merge.
96
+ const viteConfig = {
97
+ ...loaded?.config?.config,
98
+ root: path.resolve(loaded?.config?.config?.root ?? projectRoot),
99
+ } as any
100
+
101
+ const { defaultConfig } = await buildMetroConfigInputFromViteConfig(
102
+ viteConfig,
103
+ {
104
+ ...metroPluginOptions,
105
+ mainModuleName: 'one/metro-entry',
106
+ }
107
+ )
108
+
109
+ return defaultConfig
110
+ }
111
+
112
+ export default withOne
@@ -20,11 +20,21 @@ function getUserOneOptions() {
20
20
  export async function loadUserOneOptions(command: 'serve' | 'build', silent = false) {
21
21
  // Suppress console output if silent
22
22
  const originalConsoleError = console.error
23
+ const previousIsVxrnCli = process.env.IS_VXRN_CLI
24
+ const previousOneOptions = globalThis['__oneOptions']
25
+ const previousVxrnPluginConfig = globalThis['__vxrnPluginConfig__']
26
+ const previousVxrnMetroOptions = globalThis['__vxrnMetroOptions__']
27
+
23
28
  if (silent) {
24
29
  console.error = () => {}
25
30
  }
26
31
 
27
32
  try {
33
+ process.env.IS_VXRN_CLI = 'true'
34
+ delete globalThis['__oneOptions']
35
+ delete globalThis['__vxrnPluginConfig__']
36
+ delete globalThis['__vxrnMetroOptions__']
37
+
28
38
  const config = await loadConfigFromFile({
29
39
  mode: command === 'serve' ? 'dev' : 'prod',
30
40
  command,
@@ -45,8 +55,20 @@ export async function loadUserOneOptions(command: 'serve' | 'build', silent = fa
45
55
  return {
46
56
  config,
47
57
  oneOptions,
58
+ metroOptions: globalThis['__vxrnMetroOptions__'],
48
59
  }
60
+ } catch (error) {
61
+ globalThis['__oneOptions'] = previousOneOptions
62
+ globalThis['__vxrnPluginConfig__'] = previousVxrnPluginConfig
63
+ globalThis['__vxrnMetroOptions__'] = previousVxrnMetroOptions
64
+ throw error
49
65
  } finally {
66
+ if (previousIsVxrnCli === undefined) {
67
+ delete process.env.IS_VXRN_CLI
68
+ } else {
69
+ process.env.IS_VXRN_CLI = previousIsVxrnCli
70
+ }
71
+
50
72
  if (silent) {
51
73
  console.error = originalConsoleError
52
74
  }
@@ -108,24 +108,40 @@ export function autoWarmPlugin(persistPath?: string): Plugin {
108
108
  (d) => !userInclude.has(d) && !excludeSet.has(d)
109
109
  )
110
110
 
111
- // merge with existing cache
111
+ // merge with existing cache. if the file exists but can't be parsed
112
+ // (race with another dev server, partial write), bail rather than
113
+ // clobber it with a thinner set. that's where ping-pong diffs come
114
+ // from when a persistPath is committed.
112
115
  const allDeps = new Set(depsToCache)
113
- try {
114
- if (existsSync(cacheFile)) {
115
- const existing = JSON.parse(readFileSync(cacheFile, 'utf-8'))
116
+ let existingRaw: string | undefined
117
+ if (existsSync(cacheFile)) {
118
+ try {
119
+ existingRaw = readFileSync(cacheFile, 'utf-8')
120
+ const existing = JSON.parse(existingRaw)
116
121
  if (Array.isArray(existing.deps)) {
117
122
  for (const d of existing.deps) {
118
123
  if (!excludeSet.has(d)) allDeps.add(d)
119
124
  }
120
125
  }
126
+ } catch {
127
+ return
121
128
  }
122
- } catch {}
129
+ }
123
130
 
124
131
  const sorted = [...allDeps].sort()
132
+ const nextRaw = JSON.stringify({ deps: sorted }, null, 2)
133
+
134
+ // skip the write when the merged result is byte-identical to what's
135
+ // already on disk. without this, every snapshot where the optimizer's
136
+ // discovered/optimized count grew rewrites the file even though the
137
+ // sorted+deduped output didn't actually change, producing cosmetic
138
+ // git churn on committed cache files.
139
+ if (existingRaw === nextRaw) return
140
+
125
141
  const dir = dirname(cacheFile)
126
142
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
127
143
 
128
- writeFileSync(cacheFile, JSON.stringify({ deps: sorted }, null, 2))
144
+ writeFileSync(cacheFile, nextRaw)
129
145
  console.info(`[one] cached ${sorted.length} deps for next startup`)
130
146
  } catch {
131
147
  // not ready yet, will retry
@@ -0,0 +1,68 @@
1
+ import type { PluginItem, TransformOptions } from '@babel/core';
2
+ type BabelConfigAPI = {
3
+ cache?: ((forever?: boolean) => void) & {
4
+ forever?: () => void;
5
+ never?: () => void;
6
+ using?: (cb: () => unknown) => void;
7
+ invalidate?: (cb: () => unknown) => void;
8
+ };
9
+ cwd?: () => string;
10
+ env?: (...args: unknown[]) => string | undefined;
11
+ caller?: <T>(cb: (caller: unknown) => T) => T;
12
+ };
13
+ export type OneBabelPresetOptions = {
14
+ /** Absolute path to the project root. Defaults to the babel `cwd`. */
15
+ projectRoot?: string;
16
+ /** Router root folder relative to the project root. Defaults to `'app'`. */
17
+ routerRoot?: string;
18
+ /** Route file patterns to exclude (same shape as `one({ router: { ignoredRouteFiles } })`). */
19
+ ignoredRouteFiles?: Array<`**/*${string}`>;
20
+ /** Routing linking config, mirrors `one({ router: { linking } })`. */
21
+ linking?: unknown;
22
+ /** Path to a native setup file, relative to the project root. */
23
+ setupFile?: string | {
24
+ native?: string;
25
+ ios?: string;
26
+ android?: string;
27
+ };
28
+ /** Whether to include `babel-preset-expo` as the base preset. Defaults to true. */
29
+ includeExpoPreset?: boolean;
30
+ /**
31
+ * Whether to include `@vxrn/vite-plugin-metro/babel-plugins/import-meta-env-plugin`.
32
+ * Defaults to true. The Vite-driven Metro server injects this separately via
33
+ * `patchMetroServerWithViteConfigAndMetroPluginOptions` using the user's Vite
34
+ * `define` config — so the Vite path passes `false`. Re-applying is idempotent.
35
+ */
36
+ includeImportMetaEnv?: boolean;
37
+ };
38
+ /**
39
+ * Standalone babel preset that drops the same plugin chain that the
40
+ * Vite-driven Metro path applies into any `babel.config.{cjs,js,mjs}` file.
41
+ *
42
+ * @example
43
+ * ```js
44
+ * // babel.config.cjs
45
+ * module.exports = require('one/babel-preset')
46
+ * ```
47
+ */
48
+ export default function oneBabelPreset(api: BabelConfigAPI, options?: OneBabelPresetOptions): TransformOptions;
49
+ export type BuildOneBabelPluginsOptions = {
50
+ projectRoot: string;
51
+ relativeRouterRoot: string;
52
+ ignoredRouteFiles?: Array<`**/*${string}`>;
53
+ linking?: unknown;
54
+ setupFile?: string | {
55
+ native?: string;
56
+ ios?: string;
57
+ android?: string;
58
+ };
59
+ includeImportMetaEnv?: boolean;
60
+ };
61
+ /**
62
+ * The plugin chain shared between the Vite-driven Metro path
63
+ * (`getViteMetroPluginOptions`) and the standalone preset above.
64
+ */
65
+ export declare function buildOneBabelPlugins({ projectRoot, relativeRouterRoot, ignoredRouteFiles, linking, setupFile, includeImportMetaEnv, }: BuildOneBabelPluginsOptions): PluginItem[];
66
+ export declare function buildRouterRequireContextRegexString(ignoredRouteFiles?: Array<`**/*${string}`>): string;
67
+ export {};
68
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/babel-preset/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAU/D,KAAK,cAAc,GAAG;IACpB,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG;QACtC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;QACpB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAA;QAClB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,OAAO,KAAK,IAAI,CAAA;QACnC,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,OAAO,KAAK,IAAI,CAAA;KACzC,CAAA;IACD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,MAAM,GAAG,SAAS,CAAA;IAChD,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAA;CAC9C,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,KAAK,CAAC,OAAO,MAAM,EAAE,CAAC,CAAA;IAC1C,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACxE,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,GAAG,EAAE,cAAc,EACnB,OAAO,GAAE,qBAA0B,GAClC,gBAAgB,CA2ClB;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,iBAAiB,CAAC,EAAE,KAAK,CAAC,OAAO,MAAM,EAAE,CAAC,CAAA;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACxE,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,WAAW,EACX,kBAAkB,EAClB,iBAAiB,EACjB,OAAO,EACP,SAAS,EACT,oBAA2B,GAC5B,EAAE,2BAA2B,GAAG,UAAU,EAAE,CAgE5C;AAgCD,wBAAgB,oCAAoC,CAClD,iBAAiB,CAAC,EAAE,KAAK,CAAC,OAAO,MAAM,EAAE,CAAC,GACzC,MAAM,CA2BR"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/babel-preset/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.test.d.ts","sourceRoot":"","sources":["../../src/babel-preset/integration.test.ts"],"names":[],"mappings":""}