one 1.12.8 → 1.13.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 (189) hide show
  1. package/dist/cjs/cli/build.cjs +1 -0
  2. package/dist/cjs/cli/build.native.js +1 -0
  3. package/dist/cjs/cli/build.native.js.map +1 -1
  4. package/dist/cjs/createHandleRequest.cjs +18 -1
  5. package/dist/cjs/createHandleRequest.native.js +26 -3
  6. package/dist/cjs/createHandleRequest.native.js.map +1 -1
  7. package/dist/cjs/fork/SSRNavigationContainer.cjs +39 -9
  8. package/dist/cjs/fork/SSRNavigationContainer.native.js +47 -9
  9. package/dist/cjs/fork/SSRNavigationContainer.native.js.map +1 -1
  10. package/dist/cjs/index.cjs +2 -0
  11. package/dist/cjs/index.native.js +2 -0
  12. package/dist/cjs/index.native.js.map +1 -1
  13. package/dist/cjs/layouts/NativeTabs.cjs +49 -0
  14. package/dist/cjs/layouts/NativeTabs.native.js +53 -0
  15. package/dist/cjs/layouts/NativeTabs.native.js.map +1 -0
  16. package/dist/cjs/layouts/Tabs.cjs +4 -1
  17. package/dist/cjs/layouts/Tabs.native.js +4 -1
  18. package/dist/cjs/layouts/Tabs.native.js.map +1 -1
  19. package/dist/cjs/native-tabs.cjs +27 -0
  20. package/dist/cjs/native-tabs.native.js +30 -0
  21. package/dist/cjs/native-tabs.native.js.map +1 -0
  22. package/dist/cjs/render.cjs +3 -1
  23. package/dist/cjs/router/router.cjs +11 -3
  24. package/dist/cjs/router/router.native.js +15 -3
  25. package/dist/cjs/router/router.native.js.map +1 -1
  26. package/dist/cjs/router/useViteRoutes.cjs +5 -1
  27. package/dist/cjs/router/useViteRoutes.native.js +21 -1
  28. package/dist/cjs/router/useViteRoutes.native.js.map +1 -1
  29. package/dist/cjs/serve.cjs +9 -3
  30. package/dist/cjs/serve.native.js +13 -4
  31. package/dist/cjs/serve.native.js.map +1 -1
  32. package/dist/cjs/server/oneServe.cjs +1 -0
  33. package/dist/cjs/server/oneServe.native.js +1 -0
  34. package/dist/cjs/server/oneServe.native.js.map +1 -1
  35. package/dist/cjs/server/workerHandler.cjs +1 -0
  36. package/dist/cjs/server/workerHandler.native.js +1 -0
  37. package/dist/cjs/server/workerHandler.native.js.map +1 -1
  38. package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.cjs +1 -1
  39. package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.native.js +1 -1
  40. package/dist/cjs/utils/getPathnameFromFilePath.cjs +25 -20
  41. package/dist/cjs/utils/getPathnameFromFilePath.native.js +28 -23
  42. package/dist/cjs/utils/getPathnameFromFilePath.native.js.map +1 -1
  43. package/dist/cjs/utils/getPathnameFromFilePath.test.cjs +13 -2
  44. package/dist/cjs/utils/getPathnameFromFilePath.test.native.js +13 -2
  45. package/dist/cjs/utils/getPathnameFromFilePath.test.native.js.map +1 -1
  46. package/dist/cjs/vercel/build/generate/createSsrServerlessFunction.cjs +8 -0
  47. package/dist/cjs/vercel/build/generate/createSsrServerlessFunction.native.js +8 -0
  48. package/dist/cjs/vercel/build/generate/createSsrServerlessFunction.native.js.map +1 -1
  49. package/dist/cjs/views/Navigator.cjs +17 -3
  50. package/dist/cjs/views/Navigator.native.js +45 -2
  51. package/dist/cjs/views/Navigator.native.js.map +1 -1
  52. package/dist/cjs/vite/DevHead.cjs +1 -1
  53. package/dist/cjs/vite/DevHead.native.js.map +1 -1
  54. package/dist/cjs/vite/one.cjs +3 -2
  55. package/dist/cjs/vite/one.native.js +8 -7
  56. package/dist/cjs/vite/one.native.js.map +1 -1
  57. package/dist/cjs/vite/resolveResponse.cjs +5 -4
  58. package/dist/cjs/vite/resolveResponse.native.js +5 -4
  59. package/dist/cjs/vite/resolveResponse.native.js.map +1 -1
  60. package/dist/esm/cli/build.mjs +1 -0
  61. package/dist/esm/cli/build.mjs.map +1 -1
  62. package/dist/esm/cli/build.native.js +1 -0
  63. package/dist/esm/cli/build.native.js.map +1 -1
  64. package/dist/esm/createHandleRequest.mjs +18 -2
  65. package/dist/esm/createHandleRequest.mjs.map +1 -1
  66. package/dist/esm/createHandleRequest.native.js +24 -2
  67. package/dist/esm/createHandleRequest.native.js.map +1 -1
  68. package/dist/esm/fork/SSRNavigationContainer.mjs +40 -10
  69. package/dist/esm/fork/SSRNavigationContainer.mjs.map +1 -1
  70. package/dist/esm/fork/SSRNavigationContainer.native.js +48 -10
  71. package/dist/esm/fork/SSRNavigationContainer.native.js.map +1 -1
  72. package/dist/esm/index.js +2 -1
  73. package/dist/esm/index.js.map +1 -1
  74. package/dist/esm/index.mjs +2 -1
  75. package/dist/esm/index.mjs.map +1 -1
  76. package/dist/esm/index.native.js +2 -1
  77. package/dist/esm/index.native.js.map +1 -1
  78. package/dist/esm/layouts/NativeTabs.mjs +25 -0
  79. package/dist/esm/layouts/NativeTabs.mjs.map +1 -0
  80. package/dist/esm/layouts/NativeTabs.native.js +26 -0
  81. package/dist/esm/layouts/NativeTabs.native.js.map +1 -0
  82. package/dist/esm/layouts/Tabs.mjs +4 -1
  83. package/dist/esm/layouts/Tabs.mjs.map +1 -1
  84. package/dist/esm/layouts/Tabs.native.js +4 -1
  85. package/dist/esm/layouts/Tabs.native.js.map +1 -1
  86. package/dist/esm/native-tabs.mjs +3 -0
  87. package/dist/esm/native-tabs.mjs.map +1 -0
  88. package/dist/esm/native-tabs.native.js +3 -0
  89. package/dist/esm/native-tabs.native.js.map +1 -0
  90. package/dist/esm/render.mjs +3 -1
  91. package/dist/esm/render.mjs.map +1 -1
  92. package/dist/esm/router/router.mjs +11 -4
  93. package/dist/esm/router/router.mjs.map +1 -1
  94. package/dist/esm/router/router.native.js +15 -4
  95. package/dist/esm/router/router.native.js.map +1 -1
  96. package/dist/esm/router/useViteRoutes.mjs +5 -1
  97. package/dist/esm/router/useViteRoutes.mjs.map +1 -1
  98. package/dist/esm/router/useViteRoutes.native.js +21 -1
  99. package/dist/esm/router/useViteRoutes.native.js.map +1 -1
  100. package/dist/esm/serve.mjs +9 -3
  101. package/dist/esm/serve.mjs.map +1 -1
  102. package/dist/esm/serve.native.js +13 -4
  103. package/dist/esm/serve.native.js.map +1 -1
  104. package/dist/esm/server/oneServe.mjs +2 -1
  105. package/dist/esm/server/oneServe.mjs.map +1 -1
  106. package/dist/esm/server/oneServe.native.js +2 -1
  107. package/dist/esm/server/oneServe.native.js.map +1 -1
  108. package/dist/esm/server/workerHandler.mjs +2 -1
  109. package/dist/esm/server/workerHandler.mjs.map +1 -1
  110. package/dist/esm/server/workerHandler.native.js +2 -1
  111. package/dist/esm/server/workerHandler.native.js.map +1 -1
  112. package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.mjs +1 -1
  113. package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.native.js +1 -1
  114. package/dist/esm/utils/getPathnameFromFilePath.mjs +25 -20
  115. package/dist/esm/utils/getPathnameFromFilePath.mjs.map +1 -1
  116. package/dist/esm/utils/getPathnameFromFilePath.native.js +28 -23
  117. package/dist/esm/utils/getPathnameFromFilePath.native.js.map +1 -1
  118. package/dist/esm/utils/getPathnameFromFilePath.test.mjs +13 -2
  119. package/dist/esm/utils/getPathnameFromFilePath.test.mjs.map +1 -1
  120. package/dist/esm/utils/getPathnameFromFilePath.test.native.js +13 -2
  121. package/dist/esm/utils/getPathnameFromFilePath.test.native.js.map +1 -1
  122. package/dist/esm/vercel/build/generate/createSsrServerlessFunction.mjs +8 -0
  123. package/dist/esm/vercel/build/generate/createSsrServerlessFunction.mjs.map +1 -1
  124. package/dist/esm/vercel/build/generate/createSsrServerlessFunction.native.js +8 -0
  125. package/dist/esm/vercel/build/generate/createSsrServerlessFunction.native.js.map +1 -1
  126. package/dist/esm/views/Navigator.mjs +18 -4
  127. package/dist/esm/views/Navigator.mjs.map +1 -1
  128. package/dist/esm/views/Navigator.native.js +46 -3
  129. package/dist/esm/views/Navigator.native.js.map +1 -1
  130. package/dist/esm/vite/DevHead.mjs +1 -1
  131. package/dist/esm/vite/DevHead.mjs.map +1 -1
  132. package/dist/esm/vite/DevHead.native.js.map +1 -1
  133. package/dist/esm/vite/one.mjs +3 -2
  134. package/dist/esm/vite/one.mjs.map +1 -1
  135. package/dist/esm/vite/one.native.js +8 -7
  136. package/dist/esm/vite/one.native.js.map +1 -1
  137. package/dist/esm/vite/resolveResponse.mjs +5 -4
  138. package/dist/esm/vite/resolveResponse.mjs.map +1 -1
  139. package/dist/esm/vite/resolveResponse.native.js +5 -4
  140. package/dist/esm/vite/resolveResponse.native.js.map +1 -1
  141. package/package.json +12 -21
  142. package/src/cli/build.ts +3 -0
  143. package/src/createHandleRequest.ts +36 -1
  144. package/src/fork/SSRNavigationContainer.tsx +41 -10
  145. package/src/index.ts +1 -0
  146. package/src/layouts/NativeTabs.tsx +46 -0
  147. package/src/layouts/Tabs.tsx +48 -44
  148. package/src/native-tabs.ts +1 -0
  149. package/src/render.tsx +6 -1
  150. package/src/router/router.ts +21 -0
  151. package/src/router/useViteRoutes.tsx +7 -1
  152. package/src/serve.ts +14 -2
  153. package/src/server/oneServe.ts +2 -0
  154. package/src/server/workerHandler.ts +2 -0
  155. package/src/typed-routes/getTypedRoutesDeclarationFile.ts +1 -1
  156. package/src/types.ts +1 -0
  157. package/src/utils/getPathnameFromFilePath.test.ts +24 -4
  158. package/src/utils/getPathnameFromFilePath.ts +13 -7
  159. package/src/vercel/build/generate/createSsrServerlessFunction.ts +8 -0
  160. package/src/views/Navigator.tsx +40 -6
  161. package/src/vite/DevHead.tsx +5 -0
  162. package/src/vite/one.ts +7 -1
  163. package/src/vite/resolveResponse.ts +8 -5
  164. package/types/cli/build.d.ts.map +1 -1
  165. package/types/createHandleRequest.d.ts +1 -0
  166. package/types/createHandleRequest.d.ts.map +1 -1
  167. package/types/fork/SSRNavigationContainer.d.ts +2 -2
  168. package/types/fork/SSRNavigationContainer.d.ts.map +1 -1
  169. package/types/index.d.ts +1 -0
  170. package/types/index.d.ts.map +1 -1
  171. package/types/layouts/NativeTabs.d.ts +8 -0
  172. package/types/layouts/NativeTabs.d.ts.map +1 -0
  173. package/types/layouts/Tabs.d.ts +2 -0
  174. package/types/layouts/Tabs.d.ts.map +1 -1
  175. package/types/native-tabs.d.ts +2 -0
  176. package/types/native-tabs.d.ts.map +1 -0
  177. package/types/render.d.ts.map +1 -1
  178. package/types/router/router.d.ts +1 -0
  179. package/types/router/router.d.ts.map +1 -1
  180. package/types/router/useViteRoutes.d.ts.map +1 -1
  181. package/types/server/oneServe.d.ts.map +1 -1
  182. package/types/server/workerHandler.d.ts.map +1 -1
  183. package/types/types.d.ts +1 -0
  184. package/types/types.d.ts.map +1 -1
  185. package/types/utils/getPathnameFromFilePath.d.ts.map +1 -1
  186. package/types/vercel/build/generate/createSsrServerlessFunction.d.ts.map +1 -1
  187. package/types/views/Navigator.d.ts.map +1 -1
  188. package/types/vite/DevHead.d.ts.map +1 -1
  189. package/types/vite/one.d.ts.map +1 -1
