one 1.2.22 → 1.2.23

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 (144) hide show
  1. package/dist/cjs/cli/build.cjs +10 -2
  2. package/dist/cjs/cli/build.js +13 -2
  3. package/dist/cjs/cli/build.js.map +1 -1
  4. package/dist/cjs/cli/build.native.js +12 -3
  5. package/dist/cjs/cli/build.native.js.map +1 -1
  6. package/dist/cjs/cli/buildPage.cjs +33 -12
  7. package/dist/cjs/cli/buildPage.js +31 -12
  8. package/dist/cjs/cli/buildPage.js.map +1 -1
  9. package/dist/cjs/cli/buildPage.native.js +57 -18
  10. package/dist/cjs/cli/buildPage.native.js.map +1 -1
  11. package/dist/cjs/createApp.cjs +3 -1
  12. package/dist/cjs/createApp.js +3 -2
  13. package/dist/cjs/createApp.js.map +1 -1
  14. package/dist/cjs/fork/useLinking.cjs +2 -2
  15. package/dist/cjs/fork/useLinking.js +2 -2
  16. package/dist/cjs/fork/useLinking.js.map +1 -1
  17. package/dist/cjs/index.cjs +2 -0
  18. package/dist/cjs/index.js +2 -1
  19. package/dist/cjs/index.js.map +1 -1
  20. package/dist/cjs/index.native.js +2 -0
  21. package/dist/cjs/index.native.js.map +1 -1
  22. package/dist/cjs/router/router.cjs +22 -14
  23. package/dist/cjs/router/router.js +26 -18
  24. package/dist/cjs/router/router.js.map +1 -1
  25. package/dist/cjs/router/router.native.js +23 -15
  26. package/dist/cjs/router/router.native.js.map +1 -1
  27. package/dist/cjs/router/useScreens.cjs +5 -1
  28. package/dist/cjs/router/useScreens.js +3 -1
  29. package/dist/cjs/router/useScreens.js.map +1 -1
  30. package/dist/cjs/router/useScreens.native.js.map +1 -1
  31. package/dist/cjs/router/useViteRoutes.cjs +45 -0
  32. package/dist/cjs/router/useViteRoutes.js +43 -0
  33. package/dist/cjs/router/useViteRoutes.js.map +1 -1
  34. package/dist/cjs/router/useViteRoutes.native.js +64 -0
  35. package/dist/cjs/router/useViteRoutes.native.js.map +1 -1
  36. package/dist/cjs/server/oneServe.cjs +2 -1
  37. package/dist/cjs/server/oneServe.js +2 -1
  38. package/dist/cjs/server/oneServe.js.map +1 -1
  39. package/dist/cjs/server/oneServe.native.js +2 -1
  40. package/dist/cjs/server/oneServe.native.js.map +1 -1
  41. package/dist/cjs/useLoader.cjs +39 -40
  42. package/dist/cjs/useLoader.js +10 -7
  43. package/dist/cjs/useLoader.js.map +1 -1
  44. package/dist/cjs/useLoader.native.js +54 -54
  45. package/dist/cjs/useLoader.native.js.map +1 -1
  46. package/dist/cjs/vite/plugins/virtualEntryPlugin.cjs +12 -1
  47. package/dist/cjs/vite/plugins/virtualEntryPlugin.js +12 -1
  48. package/dist/cjs/vite/plugins/virtualEntryPlugin.js.map +1 -1
  49. package/dist/cjs/vite/plugins/virtualEntryPlugin.native.js +12 -1
  50. package/dist/cjs/vite/plugins/virtualEntryPlugin.native.js.map +1 -1
  51. package/dist/esm/cli/build.js +13 -2
  52. package/dist/esm/cli/build.js.map +1 -1
  53. package/dist/esm/cli/build.mjs +10 -2
  54. package/dist/esm/cli/build.mjs.map +1 -1
  55. package/dist/esm/cli/build.native.js +12 -3
  56. package/dist/esm/cli/build.native.js.map +1 -1
  57. package/dist/esm/cli/buildPage.js +31 -12
  58. package/dist/esm/cli/buildPage.js.map +1 -1
  59. package/dist/esm/cli/buildPage.mjs +33 -12
  60. package/dist/esm/cli/buildPage.mjs.map +1 -1
  61. package/dist/esm/cli/buildPage.native.js +57 -18
  62. package/dist/esm/cli/buildPage.native.js.map +1 -1
  63. package/dist/esm/createApp.js +3 -2
  64. package/dist/esm/createApp.js.map +1 -1
  65. package/dist/esm/createApp.mjs +3 -1
  66. package/dist/esm/createApp.mjs.map +1 -1
  67. package/dist/esm/fork/useLinking.js +2 -2
  68. package/dist/esm/fork/useLinking.js.map +1 -1
  69. package/dist/esm/fork/useLinking.mjs +2 -2
  70. package/dist/esm/fork/useLinking.mjs.map +1 -1
  71. package/dist/esm/index.js +2 -0
  72. package/dist/esm/index.js.map +1 -1
  73. package/dist/esm/index.mjs +2 -1
  74. package/dist/esm/index.mjs.map +1 -1
  75. package/dist/esm/index.native.js +2 -1
  76. package/dist/esm/index.native.js.map +1 -1
  77. package/dist/esm/router/router.js +32 -18
  78. package/dist/esm/router/router.js.map +1 -1
  79. package/dist/esm/router/router.mjs +23 -16
  80. package/dist/esm/router/router.mjs.map +1 -1
  81. package/dist/esm/router/router.native.js +24 -17
  82. package/dist/esm/router/router.native.js.map +1 -1
  83. package/dist/esm/router/useScreens.js +3 -1
  84. package/dist/esm/router/useScreens.js.map +1 -1
  85. package/dist/esm/router/useScreens.mjs +5 -1
  86. package/dist/esm/router/useScreens.mjs.map +1 -1
  87. package/dist/esm/router/useScreens.native.js.map +1 -1
  88. package/dist/esm/router/useViteRoutes.js +43 -0
  89. package/dist/esm/router/useViteRoutes.js.map +1 -1
  90. package/dist/esm/router/useViteRoutes.mjs +44 -1
  91. package/dist/esm/router/useViteRoutes.mjs.map +1 -1
  92. package/dist/esm/router/useViteRoutes.native.js +63 -1
  93. package/dist/esm/router/useViteRoutes.native.js.map +1 -1
  94. package/dist/esm/server/oneServe.js +2 -1
  95. package/dist/esm/server/oneServe.js.map +1 -1
  96. package/dist/esm/server/oneServe.mjs +2 -1
  97. package/dist/esm/server/oneServe.mjs.map +1 -1
  98. package/dist/esm/server/oneServe.native.js +2 -1
  99. package/dist/esm/server/oneServe.native.js.map +1 -1
  100. package/dist/esm/useLoader.js +10 -8
  101. package/dist/esm/useLoader.js.map +1 -1
  102. package/dist/esm/useLoader.mjs +40 -41
  103. package/dist/esm/useLoader.mjs.map +1 -1
  104. package/dist/esm/useLoader.native.js +55 -55
  105. package/dist/esm/useLoader.native.js.map +1 -1
  106. package/dist/esm/vite/plugins/virtualEntryPlugin.js +12 -1
  107. package/dist/esm/vite/plugins/virtualEntryPlugin.js.map +1 -1
  108. package/dist/esm/vite/plugins/virtualEntryPlugin.mjs +12 -1
  109. package/dist/esm/vite/plugins/virtualEntryPlugin.mjs.map +1 -1
  110. package/dist/esm/vite/plugins/virtualEntryPlugin.native.js +12 -1
  111. package/dist/esm/vite/plugins/virtualEntryPlugin.native.js.map +1 -1
  112. package/package.json +11 -11
  113. package/src/cli/build.ts +18 -1
  114. package/src/cli/buildPage.ts +38 -6
  115. package/src/createApp.tsx +6 -3
  116. package/src/fork/useLinking.ts +10 -1
  117. package/src/index.ts +1 -0
  118. package/src/router/router.ts +47 -24
  119. package/src/router/useScreens.tsx +5 -3
  120. package/src/router/useViteRoutes.tsx +109 -1
  121. package/src/server/oneServe.ts +1 -0
  122. package/src/types.ts +2 -0
  123. package/src/useLoader.ts +21 -13
  124. package/src/vite/plugins/virtualEntryPlugin.ts +12 -1
  125. package/src/vite/types.ts +13 -0
  126. package/types/cli/build.d.ts.map +1 -1
  127. package/types/cli/buildPage.d.ts +1 -1
  128. package/types/cli/buildPage.d.ts.map +1 -1
  129. package/types/createApp.d.ts.map +1 -1
  130. package/types/fork/useLinking.d.ts.map +1 -1
  131. package/types/index.d.ts +1 -0
  132. package/types/index.d.ts.map +1 -1
  133. package/types/router/router.d.ts +3 -2
  134. package/types/router/router.d.ts.map +1 -1
  135. package/types/router/useScreens.d.ts.map +1 -1
  136. package/types/router/useViteRoutes.d.ts +9 -0
  137. package/types/router/useViteRoutes.d.ts.map +1 -1
  138. package/types/server/oneServe.d.ts.map +1 -1
  139. package/types/types.d.ts +2 -0
  140. package/types/types.d.ts.map +1 -1
  141. package/types/useLoader.d.ts.map +1 -1
  142. package/types/vite/plugins/virtualEntryPlugin.d.ts.map +1 -1
  143. package/types/vite/types.d.ts +12 -0
  144. package/types/vite/types.d.ts.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one",
3
- "version": "1.2.22",
3
+ "version": "1.2.23",
4
4
  "license": "BSD-3-Clause",
5
5
  "sideEffects": [
6
6
  "setup.mjs",
@@ -121,24 +121,24 @@
121
121
  "@react-navigation/routers": "~7.5.1",
122
122
  "@swc/core": "^1.14.0",
123
123
  "@ungap/structured-clone": "^1.2.0",
124
- "@vxrn/compiler": "1.2.22",
125
- "@vxrn/resolve": "1.2.22",
126
- "@vxrn/tslib-lite": "1.2.22",
127
- "@vxrn/universal-color-scheme": "1.2.22",
128
- "@vxrn/use-isomorphic-layout-effect": "1.2.22",
129
- "@vxrn/vite-plugin-metro": "1.2.22",
124
+ "@vxrn/compiler": "1.2.23",
125
+ "@vxrn/resolve": "1.2.23",
126
+ "@vxrn/tslib-lite": "1.2.23",
127
+ "@vxrn/universal-color-scheme": "1.2.23",
128
+ "@vxrn/use-isomorphic-layout-effect": "1.2.23",
129
+ "@vxrn/vite-plugin-metro": "1.2.23",
130
130
  "babel-dead-code-elimination": "^1.0.10",
131
131
  "babel-plugin-module-resolver": "^5.0.2",
132
132
  "citty": "^0.1.6",
133
133
  "core-js": "^3.38.1",
134
- "create-vxrn": "1.2.22",
134
+ "create-vxrn": "1.2.23",
135
135
  "escape-string-regexp": "^5.0.0",
136
136
  "expo-linking": "~8.0.8",
137
137
  "expo-modules-core": "~3.0.24",
138
138
  "fast-deep-equal": "^3.1.3",
139
139
  "fast-glob": "^3.3.3",
140
140
  "fs-extra": "^11.2.0",
141
- "hono": "^4.9.2",
141
+ "hono": "^4.10.7",
142
142
  "lightningcss": "^1.30.1",
143
143
  "micromatch": "^4.0.7",
144
144
  "nanoid": "^3.3.7",
@@ -157,7 +157,7 @@
157
157
  "vite": "^7.1.12",
158
158
  "vite-plugin-barrel": "^0.4.1",
159
159
  "vite-tsconfig-paths": "^5.1.4",
160
- "vxrn": "1.2.22",
160
+ "vxrn": "1.2.23",
161
161
  "ws": "^8.18.0",
162
162
  "xxhashjs": "^0.2.2"
163
163
  },