@@ -28,6 +28,23 @@ type RequestHandlerResponse = null | string | Response
28
28
 
29
29
  const debugRouter = process.env.ONE_DEBUG_ROUTER
30
30
 
31
+ // ensure handler results are always a proper Response so middleware
32
+ // can safely use response.body / response.headers / new Response(response.body, ...)
33
+ function ensureResponse(value: any): Response {
34
+ // use isResponse (duck-type check) instead of instanceof — the Response
35
+ // constructor may differ across module realms (e.g. API handler vs middleware)
36
+ if (isResponse(value)) return value
37
+ if (typeof value === 'string') {
38
+ return new Response(value, {
39
+ headers: { 'Content-Type': 'text/html' },
40
+ })
41
+ }
42
+ if (value && typeof value === 'object') {
43
+ return Response.json(value)
44
+ }
45
+ return new Response(value)
46
+ }
47
+
31
48
  export async function runMiddlewares(
32
49
  handlers: RequestHandlers,
33
50
  request: Request,
@@ -57,7 +74,7 @@ export async function runMiddlewares(
57
74
  if (debugRouter) {
58
75
  console.info(`[one] ✓ middleware chain complete`)
59
76
  }
60
- return await getResponse()
77
+ return ensureResponse(await getResponse())
61
78
  }
62
79
 
63
80
  if (debugRouter) {
@@ -123,6 +140,7 @@ export async function resolveAPIRoute(
123
140
  loaderProps: {
124
141
  path: pathname,
125
142
  search: url.search,
143
+ subdomain: getSubdomain(url),
126
144
  params,
127
145
  },
128
146
  }),
@@ -176,6 +194,7 @@ export async function resolveLoaderRoute(
176
194
  loaderProps: {
177
195
  path: url.pathname,
178
196
  search: url.search,
197
+ subdomain: getSubdomain(url),
179
198
  request: route.type === 'ssr' ? request : undefined,
180
199
  params: getLoaderParams(url, route),
181
200
  },
@@ -272,6 +291,7 @@ export async function resolvePageRoute(
272
291
  const loaderProps = {
273
292
  path: pathname,
274
293
  search: search,
294
+ subdomain: getSubdomain(url),
275
295
  request: route.type === 'ssr' ? request : undefined,
276
296
  params: getLoaderParams(url, route),
277
297
  }
@@ -305,6 +325,21 @@ export function getURLfromRequestURL(request: Request) {
305
325
  return url
306
326
  }
307
327
 
328
+ export function getSubdomain(url: URL): string | undefined {
329
+ const host = url.hostname
330
+ // skip for IP addresses and localhost
331
+ if (!host || host === 'localhost' || /^\d+\.\d+\.\d+\.\d+$/.test(host)) {
332
+ return undefined
333
+ }
334
+ const parts = host.split('.')
335
+ // need at least 3 parts for a subdomain (sub.example.com)
336
+ if (parts.length < 3) {
337
+ return undefined
338
+ }
339
+ // return everything before the last two parts (domain.tld)
340
+ return parts.slice(0, -2).join('.')
341
+ }
342
+
308
343
  function compileRouteRegex(route: RouteInfo): RouteInfoCompiled {
309
344
  return {
310
345
  ...route,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * SSR-optimized replacement for BaseNavigationContainer.
3
- * Provides only the 4 contexts that child navigators need during SSR render,
4
- * with static/no-op values. Eliminates 32+ hooks and reduces 8 providers to 4.
3
+ * Provides only the 5 contexts that child navigators need during SSR render,
4
+ * with static/no-op values. Eliminates 32+ hooks and reduces 8 providers to 5.
5
5
  *
6
6
  * Requires @react-navigation/core package.json exports to include internal context paths.
7
7
  * See postinstall patch in the repo.
@@ -13,7 +13,7 @@ import { NavigationBuilderContext } from '@react-navigation/core/lib/module/Navi
13
13
  import { NavigationStateContext } from '@react-navigation/core/lib/module/NavigationStateContext'
14
14
  // @ts-ignore internal module
15
15
  import { SingleNavigatorContext } from '@react-navigation/core/lib/module/EnsureSingleNavigator'
16
- import { ThemeProvider } from '@react-navigation/core'
16
+ import { NavigationContainerRefContext, ThemeProvider } from '@react-navigation/core'
17
17
  import { LinkingContext } from '@react-navigation/native'
18
18
  import * as React from 'react'
19
19
 
@@ -37,6 +37,35 @@ const SSR_SINGLE_NAV_CTX = {
37
37
  unregister: noop,
38
38
  }
39
39
 
40
+ // no-op navigation ref so useNavigation() doesn't throw during SSR
41
+ const SSR_NAV_REF = {
42
+ dispatch: noop,
43
+ navigate: noop,
44
+ reset: noop,
45
+ goBack: noop,
46
+ isFocused: () => false,
47
+ canGoBack: () => false,
48
+ getParent: () => undefined,
49
+ getState: () => undefined,
50
+ getRootState: () => undefined,
51
+ getCurrentRoute: () => undefined,
52
+ getCurrentOptions: () => undefined,
53
+ isReady: () => false,
54
+ addListener: () => noop,
55
+ removeListener: noop,
56
+ resetRoot: noop,
57
+ setOptions: noop,
58
+ // CommonActions methods
59
+ setParams: noop,
60
+ popTo: noop,
61
+ pop: noop,
62
+ popToTop: noop,
63
+ push: noop,
64
+ replace: noop,
65
+ jumpTo: noop,
66
+ preload: noop,
67
+ } as any
68
+
40
69
  const getPartialState = (state: any): any => {
41
70
  if (!state) return undefined
42
71
  const { key, routeNames, ...partial } = state
@@ -105,13 +134,15 @@ export function SSRNavigationContainer({
105
134
  const linkingCtx = linking ? { options: linking } : SSR_LINKING_CTX
106
135
  return (
107
136
  <LinkingContext.Provider value={linkingCtx}>
108
- <NavigationBuilderContext.Provider value={SSR_BUILDER_CTX}>
109
- <NavigationStateContext.Provider value={getStateContext(initialState)}>
110
- <SingleNavigatorContext.Provider value={SSR_SINGLE_NAV_CTX}>
111
- <ThemeProvider value={theme}>{children}</ThemeProvider>
112
- </SingleNavigatorContext.Provider>
113
- </NavigationStateContext.Provider>
114
- </NavigationBuilderContext.Provider>
137
+ <NavigationContainerRefContext.Provider value={SSR_NAV_REF}>
138
+ <NavigationBuilderContext.Provider value={SSR_BUILDER_CTX}>
139
+ <NavigationStateContext.Provider value={getStateContext(initialState)}>
140
+ <SingleNavigatorContext.Provider value={SSR_SINGLE_NAV_CTX}>
141
+ <ThemeProvider value={theme}>{children}</ThemeProvider>
142
+ </SingleNavigatorContext.Provider>
143
+ </NavigationStateContext.Provider>
144
+ </NavigationBuilderContext.Provider>
145
+ </NavigationContainerRefContext.Provider>
115
146
  </LinkingContext.Provider>
116
147
  )
117
148
  }
package/src/index.ts CHANGED
@@ -74,6 +74,7 @@ export { href } from './href'
74
74
  // components
75
75
  export { Stack } from './layouts/Stack'
76
76
  export { Tabs } from './layouts/Tabs'
77
+ export { NativeTabs } from './layouts/NativeTabs'
77
78
  export { Protected, type ProtectedProps } from './views/Protected'
78
79
  // Stack header compositional API types
79
80
  export type {
@@ -0,0 +1,46 @@
1
+ import type { ParamListBase, TabNavigationState } from '@react-navigation/native'
2
+ import type React from 'react'
3
+
4
+ import { Protected } from '../views/Protected'
5
+ import { withLayoutContext } from './withLayoutContext'
6
+
7
+ let NativeBottomTabNavigator: React.ComponentType<any> | null = null
8
+
9
+ try {
10
+ const mod = require('@bottom-tabs/react-navigation')
11
+ NativeBottomTabNavigator = mod.createNativeBottomTabNavigator().Navigator
12
+ } catch {
13
+ // @bottom-tabs/react-navigation not installed
14
+ }
15
+
16
+ type NativeTabsType = ReturnType<typeof withLayoutContext> & {
17
+ Protected: typeof Protected
18
+ }
19
+
20
+ function createNativeTabs(): NativeTabsType {
21
+ if (!NativeBottomTabNavigator) {
22
+ throw new Error(
23
+ 'NativeTabs requires @bottom-tabs/react-navigation and react-native-bottom-tabs.\n' +
24
+ 'Install: npx expo install @bottom-tabs/react-navigation react-native-bottom-tabs'
25
+ )
26
+ }
27
+
28
+ return Object.assign(withLayoutContext(NativeBottomTabNavigator), {
29
+ Protected,
30
+ }) as NativeTabsType
31
+ }
32
+
33
+ let _nativeTabs: NativeTabsType | null = null
34
+
35
+ export const NativeTabs = new Proxy({} as NativeTabsType, {
36
+ get(_, prop) {
37
+ if (!_nativeTabs) _nativeTabs = createNativeTabs()
38
+ return (_nativeTabs as any)[prop]
39
+ },
40
+ apply(_, thisArg, args) {
41
+ if (!_nativeTabs) _nativeTabs = createNativeTabs()
42
+ return (_nativeTabs as any).apply(thisArg, args)
43
+ },
44
+ })
45
+
46
+ export default NativeTabs
@@ -10,6 +10,7 @@ import { Platform, Pressable } from 'react-native'
10
10
 
11
11
  import type { OneRouter } from '../interfaces/router'
12
12
  import { Link } from '../link/Link'
13
+ import { Protected } from '../views/Protected'
13
14
  import { withLayoutContext } from './withLayoutContext'
14
15
 
15
16
  const TabBar = ({ state, ...restProps }: BottomTabBarProps) => {
@@ -41,53 +42,56 @@ type BottomTabNavigationOptionsWithHref = BottomTabNavigationOptions & {
41
42
  href?: OneRouter.Href | null
42
43
  }
43
44
 
44
- export const Tabs = withLayoutContext<
45
- BottomTabNavigationOptionsWithHref,
46
- typeof BottomTabNavigator,
47
- TabNavigationState<ParamListBase>,
48
- BottomTabNavigationEventMap
49
- >(
50
- BottomTabNavigator,
51
- (screens) => {
52
- // Support the `href` shortcut prop.
53
- return screens.map((screen) => {
54
- if (typeof screen.options !== 'function' && screen.options?.href !== undefined) {
55
- const { href, ...options } = screen.options
56
- if (options.tabBarButton) {
57
- throw new Error('Cannot use `href` and `tabBarButton` together.')
58
- }
59
- return {
60
- ...screen,
61
- options: {
62
- ...options,
63
- tabBarButton: (props) => {
64
- if (href == null) {
65
- return null
66
- }
67
- const children =
68
- Platform.OS === 'web' ? (
69
- props.children
70
- ) : (
71
- <Pressable>{props.children}</Pressable>
45
+ export const Tabs = Object.assign(
46
+ withLayoutContext<
47
+ BottomTabNavigationOptionsWithHref,
48
+ typeof BottomTabNavigator,
49
+ TabNavigationState<ParamListBase>,
50
+ BottomTabNavigationEventMap
51
+ >(
52
+ BottomTabNavigator,
53
+ (screens) => {
54
+ // Support the `href` shortcut prop.
55
+ return screens.map((screen) => {
56
+ if (typeof screen.options !== 'function' && screen.options?.href !== undefined) {
57
+ const { href, ...options } = screen.options
58
+ if (options.tabBarButton) {
59
+ throw new Error('Cannot use `href` and `tabBarButton` together.')
60
+ }
61
+ return {
62
+ ...screen,
63
+ options: {
64
+ ...options,
65
+ tabBarButton: (props) => {
66
+ if (href == null) {
67
+ return null
68
+ }
69
+ const children =
70
+ Platform.OS === 'web' ? (
71
+ props.children
72
+ ) : (
73
+ <Pressable>{props.children}</Pressable>
74
+ )
75
+ return (
76
+ <Link
77
+ {...(props as any)}
78
+ style={[{ display: 'flex' }, props.style]}
79
+ href={href}
80
+ asChild={Platform.OS !== 'web'}
81
+ // biome-ignore lint/correctness/noChildrenProp: children prop needed for asChild pattern
82
+ children={children}
83
+ />
72
84
  )
73
- return (
74
- <Link
75
- {...(props as any)}
76
- style={[{ display: 'flex' }, props.style]}
77
- href={href}
78
- asChild={Platform.OS !== 'web'}
79
- // biome-ignore lint/correctness/noChildrenProp: children prop needed for asChild pattern
80
- children={children}
81
- />
82
- )
85
+ },
83
86
  },
84
- },
87
+ }
85
88
  }
86
- }
87
- return screen
88
- })
89
- },
90
- { props: { tabBar: TabBar } }
89
+ return screen
90
+ })
91
+ },
92
+ { props: { tabBar: TabBar } }
93
+ ),
94
+ { Protected }
91
95
  )
92
96
 
93
97
  export default Tabs
@@ -0,0 +1 @@
1
+ export { NativeTabs, NativeTabs as default } from './layouts/NativeTabs'
package/src/render.tsx CHANGED
@@ -11,7 +11,12 @@ export function render(element: React.ReactNode) {
11
11
 
12
12
  if (globalThis['__vxrnRoot']) {
13
13
  globalThis['__vxrnVersion']++
14
- globalThis['__vxrnRoot'].render(element)
14
+ // wrap in startTransition so Suspense-based route resolution keeps the
15
+ // old tree visible while new route modules resolve, instead of flashing
16
+ // the fallback (or losing the tree entirely in error boundaries)
17
+ startTransition(() => {
18
+ globalThis['__vxrnRoot'].render(element)
19
+ })
15
20
  } else {
16
21
  startTransition(() => {
17
22
  const rootElement = process.env.ONE_USE_FASTER_DOCUMENT
@@ -131,6 +131,10 @@ export function isRouteProtected(href: string): boolean {
131
131
  export let hasAttemptedToHideSplash = false
132
132
  export let initialState: OneRouter.ResultState | undefined
133
133
  export let rootState: OneRouter.ResultState | undefined
134
+ // the original pathname from the initial page load, used by late-mounting
135
+ // navigators to determine the correct initial route even after React Navigation's
136
+ // linking has pushed a different URL during unmount/remount cycles
137
+ export let initialPathname: string | undefined
134
138
 
135
139
  let nextState: OneRouter.ResultState | undefined
136
140
  export let routeInfo: UrlObject | undefined
@@ -282,6 +286,7 @@ export function initialize(
282
286
 
283
287
  function cleanUpState() {
284
288
  initialState = undefined
289
+ initialPathname = undefined
285
290
  rootState = undefined
286
291
  nextState = undefined
287
292
  routeInfo = undefined
@@ -293,6 +298,11 @@ function cleanUpState() {
293
298
  function setupLinkingAndRouteInfo(initialLocation?: URL) {
294
299
  initialState = setupLinking(routeNode, initialLocation)
295
300
 
301
+ // capture the original pathname before React Navigation's linking can modify it
302
+ initialPathname =
303
+ initialLocation?.pathname ??
304
+ (typeof window !== 'undefined' ? window.location.pathname : undefined)
305
+
296
306
  if (initialState) {
297
307
  rootState = initialState
298
308
  routeInfo = getRouteInfo(initialState)
@@ -567,6 +577,17 @@ function syncStoreRootState() {
567
577
  if (navigationRef.isReady()) {
568
578
  const currentState = navigationRef.getRootState() as unknown as OneRouter.ResultState
569
579
  if (rootState !== currentState) {
580
+ // when a parent layout conditionally renders (e.g. auth gate), getRootState()
581
+ // can return incomplete/wrong state before all navigators mount. don't
582
+ // overwrite routeInfo with wrong pathname while initial state is still valid.
583
+ if (initialState && routeInfo?.pathname) {
584
+ const nextRouteInfo = getRouteInfo(currentState)
585
+ if (nextRouteInfo.pathname !== routeInfo.pathname) {
586
+ // pathname would change — skip to preserve initial URL truth
587
+ rootState = currentState
588
+ return
589
+ }
590
+ }
570
591
  updateState(currentState)
571
592
  }
572
593
  }
@@ -15,9 +15,15 @@ export function useViteRoutes(
15
15
  version?: number
16
16
  ) {
17
17
  if (version && version > lastVersion) {
18
- // reload
18
+ // reload — clear stale route caches so fresh modules are used
19
19
  context = null
20
20
  lastVersion = version
21
+ // clear preloaded modules from previous render cycle — they point to
22
+ // old route module instances and would short-circuit resolve() before
23
+ // it reaches the new routesSync functions
24
+ for (const key of Object.keys(preloadedModules)) {
25
+ delete preloadedModules[key]
26
+ }
21
27
  }
22
28
 
23
29
  if (!context) {
package/src/serve.ts CHANGED
@@ -181,7 +181,12 @@ async function startWorker(args: Parameters<typeof serve>[0]) {
181
181
 
182
182
  const { labelProcess } = await import('./cli/label-process')
183
183
  const { removeUndefined } = await import('./utils/removeUndefined')
184
- const { loadEnv, serve: vxrnServe, serveStaticAssets } = await import('vxrn/serve')
184
+ const {
185
+ loadEnv,
186
+ serve: vxrnServe,
187
+ serveStaticAssets,
188
+ compileCacheRules,
189
+ } = await import('vxrn/serve')
185
190
  const { oneServe } = await import('./server/oneServe')
186
191
 
187
192
  labelProcess('serve')
@@ -190,6 +195,11 @@ async function startWorker(args: Parameters<typeof serve>[0]) {
190
195
  await loadEnv('production')
191
196
  }
192
197
 
198
+ // compile cache rules once at startup so every request is a single regex test
199
+ const cacheRules = oneOptions.server?.cacheControl
200
+ ? compileCacheRules(oneOptions.server.cacheControl)
201
+ : undefined
202
+
193
203
  return await vxrnServe({
194
204
  outDir: buildInfo.outDir || outDir,
195
205
  app: args?.app,
@@ -201,7 +211,9 @@ async function startWorker(args: Parameters<typeof serve>[0]) {
201
211
  }),
202
212
 
203
213
  async beforeRegisterRoutes(options, app) {
204
- await oneServe(oneOptions, buildInfo, app, { serveStaticAssets })
214
+ await oneServe(oneOptions, buildInfo, app, {
215
+ serveStaticAssets: (ctx) => serveStaticAssets({ ...ctx, cacheRules }),
216
+ })
205
217
  },
206
218
 
207
219
  async afterRegisterRoutes(options, app) {},
@@ -9,6 +9,7 @@ import {
9
9
  } from '../constants'
10
10
  import {
11
11
  compileManifest,
12
+ getSubdomain,
12
13
  getURLfromRequestURL,
13
14
  type RequestHandlers,
14
15
  runMiddlewares,
@@ -748,6 +749,7 @@ url: ${url}`)
748
749
  const loaderProps = {
749
750
  path: pathname,
750
751
  search,
752
+ subdomain: getSubdomain(getURLfromRequestURL(request)),
751
753
  request,
752
754
  params,
753
755
  }
@@ -5,6 +5,7 @@ import {
5
5
  } from '../constants'
6
6
  import {
7
7
  compileManifest,
8
+ getSubdomain,
8
9
  getURLfromRequestURL,
9
10
  type RequestHandlers,
10
11
  resolveAPIRoute,
@@ -704,6 +705,7 @@ export function createWorkerHandler(options: WorkerHandlerOptions) {
704
705
  const loaderProps = {
705
706
  path: pathname,
706
707
  search: url.search,
708
+ subdomain: getSubdomain(url),
707
709
  request,
708
710
  params,
709
711
  }
@@ -52,7 +52,7 @@ ${
52
52
  */
53
53
  type RouteInfo<Params = Record<string, never>> = {
54
54
  Params: Params
55
- LoaderProps: { path: string; params: Params; request?: Request }
55
+ LoaderProps: { path: string; search?: string; subdomain?: string; params: Params; request?: Request }
56
56
  }`
57
57
  : ''
58
58
  }
package/src/types.ts CHANGED
@@ -12,6 +12,7 @@ export type RenderApp = (props: RenderAppProps) => Promise<string>
12
12
  export type LoaderProps<Params extends object = Record<string, string | string[]>> = {
13
13
  path: string
14
14
  search?: string
15
+ subdomain?: string
15
16
  params: Params
16
17
  request?: Request
17
18
  }
@@ -92,15 +92,35 @@ describe('getPathnameFromFilePath', () => {
92
92
  )
93
93
  })
94
94
 
95
- it('substitutes filename param, dirname params become placeholders', () => {
96
- // getPathnameFromFilePath only substitutes params in the filename segment,
97
- // dirname params are converted to :param placeholders via regex
95
+ it('substitutes params in both dirname and filename', () => {
98
96
  expect(
99
97
  getPathnameFromFilePath('/servers/[serverId]/[channelId]+spa.tsx', {
100
98
  serverId: 'abc',
101
99
  channelId: '123',
102
100
  })
103
- ).toBe('/servers/:serverId/123')
101
+ ).toBe('/servers/abc/123')
102
+ })
103
+
104
+ it('substitutes dirname params for SSG dynamic routes', () => {
105
+ expect(getPathnameFromFilePath('/[lang]/index+ssg.tsx', { lang: 'en' }, true)).toBe(
106
+ '/en/'
107
+ )
108
+ expect(getPathnameFromFilePath('/[lang]/index+ssg.tsx', { lang: 'ko' }, true)).toBe(
109
+ '/ko/'
110
+ )
111
+ })
112
+
113
+ it('substitutes nested dirname params', () => {
114
+ expect(
115
+ getPathnameFromFilePath(
116
+ '/[lang]/[region]/index+ssg.tsx',
117
+ {
118
+ lang: 'en',
119
+ region: 'us',
120
+ },
121
+ true
122
+ )
123
+ ).toBe('/en/us/')
104
124
  })
105
125
  })
106
126
 
@@ -7,13 +7,6 @@ export function getPathnameFromFilePath(
7
7
  options: { preserveExtensions?: boolean; includeIndex?: boolean } = {}
8
8
  ) {
9
9
  const path = inputPath.replace(/\+(spa|ssg|ssr|api)\.tsx?$/, '')
10
- // remove groups, folder render mode suffixes, and convert [param] to :param in dirname
11
- const dirname = Path.dirname(path)
12
- .split('/')
13
- .map((segment) => segment.replace(/\+(api|ssg|ssr|spa)$/, ''))
14
- .join('/')
15
- .replace(/\([^/]+\)/gi, '')
16
- .replace(/\[([^\]]+)\]/g, ':$1')
17
10
  const file = Path.basename(path)
18
11
  const fileName = options.preserveExtensions ? file : file.replace(/\.[a-z]+$/, '')
19
12
 
@@ -30,6 +23,19 @@ ${JSON.stringify(params, null, 2)}`
30
23
  )
31
24
  }
32
25
 
26
+ // remove groups, folder render mode suffixes, and substitute [param] in dirname
27
+ const dirname = Path.dirname(path)
28
+ .split('/')
29
+ .map((segment) => segment.replace(/\+(api|ssg|ssr|spa)$/, ''))
30
+ .join('/')
31
+ .replace(/\([^/]+\)/gi, '')
32
+ .replace(/\[([^\]]+)\]/g, (_, paramName) => {
33
+ const value = params[paramName]
34
+ if (value != null) return String(value)
35
+ if (strict) throw paramsError(paramName)
36
+ return ':' + paramName
37
+ })
38
+
33
39
  const nameWithParams = (() => {
34
40
  if (fileName === 'index' && !options.includeIndex) {
35
41
  return '/'
@@ -101,9 +101,17 @@ export async function createSsrServerlessFunction(
101
101
  originalPath = originalPath.replace(\`:$\{key}\`, String(value));
102
102
  }
103
103
 
104
+ const host = url.hostname;
105
+ let subdomain;
106
+ if (host && host !== 'localhost' && !/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host)) {
107
+ const parts = host.split('.');
108
+ if (parts.length >= 3) subdomain = parts.slice(0, -2).join('.');
109
+ }
110
+
104
111
  const loaderProps = {
105
112
  path: originalPath,
106
113
  params: routeParams,
114
+ subdomain,
107
115
  request,
108
116
  }
109
117
 
@@ -13,7 +13,7 @@ import {
13
13
  useNotFoundState,
14
14
  } from '../notFoundState'
15
15
  import { useContextKey } from '../router/Route'
16
- import { routeNode as globalRouteNode } from '../router/router'
16
+ import { routeNode as globalRouteNode, initialPathname } from '../router/router'
17
17
  import { registerProtectedRoutes, unregisterProtectedRoutes } from '../router/router'
18
18
  import { useSortedScreens, getQualifiedRouteComponent } from '../router/useScreens'
19
19
  import { Screen } from './Screen'
@@ -171,6 +171,38 @@ function QualifiedNavigator({
171
171
  contextKey,
172
172
  router = StackRouter,
173
173
  }: NavigatorProps & { contextKey: string; screens: React.ReactNode[] }) {
174
+ // LATE MOUNT FIX: when a parent layout conditionally renders (e.g. auth gate),
175
+ // this navigator may mount after initialState was consumed. compute the
176
+ // correct initialRouteName from the original URL so the navigator starts on
177
+ // the right route instead of defaulting to the first one.
178
+ // uses initialPathname (captured at setup) instead of window.location.pathname
179
+ // because React Navigation's linking can push a wrong URL during the delay.
180
+ const resolvedInitialRouteName = React.useMemo(() => {
181
+ if (initialRouteName) return initialRouteName
182
+
183
+ const browserPath =
184
+ initialPathname ??
185
+ (typeof window !== 'undefined' ? window.location.pathname : undefined)
186
+ if (!browserPath) return undefined
187
+
188
+ // extract screen names from the screens array
189
+ const screenNames: string[] = []
190
+ for (const screen of screens) {
191
+ const props = (screen as any)?.props
192
+ if (props?.name) screenNames.push(props.name)
193
+ }
194
+
195
+ // find which screen matches the URL
196
+ for (const name of screenNames) {
197
+ const base = name.replace(/\/index$/, '')
198
+ if (browserPath.endsWith('/' + base) || browserPath.includes('/' + base + '/')) {
199
+ return name
200
+ }
201
+ }
202
+
203
+ return undefined
204
+ }, [initialRouteName, screens])
205
+
174
206
  const { state, navigation, descriptors, NavigationContent } = useNavigationBuilder(
175
207
  router,
176
208
  {
@@ -178,7 +210,7 @@ function QualifiedNavigator({
178
210
  id: contextKey,
179
211
  children: screens,
180
212
  screenOptions,
181
- initialRouteName,
213
+ initialRouteName: resolvedInitialRouteName,
182
214
  }
183
215
  )
184
216
 
@@ -252,11 +284,13 @@ export function useSlot() {
252
284
 
253
285
  const renderedElement = descriptorsRef.current[current.key]?.render() ?? null
254
286
 
255
- // Use static key to prevent layout remounts when route keys change during navigation.
256
- // Safe because Slot only renders one screen at a time.
257
- // Use cloneElement to properly clone the React element with a new key.
287
+ // Use key based on route name to prevent layout remounts when route keys change
288
+ // (same route, different key), while allowing React to swap components when
289
+ // the actual route changes (e.g. late-mounting navigator correcting its state).
258
290
  if (renderedElement !== null) {
259
- return React.cloneElement(renderedElement, { key: SLOT_STATIC_KEY })
291
+ return React.cloneElement(renderedElement, {
292
+ key: `${SLOT_STATIC_KEY}-${current.name}`,
293
+ })
260
294
  }
261
295
 
262
296
  return renderedElement