@@ -175,7 +175,7 @@
175
175
  "devDependencies": {
176
176
  "@react-navigation/core": "^7.13.0",
177
177
  "@react-navigation/native": "~7.1.19",
178
- "@tamagui/build": "^1.138.6",
178
+ "@tamagui/build": "^1.139.1",
179
179
  "@types/node": "^24.10.0",
180
180
  "@types/react-dom": "^19.2.2",
181
181
  "@types/xxhashjs": "^0.2.4",
package/src/cli/build.ts CHANGED
@@ -403,6 +403,22 @@ export async function build(args: {
403
403
  // nested path pages need to reference root assets
404
404
  .map((path) => `/${path}`)
405
405
 
406
+ // Read CSS file contents if inlineLayoutCSS is enabled
407
+ let allCSSContents: string[] | undefined
408
+ if (oneOptions.web?.inlineLayoutCSS) {
409
+ allCSSContents = await Promise.all(
410
+ allCSS.map(async (cssPath) => {
411
+ const filePath = join(clientDir, cssPath)
412
+ try {
413
+ return await FSExtra.readFile(filePath, 'utf-8')
414
+ } catch (err) {
415
+ console.warn(`[one] Warning: Could not read CSS file ${filePath}`)
416
+ return ''
417
+ }
418
+ })
419
+ )
420
+ }
421
+
406
422
  if (process.env.DEBUG) {
407
423
  console.info('[one] building routes', { foundRoute, layoutEntries, allEntries, allCSS })
408
424
  }
@@ -468,7 +484,8 @@ export async function build(args: {
468
484
  serverJsPath,
469
485
  preloads,
470
486
  allCSS,
471
- routePreloads
487
+ routePreloads,
488
+ allCSSContents
472
489
  )
473
490
  })
474
491
 
@@ -22,7 +22,8 @@ export async function buildPage(
22
22
  serverJsPath: string,
23
23
  preloads: string[],
24
24
  allCSS: string[],
25
- routePreloads: Record<string, string>
25
+ routePreloads: Record<string, string>,
26
+ allCSSContents?: string[]
26
27
  ): Promise<One.RouteBuildInfo> {
27
28
  const render = await getRender(serverEntry)
28
29
  const htmlPath = `${path.endsWith('/') ? `${removeTrailingSlash(path)}/index` : path}.html`
@@ -35,12 +36,33 @@ export async function buildPage(
35
36
  let loaderData = {}
36
37
 
37
38
  try {
38
- // todo await optimize
39
- await FSExtra.writeFile(
40
- join(clientDir, preloadPath),
41
- preloads.map((preload) => `import "${preload}"`).join('\n')
39
+ // generate preload file with route module registration
40
+ const routeImports: string[] = []
41
+ const routeRegistrations: string[] = []
42
+ let routeIndex = 0
43
+
44
+ for (const [routeKey, bundlePath] of Object.entries(routePreloads)) {
45
+ const varName = `_r${routeIndex++}`
46
+ routeImports.push(`import * as ${varName} from "${bundlePath}"`)
47
+ routeRegistrations.push(`registerPreloadedRoute("${routeKey}", ${varName})`)
48
+ }
49
+
50
+ // Use window global for registration since ES module exports get tree-shaken
51
+ const registrationCalls = routeRegistrations.map((call) =>
52
+ call.replace('registerPreloadedRoute(', 'window.__oneRegisterPreloadedRoute(')
42
53
  )
43
54
 
55
+ const preloadContent = [
56
+ // import all route modules
57
+ ...routeImports,
58
+ // static imports for cache warming (original behavior)
59
+ ...preloads.map((preload) => `import "${preload}"`),
60
+ // register all route modules using window global
61
+ ...registrationCalls,
62
+ ].join('\n')
63
+
64
+ await FSExtra.writeFile(join(clientDir, preloadPath), preloadContent)
65
+
44
66
  const exported = await import(toAbsolute(serverJsPath))
45
67
 
46
68
  if (exported.loader) {
@@ -73,11 +95,20 @@ if (typeof document === 'undefined') globalThis.document = {}
73
95
  loaderProps,
74
96
  loaderData,
75
97
  css: allCSS,
98
+ cssContents: allCSSContents,
76
99
  mode: 'ssg',
77
100
  routePreloads,
78
101
  })
79
102
  await outputFile(htmlOutPath, html)
80
103
  } else if (foundRoute.type === 'spa') {
104
+ // Generate CSS - either inline styles or link tags
105
+ const cssOutput = allCSSContents
106
+ ? allCSSContents
107
+ .filter(Boolean)
108
+ .map((content) => ` <style>${content}</style>`)
109
+ .join('\n')
110
+ : allCSS.map((file) => ` <link rel="stylesheet" href=${file} />`).join('\n')
111
+
81
112
  await outputFile(
82
113
  htmlOutPath,
83
114
  `<html><head>
@@ -85,7 +116,7 @@ if (typeof document === 'undefined') globalThis.document = {}
85
116
  ${preloads
86
117
  .map((preload) => ` <script type="module" src="${preload}"></script>`)
87
118
  .join('\n')}
88
- ${allCSS.map((file) => ` <link rel="stylesheet" href=${file} />`).join('\n')}
119
+ ${cssOutput}
89
120
  </head></html>`
90
121
  )
91
122
  }
@@ -112,6 +143,7 @@ params:\n\n${JSON.stringify(params || null, null, 2)}`
112
143
  return {
113
144
  type: foundRoute.type,
114
145
  css: allCSS,
146
+ cssContents: allCSSContents,
115
147
  routeFile: foundRoute.file,
116
148
  middlewares,
117
149
  cleanPath,
package/src/createApp.tsx CHANGED
@@ -26,7 +26,8 @@ export function createApp(options: CreateAppProps) {
26
26
  return {
27
27
  options,
28
28
  render: async (props: RenderAppProps) => {
29
- let { loaderData, loaderProps, css, mode, loaderServerData, routePreloads } = props
29
+ let { loaderData, loaderProps, css, cssContents, mode, loaderServerData, routePreloads } =
30
+ props
30
31
 
31
32
  setServerContext({
32
33
  postRenderData: loaderServerData,
@@ -34,6 +35,7 @@ export function createApp(options: CreateAppProps) {
34
35
  loaderProps,
35
36
  mode,
36
37
  css,
38
+ cssContents,
37
39
  routePreloads,
38
40
  })
39
41
 
@@ -121,14 +123,15 @@ export function createApp(options: CreateAppProps) {
121
123
  const serverContext = getServerContext() || {}
122
124
  const routePreloads = serverContext.routePreloads
123
125
 
124
- // preload routes using build-time mapping (production only, dev has no preloads)
126
+ // preload routes using build-time mapping (production SSG)
127
+ // for SPA/dev mode, fall back to importing root layout directly
125
128
  const preloadPromises = routePreloads
126
129
  ? Object.entries(routePreloads).map(async ([routeKey, bundlePath]) => {
127
130
  const mod = await import(/* @vite-ignore */ bundlePath)
128
131
  registerPreloadedRoute(routeKey, mod)
129
132
  return mod
130
133
  })
131
- : []
134
+ : [options.routes[`/${options.routerRoot}/_layout.tsx`]?.()]
132
135
 
133
136
  return Promise.all(preloadPromises)
134
137
  .then(() => {
@@ -157,9 +157,18 @@ export function useLinking(
157
157
  (state: ResultState) => {
158
158
  const navigation = ref.current
159
159
  const rootState = navigation?.getRootState()
160
+ // @modified - start
161
+ // Fix for back/forward button navigation: if routeNames is undefined (stale state),
162
+ // don't reject the navigation. This can happen during browser back/forward.
163
+ // See: https://github.com/expo/expo/pull/37747
164
+ const routeNames = rootState?.routeNames
165
+ if (!routeNames) {
166
+ return false // Don't reject navigation if we can't validate
167
+ }
168
+ // @modified - end
160
169
  // Make sure that the routes in the state exist in the root navigator
161
170
  // Otherwise there's an error in the linking configuration
162
- return state?.routes.some((r) => !rootState?.routeNames?.includes(r.name))
171
+ return state?.routes.some((r) => !routeNames.includes(r.name))
163
172
  },
164
173
  [ref]
165
174
  )
package/src/index.ts CHANGED
@@ -66,6 +66,7 @@ export { createRoute, route } from './router/createRoute'
66
66
  export { router } from './router/imperative-api'
67
67
  export * as routerStore from './router/router'
68
68
  export { useNavigation } from './router/useNavigation'
69
+ export { registerPreloadedRoute } from './router/useViteRoutes'
69
70
  export type { Endpoint, LoaderProps } from './types'
70
71
  // React Navigation
71
72
  export { useFocusEffect } from './useFocusEffect'
@@ -6,7 +6,13 @@
6
6
 
7
7
  import { type NavigationContainerRefWithCurrent, StackActions } from '@react-navigation/native'
8
8
  import * as Linking from 'expo-linking'
9
- import { type ComponentType, Fragment, startTransition, useSyncExternalStore } from 'react'
9
+ import {
10
+ type ComponentType,
11
+ Fragment,
12
+ startTransition,
13
+ useDeferredValue,
14
+ useSyncExternalStore,
15
+ } from 'react'
10
16
  import { Platform } from 'react-native'
11
17
  import type { OneRouter } from '../interfaces/router'
12
18
  import { resolveHref } from '../link/href'
@@ -24,6 +30,7 @@ import { getLinking, resetLinking, setupLinking } from './linkingConfig'
24
30
  import type { RouteNode } from './Route'
25
31
  import { sortRoutes } from './sortRoutes'
26
32
  import { getQualifiedRouteComponent } from './useScreens'
33
+ import { preloadRouteModules } from './useViteRoutes'
27
34
  import { getNavigateAction } from './utils/getNavigateAction'
28
35
 
29
36
  // Module-scoped variables
@@ -334,7 +341,9 @@ export function routeInfoSnapshot() {
334
341
 
335
342
  // Hook functions
336
343
  export function useOneRouter() {
337
- return useSyncExternalStore(subscribeToStore, snapshot, snapshot)
344
+ const state = useSyncExternalStore(subscribeToStore, snapshot, snapshot)
345
+ // useDeferredValue makes the transition concurrent, preventing main thread blocking
346
+ return useDeferredValue(state)
338
347
  }
339
348
 
340
349
  function syncStoreRootState() {
@@ -351,12 +360,14 @@ function syncStoreRootState() {
351
360
 
352
361
  export function useStoreRootState() {
353
362
  syncStoreRootState()
354
- return useSyncExternalStore(subscribeToRootState, rootStateSnapshot, rootStateSnapshot)
363
+ const state = useSyncExternalStore(subscribeToRootState, rootStateSnapshot, rootStateSnapshot)
364
+ return useDeferredValue(state)
355
365
  }
356
366
 
357
367
  export function useStoreRouteInfo() {
358
368
  syncStoreRootState()
359
- return useSyncExternalStore(subscribeToRootState, routeInfoSnapshot, routeInfoSnapshot)
369
+ const state = useSyncExternalStore(subscribeToRootState, routeInfoSnapshot, routeInfoSnapshot)
370
+ return useDeferredValue(state)
360
371
  }
361
372
 
362
373
  // Cleanup function
@@ -367,38 +378,49 @@ export function cleanup() {
367
378
  }
368
379
 
369
380
  // TODO
370
- export const preloadingLoader = {}
371
-
372
- function setupPreload(href: string) {
373
- if (preloadingLoader[href]) return
374
- preloadingLoader[href] = async () => {
375
- try {
376
- const [_preload, loader] = await Promise.all([
377
- dynamicImport(getPreloadPath(href)),
378
- dynamicImport(getLoaderPath(href)),
379
- ])
380
- const response = await loader
381
- return await response.loader?.()
382
- } catch (err) {
383
- console.error(`Error preloading loader: ${err}`)
381
+ export const preloadingLoader: Record<string, Promise<any> | undefined> = {}
382
+
383
+ async function doPreload(href: string) {
384
+ const preloadPath = getPreloadPath(href)
385
+ const loaderPath = getLoaderPath(href)
386
+ try {
387
+ const [_preload, loader] = await Promise.all([
388
+ dynamicImport(preloadPath),
389
+ dynamicImport(loaderPath),
390
+ preloadRouteModules(href),
391
+ ])
392
+
393
+ if (!loader?.loader) {
384
394
  return null
385
395
  }
396
+
397
+ const result = await loader.loader()
398
+ return result ?? null
399
+ } catch (err) {
400
+ console.error(`[one] preload error for ${href}:`, err)
401
+ return null
386
402
  }
387
403
  }
388
404
 
389
- export function preloadRoute(href: string) {
405
+ // Store resolved preload data separately from promises
406
+ export const preloadedLoaderData: Record<string, any> = {}
407
+
408
+ export function preloadRoute(href: string): Promise<any> | undefined {
390
409
  if (process.env.TAMAGUI_TARGET === 'native') {
391
- // not enabled for now
392
410
  return
393
411
  }
394
412
  if (process.env.NODE_ENV === 'development') {
395
413
  return
396
414
  }
397
415
 
398
- setupPreload(href)
399
- if (typeof preloadingLoader[href] === 'function') {
400
- void preloadingLoader[href]()
416
+ if (!preloadingLoader[href]) {
417
+ preloadingLoader[href] = doPreload(href).then((data) => {
418
+ // Store the resolved data for synchronous access
419
+ preloadedLoaderData[href] = data
420
+ return data
421
+ })
401
422
  }
423
+ return preloadingLoader[href]
402
424
  }
403
425
 
404
426
  export async function linkTo(href: string, event?: string, options?: OneRouter.LinkToOptions) {
@@ -477,7 +499,8 @@ export async function linkTo(href: string, event?: string, options?: OneRouter.L
477
499
 
478
500
  setLoadingState('loading')
479
501
 
480
- preloadRoute(href)
502
+ // await preload on web to ensure route modules are loaded before navigating
503
+ await preloadRoute(href)
481
504
 
482
505
  const rootState = navigationRef.getRootState()
483
506
 
@@ -205,9 +205,11 @@ export function getQualifiedRouteComponent(value: RouteNode) {
205
205
  __html: `globalThis['global'] = globalThis`,
206
206
  }}
207
207
  />
208
- {serverContext?.css?.map((file) => {
209
- return <link key={file} rel="stylesheet" href={file} />
210
- })}
208
+ {serverContext?.cssContents
209
+ ? serverContext.cssContents.map((content, i) =>
210
+ content ? <style key={i} dangerouslySetInnerHTML={{ __html: content }} /> : null
211
+ )
212
+ : serverContext?.css?.map((file) => <link key={file} rel="stylesheet" href={file} />)}
211
213
  <ServerContextScript />
212
214
  {headChildren}
213
215
  </head>
@@ -37,6 +37,114 @@ export function getPreloadedModule(key: string): any {
37
37
  return preloadedModules[key]
38
38
  }
39
39
 
40
+ export function getPreloadedModuleKeys(): string[] {
41
+ return Object.keys(preloadedModules)
42
+ }
43
+
44
+ /**
45
+ * Checks if a dynamic route pattern matches an actual path.
46
+ * Used to preload route modules for dynamic routes like [slug].
47
+ *
48
+ * @example
49
+ * matchDynamicRoute("docs/[slug]", "docs/getting-started") // true
50
+ * matchDynamicRoute("[...slug]", "a/b/c") // true (catch-all)
51
+ */
52
+ function matchDynamicRoute(routePattern: string, actualPath: string): boolean {
53
+ const routeSegments = routePattern.split('/')
54
+ const pathSegments = actualPath.split('/')
55
+
56
+ // handle catch-all routes like [...slug]
57
+ const hasCatchAll = routeSegments.some((s) => s.startsWith('[...'))
58
+ if (hasCatchAll) {
59
+ // find the catch-all segment position
60
+ const catchAllIdx = routeSegments.findIndex((s) => s.startsWith('[...'))
61
+ // all segments before catch-all must match exactly (or be dynamic)
62
+ for (let i = 0; i < catchAllIdx; i++) {
63
+ if (!routeSegments[i]) continue
64
+ if (routeSegments[i].startsWith('[')) continue // dynamic segment matches anything
65
+ if (routeSegments[i] !== pathSegments[i]) return false
66
+ }
67
+ // catch-all matches any remaining segments
68
+ return pathSegments.length >= catchAllIdx
69
+ }
70
+
71
+ // for non-catch-all, segment count should match
72
+ if (routeSegments.length !== pathSegments.length) return false
73
+
74
+ for (let i = 0; i < routeSegments.length; i++) {
75
+ const routeSeg = routeSegments[i]
76
+ const pathSeg = pathSegments[i]
77
+
78
+ // dynamic segment [param] matches any value
79
+ if (routeSeg.startsWith('[') && routeSeg.endsWith(']')) {
80
+ continue
81
+ }
82
+
83
+ // static segment must match exactly
84
+ if (routeSeg !== pathSeg) return false
85
+ }
86
+
87
+ return true
88
+ }
89
+
90
+ /**
91
+ * Preloads route modules for a given URL path (production only).
92
+ * This ensures route components are loaded before navigation completes,
93
+ * preventing Suspense boundaries from triggering and causing flicker.
94
+ *
95
+ * Called during `linkTo()` to preload routes before client-side navigation.
96
+ */
97
+ export async function preloadRouteModules(href: string): Promise<void> {
98
+ const globbed = globalThis['__importMetaGlobbed']
99
+ if (!globbed) return
100
+
101
+ // normalize href to match route keys - /docs -> docs
102
+ const normalizedHref = href === '/' ? '' : href.replace(/^\//, '').replace(/\/$/, '')
103
+
104
+ const promises: Promise<any>[] = []
105
+
106
+ for (const key of Object.keys(globbed)) {
107
+ // key looks like "/app/(site)/docs/_layout.tsx" or "/app/(site)/docs/index+ssg.tsx"
108
+ // strip the /app/ prefix first
109
+ let routePath = key.replace(/^\/[^/]+\//, '')
110
+
111
+ // strip route groups like (site), (app) etc
112
+ routePath = routePath.replace(/\([^)]+\)\//g, '')
113
+
114
+ // strip file suffixes but keep the path structure
115
+ routePath = routePath
116
+ .replace(/\/_layout\.tsx$/, '')
117
+ .replace(/\/index(\+[a-z]+)?\.tsx$/, '')
118
+ .replace(/(\+[a-z]+)?\.tsx$/, '')
119
+
120
+ // remove leading slash if any
121
+ routePath = routePath.replace(/^\//, '')
122
+
123
+ // check if this route is part of the target path
124
+ const isStaticMatch =
125
+ routePath === normalizedHref || // exact match
126
+ routePath.startsWith(normalizedHref + '/') || // child route
127
+ normalizedHref.startsWith(routePath + '/') || // parent layout
128
+ routePath === '' || // root layout
129
+ (normalizedHref !== '' && routePath === normalizedHref.split('/')[0]) // top-level match
130
+
131
+ // also check dynamic route patterns like docs/[slug]
132
+ const isDynamicMatch = routePath.includes('[') && matchDynamicRoute(routePath, normalizedHref)
133
+
134
+ if ((isStaticMatch || isDynamicMatch) && typeof globbed[key] === 'function') {
135
+ promises.push(
136
+ globbed[key]()
137
+ .then((mod: any) => {
138
+ preloadedModules[key] = mod
139
+ })
140
+ .catch(() => {})
141
+ )
142
+ }
143
+ }
144
+
145
+ await Promise.all(promises)
146
+ }
147
+
40
148
  export function loadRoutes(
41
149
  paths: GlobbedRouteImports,
42
150
  routerRoot: string,
@@ -88,7 +196,7 @@ export function globbedRoutesToRouteContext(
88
196
  return loadedRoutes[id]
89
197
  }
90
198
 
91
- // check if this route was preloaded before hydration
199
+ // check if this route was preloaded (via preload file or hydration)
92
200
  const preloadKey = id.replace('./', `/${routerRoot}/`)
93
201
  const preloaded = getPreloadedModule(preloadKey)
94
202
  if (preloaded) {
@@ -119,6 +119,7 @@ export async function oneServe(oneOptions: One.PluginOptions, buildInfo: One.Bui
119
119
  path: loaderProps?.path || '/',
120
120
  preloads,
121
121
  css: buildInfo.css,
122
+ cssContents: buildInfo.cssContents,
122
123
  })
123
124
 
124
125
  return new Response(rendered, {
package/src/types.ts CHANGED
@@ -21,6 +21,8 @@ export type RenderAppProps = {
21
21
  path: string
22
22
  preloads?: string[]
23
23
  css?: string[]
24
+ /** When inlineLayoutCSS is enabled, this contains the actual CSS content to inline */
25
+ cssContents?: string[]
24
26
  loaderServerData?: any
25
27
  loaderData?: any
26
28
  loaderProps?: LoaderProps
package/src/useLoader.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { useCallback, useSyncExternalStore } from 'react'
1
+ import { useCallback, useDeferredValue, useSyncExternalStore } from 'react'
2
2
  import { useParams, usePathname } from './hooks'
3
- import { resolveHref } from './link/href'
4
- import { preloadingLoader } from './router/router'
3
+ import { preloadedLoaderData, preloadingLoader } from './router/router'
5
4
  import { getLoaderPath } from './utils/cleanUrl'
6
5
  import { dynamicImport } from './utils/dynamicImport'
7
6
  import { weakKey } from './utils/weakKey'
@@ -89,7 +88,9 @@ export function useLoaderState<
89
88
 
90
89
  const params = useParams()
91
90
  const pathname = usePathname()
92
- const currentPath = resolveHref({ pathname, params }).replace(/index$/, '')
91
+ // use just the pathname for matching, don't use resolveHref which adds params as query string
92
+ // (the pathname is already resolved like /docs/getting-started, not /docs/[slug])
93
+ const currentPath = pathname.replace(/\/index$/, '').replace(/\/$/, '') || '/'
93
94
 
94
95
  // server-side
95
96
  if (typeof window === 'undefined' && loader) {
@@ -103,9 +104,9 @@ export function useLoaderState<
103
104
  return { data: serverData, refetch: async () => {}, state: 'idle' } as any
104
105
  }
105
106
 
106
- // preloaded data from SSR
107
- const preloadedData =
108
- loaderPropsFromServerContext?.path === currentPath ? loaderDataFromServerContext : undefined
107
+ // preloaded data from SSR/SSG - only use if server context path matches current path
108
+ const serverContextPath = loaderPropsFromServerContext?.path
109
+ const preloadedData = serverContextPath === currentPath ? loaderDataFromServerContext : undefined
109
110
 
110
111
  const loaderStateEntry = useSyncExternalStore(
111
112
  subscribe,
@@ -130,14 +131,21 @@ export function useLoaderState<
130
131
  !loaderStateEntry.hasLoadedOnce &&
131
132
  loader
132
133
  ) {
133
- // check for preloading loader first
134
- if (preloadingLoader[currentPath]) {
135
- if (typeof preloadingLoader[currentPath] === 'function') {
136
- preloadingLoader[currentPath] = preloadingLoader[currentPath]()
137
- }
138
- const promise = preloadingLoader[currentPath]
134
+ // check for already-resolved preloaded data first (synchronous)
135
+ const resolvedPreloadData = preloadedLoaderData[currentPath]
136
+ if (resolvedPreloadData !== undefined) {
137
+ // Data was preloaded and already resolved - use it directly
138
+ delete preloadedLoaderData[currentPath]
139
+ delete preloadingLoader[currentPath]
140
+ loaderStateEntry.data = resolvedPreloadData
141
+ loaderStateEntry.hasLoadedOnce = true
142
+ } else if (preloadingLoader[currentPath]) {
143
+ // Preload is in progress - wait for it
144
+ const preloadPromise = preloadingLoader[currentPath]!
145
+ const promise = preloadPromise
139
146
  .then((val: any) => {
140
147
  delete preloadingLoader[currentPath]
148
+ delete preloadedLoaderData[currentPath]
141
149
  updateState(currentPath, {
142
150
  data: val,
143
151
  hasLoadedOnce: true,
@@ -66,7 +66,18 @@ export function createVirtualEntry(options: {
66
66
  return `
67
67
  ${prependCode}
68
68
 
69
- import { createApp } from 'one'
69
+ import { createApp, registerPreloadedRoute as _registerPreloadedRoute } from 'one'
70
+
71
+ // Export registerPreloadedRoute so preload files can import it from this bundle
72
+ // Named export that wraps the original function
73
+ export function registerPreloadedRoute(key, module) {
74
+ return _registerPreloadedRoute(key, module)
75
+ }
76
+
77
+ // Also expose on window for debugging and to prevent tree-shaking
78
+ if (typeof window !== 'undefined') {
79
+ window.__oneRegisterPreloadedRoute = registerPreloadedRoute
80
+ }
70
81
 
71
82
  // globbing ${JSON.stringify(routeGlobs)}
72
83
  export default createApp({
package/src/vite/types.ts CHANGED
@@ -333,6 +333,15 @@ export namespace One {
333
333
  * @default node
334
334
  */
335
335
  deploy?: 'vercel' | 'node'
336
+
337
+ /**
338
+ * @experimental
339
+ * When true, inlines the CSS content directly into the HTML instead of using <link> tags.
340
+ * This can improve performance by eliminating an extra network request for CSS.
341
+ *
342
+ * @default false
343
+ */
344
+ inlineLayoutCSS?: boolean
336
345
  }
337
346
 
338
347
  server?: VXRNOptions['server']
@@ -411,10 +420,14 @@ export namespace One {
411
420
  loaderData: any
412
421
  preloads: string[]
413
422
  css: string[]
423
+ /** When inlineLayoutCSS is enabled, contains the actual CSS content */
424
+ cssContents?: string[]
414
425
  }
415
426
 
416
427
  export type ServerContext = {
417
428
  css?: string[]
429
+ /** When inlineLayoutCSS is enabled, this contains the actual CSS content to inline */
430
+ cssContents?: string[]
418
431
  postRenderData?: any
419
432
  loaderData?: any
420
433
  loaderProps?: any
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAmCA,wBAAsB,KAAK,CAAC,IAAI,EAAE;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,CAAA;CACrC,iBAmjBA"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAmCA,wBAAsB,KAAK,CAAC,IAAI,EAAE;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,CAAA;CACrC,iBAokBA"}
@@ -1,3 +1,3 @@
1
1
  import type { One, RouteInfo } from '../vite/types';
2
- export declare function buildPage(serverEntry: string, path: string, relativeId: string, params: any, foundRoute: RouteInfo<string>, clientManifestEntry: any, staticDir: string, clientDir: string, builtMiddlewares: Record<string, string>, serverJsPath: string, preloads: string[], allCSS: string[], routePreloads: Record<string, string>): Promise<One.RouteBuildInfo>;
2
+ export declare function buildPage(serverEntry: string, path: string, relativeId: string, params: any, foundRoute: RouteInfo<string>, clientManifestEntry: any, staticDir: string, clientDir: string, builtMiddlewares: Record<string, string>, serverJsPath: string, preloads: string[], allCSS: string[], routePreloads: Record<string, string>, allCSSContents?: string[]): Promise<One.RouteBuildInfo>;
3
3
  //# sourceMappingURL=buildPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"buildPage.d.ts","sourceRoot":"","sources":["../../src/cli/buildPage.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAInD,wBAAsB,SAAS,CAC7B,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,GAAG,EACX,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,EAC7B,mBAAmB,EAAE,GAAG,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAAE,EAClB,MAAM,EAAE,MAAM,EAAE,EAChB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAsG7B"}
1
+ {"version":3,"file":"buildPage.d.ts","sourceRoot":"","sources":["../../src/cli/buildPage.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAInD,wBAAsB,SAAS,CAC7B,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,GAAG,EACX,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,EAC7B,mBAAmB,EAAE,GAAG,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAAE,EAClB,MAAM,EAAE,MAAM,EAAE,EAChB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,cAAc,CAAC,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAqI7B"}