one 1.25.13 → 1.25.16

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 (195) hide show
  1. package/dist/cjs/cli/build.cjs +29 -25
  2. package/dist/cjs/cli/build.native.js +262 -219
  3. package/dist/cjs/cli/build.native.js.map +1 -1
  4. package/dist/cjs/cli/buildPage.cjs +8 -19
  5. package/dist/cjs/cli/buildPage.native.js +8 -19
  6. package/dist/cjs/cli/buildPage.native.js.map +1 -1
  7. package/dist/cjs/cli/buildPageWorker.cjs +1 -1
  8. package/dist/cjs/cli/buildPageWorker.native.js +1 -1
  9. package/dist/cjs/cli/buildPageWorker.native.js.map +1 -1
  10. package/dist/cjs/cli/serverRouteModules.cjs +47 -0
  11. package/dist/cjs/cli/serverRouteModules.native.js +52 -0
  12. package/dist/cjs/cli/serverRouteModules.native.js.map +1 -0
  13. package/dist/cjs/cli/workerPool.native.js.map +1 -1
  14. package/dist/cjs/createApp.cjs +3 -0
  15. package/dist/cjs/headless-server.native.js.map +1 -1
  16. package/dist/cjs/headless.cjs +4 -0
  17. package/dist/cjs/headless.native.js +4 -0
  18. package/dist/cjs/headless.native.js.map +1 -1
  19. package/dist/cjs/router/diagnoseRouteLoadFailure.native.js.map +1 -1
  20. package/dist/cjs/router/useViteRoutes.cjs +9 -1
  21. package/dist/cjs/router/useViteRoutes.native.js +9 -1
  22. package/dist/cjs/router/useViteRoutes.native.js.map +1 -1
  23. package/dist/cjs/server/oneServe.cjs +23 -4
  24. package/dist/cjs/server/oneServe.native.js +25 -6
  25. package/dist/cjs/server/oneServe.native.js.map +1 -1
  26. package/dist/cjs/server/routeExportsFromEntry.cjs +47 -0
  27. package/dist/cjs/server/routeExportsFromEntry.native.js +52 -0
  28. package/dist/cjs/server/routeExportsFromEntry.native.js.map +1 -0
  29. package/dist/cjs/server/workerHandler.cjs +19 -6
  30. package/dist/cjs/server/workerHandler.native.js +20 -6
  31. package/dist/cjs/server/workerHandler.native.js.map +1 -1
  32. package/dist/cjs/server/workerHandler.test.cjs +24 -16
  33. package/dist/cjs/server/workerHandler.test.native.js +27 -16
  34. package/dist/cjs/server/workerHandler.test.native.js.map +1 -1
  35. package/dist/cjs/views/ScrollBehavior.native.js.map +1 -1
  36. package/dist/cjs/vite/cloudflareWranglerConfig.native.js.map +1 -1
  37. package/dist/cjs/vite/one.cjs +5 -2
  38. package/dist/cjs/vite/one.native.js +5 -2
  39. package/dist/cjs/vite/one.native.js.map +1 -1
  40. package/dist/cjs/vite/plugins/clientTreeShakePlugin.cjs +5 -3
  41. package/dist/cjs/vite/plugins/clientTreeShakePlugin.native.js +6 -3
  42. package/dist/cjs/vite/plugins/clientTreeShakePlugin.native.js.map +1 -1
  43. package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.cjs +18 -0
  44. package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.native.js +18 -0
  45. package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.native.js.map +1 -1
  46. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.cjs +4 -9
  47. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js +4 -9
  48. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
  49. package/dist/cjs/vite/plugins/virtualEntryPlugin.cjs +7 -6
  50. package/dist/cjs/vite/plugins/virtualEntryPlugin.native.js +9 -6
  51. package/dist/cjs/vite/plugins/virtualEntryPlugin.native.js.map +1 -1
  52. package/dist/cjs/vite/plugins/workerdDevPlugin.cjs +0 -13
  53. package/dist/cjs/vite/plugins/workerdDevPlugin.native.js +26 -58
  54. package/dist/cjs/vite/plugins/workerdDevPlugin.native.js.map +1 -1
  55. package/dist/esm/cli/build.mjs +30 -26
  56. package/dist/esm/cli/build.mjs.map +1 -1
  57. package/dist/esm/cli/build.native.js +263 -220
  58. package/dist/esm/cli/build.native.js.map +1 -1
  59. package/dist/esm/cli/buildPage.mjs +8 -19
  60. package/dist/esm/cli/buildPage.mjs.map +1 -1
  61. package/dist/esm/cli/buildPage.native.js +8 -19
  62. package/dist/esm/cli/buildPage.native.js.map +1 -1
  63. package/dist/esm/cli/buildPageWorker.mjs +1 -1
  64. package/dist/esm/cli/buildPageWorker.mjs.map +1 -1
  65. package/dist/esm/cli/buildPageWorker.native.js +1 -1
  66. package/dist/esm/cli/buildPageWorker.native.js.map +1 -1
  67. package/dist/esm/cli/serverRouteModules.mjs +22 -0
  68. package/dist/esm/cli/serverRouteModules.mjs.map +1 -0
  69. package/dist/esm/cli/serverRouteModules.native.js +24 -0
  70. package/dist/esm/cli/serverRouteModules.native.js.map +1 -0
  71. package/dist/esm/cli/workerPool.mjs.map +1 -1
  72. package/dist/esm/cli/workerPool.native.js.map +1 -1
  73. package/dist/esm/createApp.mjs +4 -1
  74. package/dist/esm/createApp.mjs.map +1 -1
  75. package/dist/esm/fork/useLinking.native.test.mjs +1 -1
  76. package/dist/esm/getURL.native.test.mjs +1 -1
  77. package/dist/esm/headless-server.mjs.map +1 -1
  78. package/dist/esm/headless-server.native.js.map +1 -1
  79. package/dist/esm/headless.mjs +4 -0
  80. package/dist/esm/headless.mjs.map +1 -1
  81. package/dist/esm/headless.native.js +4 -0
  82. package/dist/esm/headless.native.js.map +1 -1
  83. package/dist/esm/router/diagnoseRouteLoadFailure.mjs.map +1 -1
  84. package/dist/esm/router/diagnoseRouteLoadFailure.native.js.map +1 -1
  85. package/dist/esm/router/hmrImport.native.test.mjs +1 -1
  86. package/dist/esm/router/routeHmr.native.test.mjs +1 -1
  87. package/dist/esm/router/routeHmr.native.test.mjs.map +1 -1
  88. package/dist/esm/router/useViteRoutes.mjs +9 -1
  89. package/dist/esm/router/useViteRoutes.mjs.map +1 -1
  90. package/dist/esm/router/useViteRoutes.native.js +9 -1
  91. package/dist/esm/router/useViteRoutes.native.js.map +1 -1
  92. package/dist/esm/server/oneServe.mjs +23 -4
  93. package/dist/esm/server/oneServe.mjs.map +1 -1
  94. package/dist/esm/server/oneServe.native.js +25 -6
  95. package/dist/esm/server/oneServe.native.js.map +1 -1
  96. package/dist/esm/server/routeExportsFromEntry.mjs +21 -0
  97. package/dist/esm/server/routeExportsFromEntry.mjs.map +1 -0
  98. package/dist/esm/server/routeExportsFromEntry.native.js +23 -0
  99. package/dist/esm/server/routeExportsFromEntry.native.js.map +1 -0
  100. package/dist/esm/server/workerHandler.mjs +19 -6
  101. package/dist/esm/server/workerHandler.mjs.map +1 -1
  102. package/dist/esm/server/workerHandler.native.js +20 -6
  103. package/dist/esm/server/workerHandler.native.js.map +1 -1
  104. package/dist/esm/server/workerHandler.test.mjs +24 -16
  105. package/dist/esm/server/workerHandler.test.mjs.map +1 -1
  106. package/dist/esm/server/workerHandler.test.native.js +27 -16
  107. package/dist/esm/server/workerHandler.test.native.js.map +1 -1
  108. package/dist/esm/views/ScrollBehavior.mjs.map +1 -1
  109. package/dist/esm/views/ScrollBehavior.native.js.map +1 -1
  110. package/dist/esm/vite/cloudflareWranglerConfig.mjs.map +1 -1
  111. package/dist/esm/vite/cloudflareWranglerConfig.native.js.map +1 -1
  112. package/dist/esm/vite/one.mjs +5 -2
  113. package/dist/esm/vite/one.mjs.map +1 -1
  114. package/dist/esm/vite/one.native.js +5 -2
  115. package/dist/esm/vite/one.native.js.map +1 -1
  116. package/dist/esm/vite/plugins/clientTreeShakePlugin.mjs +5 -3
  117. package/dist/esm/vite/plugins/clientTreeShakePlugin.mjs.map +1 -1
  118. package/dist/esm/vite/plugins/clientTreeShakePlugin.native.js +6 -3
  119. package/dist/esm/vite/plugins/clientTreeShakePlugin.native.js.map +1 -1
  120. package/dist/esm/vite/plugins/clientTreeShakePlugin.test.mjs +18 -0
  121. package/dist/esm/vite/plugins/clientTreeShakePlugin.test.mjs.map +1 -1
  122. package/dist/esm/vite/plugins/clientTreeShakePlugin.test.native.js +18 -0
  123. package/dist/esm/vite/plugins/clientTreeShakePlugin.test.native.js.map +1 -1
  124. package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs +4 -9
  125. package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs.map +1 -1
  126. package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js +4 -9
  127. package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
  128. package/dist/esm/vite/plugins/virtualEntryPlugin.mjs +7 -6
  129. package/dist/esm/vite/plugins/virtualEntryPlugin.mjs.map +1 -1
  130. package/dist/esm/vite/plugins/virtualEntryPlugin.native.js +9 -6
  131. package/dist/esm/vite/plugins/virtualEntryPlugin.native.js.map +1 -1
  132. package/dist/esm/vite/plugins/workerdDevPlugin.mjs +0 -13
  133. package/dist/esm/vite/plugins/workerdDevPlugin.mjs.map +1 -1
  134. package/dist/esm/vite/plugins/workerdDevPlugin.native.js +26 -58
  135. package/dist/esm/vite/plugins/workerdDevPlugin.native.js.map +1 -1
  136. package/package.json +9 -9
  137. package/src/cli/build.ts +41 -32
  138. package/src/cli/buildPage.ts +18 -22
  139. package/src/cli/buildPageWorker.ts +1 -0
  140. package/src/cli/serverRouteModules.ts +33 -0
  141. package/src/cli/workerPool.ts +1 -0
  142. package/src/createApp.tsx +5 -1
  143. package/src/headless-server.ts +0 -1
  144. package/src/headless.tsx +5 -0
  145. package/src/router/diagnoseRouteLoadFailure.ts +3 -1
  146. package/src/router/useViteRoutes.tsx +9 -1
  147. package/src/server/oneServe.ts +40 -16
  148. package/src/server/routeExportsFromEntry.ts +59 -0
  149. package/src/server/workerHandler.test.ts +21 -16
  150. package/src/server/workerHandler.ts +25 -7
  151. package/src/views/ScrollBehavior.tsx +5 -1
  152. package/src/vite/cloudflareWranglerConfig.ts +8 -4
  153. package/src/vite/one.ts +8 -3
  154. package/src/vite/plugins/clientTreeShakePlugin.test.ts +28 -0
  155. package/src/vite/plugins/clientTreeShakePlugin.ts +17 -4
  156. package/src/vite/plugins/fileSystemRouterPlugin.tsx +6 -12
  157. package/src/vite/plugins/virtualEntryPlugin.ts +10 -8
  158. package/src/vite/plugins/workerdDevPlugin.ts +6 -16
  159. package/types/cli/build.d.ts.map +1 -1
  160. package/types/cli/buildPage.d.ts +1 -1
  161. package/types/cli/buildPage.d.ts.map +1 -1
  162. package/types/cli/serverRouteModules.d.ts +2 -0
  163. package/types/cli/serverRouteModules.d.ts.map +1 -0
  164. package/types/cli/workerPool.d.ts +1 -0
  165. package/types/cli/workerPool.d.ts.map +1 -1
  166. package/types/createApp.d.ts.map +1 -1
  167. package/types/headless-server.d.ts +0 -1
  168. package/types/headless-server.d.ts.map +1 -1
  169. package/types/headless.d.ts.map +1 -1
  170. package/types/router/diagnoseRouteLoadFailure.d.ts.map +1 -1
  171. package/types/router/useViteRoutes.d.ts.map +1 -1
  172. package/types/server/oneServe.d.ts +1 -1
  173. package/types/server/oneServe.d.ts.map +1 -1
  174. package/types/server/routeExportsFromEntry.d.ts +29 -0
  175. package/types/server/routeExportsFromEntry.d.ts.map +1 -0
  176. package/types/server/workerHandler.d.ts +1 -1
  177. package/types/server/workerHandler.d.ts.map +1 -1
  178. package/types/views/ScrollBehavior.d.ts.map +1 -1
  179. package/types/vite/cloudflareWranglerConfig.d.ts.map +1 -1
  180. package/types/vite/one.d.ts.map +1 -1
  181. package/types/vite/plugins/clientTreeShakePlugin.d.ts +2 -1
  182. package/types/vite/plugins/clientTreeShakePlugin.d.ts.map +1 -1
  183. package/types/vite/plugins/fileSystemRouterPlugin.d.ts.map +1 -1
  184. package/types/vite/plugins/virtualEntryPlugin.d.ts.map +1 -1
  185. package/types/vite/plugins/workerdDevPlugin.d.ts.map +1 -1
  186. package/dist/cjs/vite/replaceLoader.cjs +0 -50
  187. package/dist/cjs/vite/replaceLoader.native.js +0 -54
  188. package/dist/cjs/vite/replaceLoader.native.js.map +0 -1
  189. package/dist/esm/vite/replaceLoader.mjs +0 -25
  190. package/dist/esm/vite/replaceLoader.mjs.map +0 -1
  191. package/dist/esm/vite/replaceLoader.native.js +0 -26
  192. package/dist/esm/vite/replaceLoader.native.js.map +0 -1
  193. package/src/vite/replaceLoader.ts +0 -39
  194. package/types/vite/replaceLoader.d.ts +0 -5
  195. package/types/vite/replaceLoader.d.ts.map +0 -1
package/src/headless.tsx CHANGED
@@ -4,6 +4,7 @@ import type { OneLinkingConfig } from './link/getLinking'
4
4
  import { Root } from './Root'
5
5
  import { resolveClientLoader } from './clientLoaderResolver'
6
6
  import { render } from './render'
7
+ import { handleSkewError, isChunkLoadError } from './utils/dynamicImport'
7
8
  import { findRootLayout } from './utils/findRootLayout'
8
9
  import type { One } from './vite/types'
9
10
 
@@ -51,5 +52,9 @@ export function createApp(options: CreateHeadlessAppProps) {
51
52
  })
52
53
  .catch((err) => {
53
54
  console.error(`[one/headless] Error during initialization:`, err)
55
+ if (isChunkLoadError(err)) {
56
+ handleSkewError()
57
+ }
54
58
  })
55
59
  }
60
+
@@ -28,7 +28,9 @@ function collectImports(source: string, base: string): string[] {
28
28
  out.push(new URL(spec, base).href)
29
29
  } catch {}
30
30
  }
31
- for (const m of source.matchAll(/(?:^|[\s;}])(?:import|export)[^'"]{0,300}?from\s*["']([^"']+)["']/g))
31
+ for (const m of source.matchAll(
32
+ /(?:^|[\s;}])(?:import|export)[^'"]{0,300}?from\s*["']([^"']+)["']/g
33
+ ))
32
34
  add(m[1])
33
35
  for (const m of source.matchAll(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g)) add(m[1])
34
36
  for (const m of source.matchAll(/(?:^|[\s;}])import\s*["']([^"']+)["']/g)) add(m[1])
@@ -1,5 +1,6 @@
1
1
  import type { GlobbedRouteImports } from '../types'
2
2
  import type { One } from '../vite/types'
3
+ import { handleSkewError, isChunkLoadError } from '../utils/dynamicImport'
3
4
  import { diagnoseRouteLoadFailure } from './diagnoseRouteLoadFailure'
4
5
  import { hmrImport } from './hmrImport'
5
6
 
@@ -146,7 +147,11 @@ export async function preloadRouteModules(href: string): Promise<void> {
146
147
  .then((mod: any) => {
147
148
  preloadedModules[key] = mod
148
149
  })
149
- .catch(() => {})
150
+ .catch((err: any) => {
151
+ if (isChunkLoadError(err)) {
152
+ handleSkewError()
153
+ }
154
+ })
150
155
  )
151
156
  }
152
157
  }
@@ -284,6 +289,9 @@ export function globbedRoutesToRouteContext(
284
289
  })
285
290
  .catch((err) => {
286
291
  console.error(`Error loading route`, id, err, new Error().stack)
292
+ if (isChunkLoadError(err)) {
293
+ handleSkewError()
294
+ }
287
295
  // rendering an empty component keeps one bad route from taking down the
288
296
  // app, but it also makes the failure invisible: nothing inside the route
289
297
  // mounts and the only trace is the line above. in dev, hand it to the
@@ -16,10 +16,12 @@ import {
16
16
  } from '../createHandleRequest'
17
17
  import type { RenderAppProps } from '../types'
18
18
  import { getPathFromLoaderPath } from '../utils/cleanUrl'
19
+ import { getRouterRootFromOneOptions } from '../utils/getRouterRootFromOneOptions'
19
20
  import { toAbsoluteUrl } from '../utils/toAbsolute'
20
21
  import { toServerOutputPath } from '../utils/toServerOutputPath'
21
22
  import type { One } from '../vite/types'
22
23
  import type { RouteInfoCompiled } from './createRoutesManifest'
24
+ import { getRouteExportsFromEntry } from './routeExportsFromEntry'
23
25
  import { setSSRLoaderData } from './ssrLoaderData'
24
26
  import { getFetchStaticHtml } from './staticHtmlFetcher'
25
27
 
@@ -74,8 +76,8 @@ async function readStaticHtml(htmlPath: string, outDir = 'dist'): Promise<string
74
76
  * Modules are loaded on-demand when a route is matched, not all upfront.
75
77
  */
76
78
  type LazyRoutes = {
79
+ pages?: Record<string, () => Promise<any>>
77
80
  serverEntry: () => Promise<{ default: { render: (props: any) => any } }>
78
- pages: Record<string, () => Promise<any>>
79
81
  api: Record<string, () => Promise<any>>
80
82
  middlewares: Record<string, () => Promise<any>>
81
83
  }
@@ -150,6 +152,7 @@ export async function oneServe(
150
152
  }
151
153
 
152
154
  const apiCJS = oneOptions.build?.api?.outputFormat === 'cjs'
155
+ const routerRoot = getRouterRootFromOneOptions(oneOptions)
153
156
 
154
157
  // pre-computed constants to avoid per-request overhead
155
158
  const useStreaming = !process.env.ONE_BUFFERED_SSR
@@ -169,6 +172,26 @@ export async function oneServe(
169
172
  { promise: Promise<any>; expires: number }
170
173
  >()
171
174
 
175
+ // route modules are resolved through the built server entry, see
176
+ // routeExportsFromEntry
177
+ async function importRouteModule(
178
+ lazyKey: string | undefined,
179
+ serverPath: string | undefined
180
+ ): Promise<any> {
181
+ if (lazyKey && options?.lazyRoutes?.pages?.[lazyKey]) {
182
+ return await options.lazyRoutes.pages[lazyKey]()
183
+ }
184
+ if (lazyKey) {
185
+ const entry = await loadServerEntry()
186
+ const exported = await getRouteExportsFromEntry(entry, routerRoot, lazyKey)
187
+ if (exported) return exported
188
+ }
189
+ // layouts and middlewares outside the router root still resolve by path
190
+ return await import(
191
+ toAbsoluteUrl(toServerOutputPath(serverPath || lazyKey || '', outDir))
192
+ )
193
+ }
194
+
172
195
  // resolve a route module's loader - sync on cache hit, async on cold start
173
196
  function resolveLoaderSync(
174
197
  serverPath: string | undefined,
@@ -180,18 +203,11 @@ export async function oneServe(
180
203
 
181
204
  // cold path - async import
182
205
  return (async () => {
183
- const pathToResolve = serverPath || lazyKey || ''
184
- const resolvedPath = toServerOutputPath(pathToResolve, outDir)
185
-
186
206
  let routeExported: any
187
207
  if (moduleImportCache.has(cacheKey)) {
188
208
  routeExported = moduleImportCache.get(cacheKey)
189
209
  } else {
190
- routeExported = lazyKey
191
- ? options?.lazyRoutes?.pages?.[lazyKey]
192
- ? await options.lazyRoutes.pages[lazyKey]()
193
- : await import(toAbsoluteUrl(resolvedPath))
194
- : await import(toAbsoluteUrl(serverPath!))
210
+ routeExported = await importRouteModule(lazyKey, serverPath)
195
211
  setBounded(moduleImportCache, cacheKey, routeExported, MODULE_CACHE_MAX)
196
212
  }
197
213
 
@@ -286,18 +302,26 @@ export async function oneServe(
286
302
  let render: ((props: RenderAppProps) => any) | null = null
287
303
  let renderStream: ((props: RenderAppProps) => Promise<ReadableStream>) | null = null
288
304
  let renderLoading: Promise<void> | null = null
305
+ let entryLoading: Promise<any> | null = null
289
306
 
290
- function ensureRenderLoaded(): void | Promise<void> {
291
- if (render) return // sync!
292
- if (renderLoading) return renderLoading
293
- renderLoading = (async () => {
294
- const entry = options?.lazyRoutes?.serverEntry
295
- ? await options.lazyRoutes.serverEntry()
296
- : await import(
307
+ function loadServerEntry(): Promise<any> {
308
+ if (!entryLoading) {
309
+ entryLoading = options?.lazyRoutes?.serverEntry
310
+ ? options.lazyRoutes.serverEntry()
311
+ : import(
297
312
  toAbsoluteUrl(
298
313
  `${serverOptions.root}/${outDir}/server/_virtual_one-entry.${typeof oneOptions.build?.server === 'object' && oneOptions.build.server.outputFormat === 'cjs' ? 'c' : ''}js`
299
314
  )
300
315
  )
316
+ }
317
+ return entryLoading
318
+ }
319
+
320
+ function ensureRenderLoaded(): void | Promise<void> {
321
+ if (render) return // sync!
322
+ if (renderLoading) return renderLoading
323
+ renderLoading = (async () => {
324
+ const entry = await loadServerEntry()
301
325
  render = entry.default.render as (props: RenderAppProps) => any
302
326
  renderStream = entry.default.renderStream as
303
327
  | ((props: RenderAppProps) => Promise<ReadableStream>)
@@ -0,0 +1,59 @@
1
+ /**
2
+ * route modules have to be read through the built server entry's own route map,
3
+ * never by importing whichever output chunk happens to contain them.
4
+ *
5
+ * a bundler makes no promise that a chunk re-exports a module's bindings under
6
+ * their source names. when a route is both dynamically imported by the entry and
7
+ * statically imported by another route (docs pages mirrored under a second url,
8
+ * say), or when the app sets `output.strictExecutionOrder`, rolldown emits the
9
+ * chunk as
10
+ *
11
+ * export { loader as a, init__slug_ as i, _slug__exports as n, generateStaticParams as r }
12
+ *
13
+ * so every read by source name comes back undefined: the build then reports a
14
+ * route that exports generateStaticParams as missing it, and any loader on that
15
+ * route silently stops running.
16
+ *
17
+ * the entry's `import.meta.glob` map is written by the bundler itself, which
18
+ * rewrites each entry to whatever preserves the real namespace
19
+ * (`import(chunk).then((n) => (n.i(), n.n))` for the wrapped case above). so it
20
+ * resolves correctly no matter how the chunking lands.
21
+ */
22
+
23
+ /** the key the entry's globs use: `./posts/[slug]+ssg.tsx` -> `/app/posts/[slug]+ssg.tsx` */
24
+ export function routeEntryKey(routerRoot: string, routeFile: string) {
25
+ return `/${routerRoot}/${routeFile.replace(/^\.?\//, '')}`
26
+ }
27
+
28
+ /**
29
+ * resolve one route's exports out of an already-loaded server entry namespace.
30
+ * returns null when the entry has no such route.
31
+ */
32
+ export async function getRouteExportsFromEntry(
33
+ entry: any,
34
+ routerRoot: string,
35
+ routeFile: string
36
+ ): Promise<Record<string, any> | null> {
37
+ const key = routeEntryKey(routerRoot, routeFile)
38
+
39
+ // a route that declares spa by filename or parent directory is stubbed out of
40
+ // the route map, so the server never holds its page component. every stubbed
41
+ // route has an entry in this parallel per-export map whether or not it
42
+ // actually exports a loader, which is what tells a stub from a real route. a
43
+ // route that is spa only because of `web.defaultRenderMode` is never stubbed.
44
+ const spaExports = entry?.oneServerSpaRouteExports
45
+ if (spaExports?.loader && key in spaExports.loader) {
46
+ const [loader, loaderCache, generateStaticParams, sitemap] = await Promise.all([
47
+ spaExports.loader[key](),
48
+ spaExports.loaderCache?.[key]?.(),
49
+ spaExports.generateStaticParams?.[key]?.(),
50
+ spaExports.sitemap?.[key]?.(),
51
+ ])
52
+ return { loader, loaderCache, generateStaticParams, sitemap }
53
+ }
54
+
55
+ // some output formats nest the entry default one level deeper
56
+ const app = entry?.default?.options ? entry.default : entry?.default?.default
57
+ const importRoute = app?.options?.routes?.[key]
58
+ return importRoute ? await importRoute() : null
59
+ }
@@ -2,8 +2,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
2
2
  import type { One } from '../vite/types'
3
3
  import { createWorkerHandler, type LazyRoutes } from './workerHandler'
4
4
 
5
+ // route files are router-root relative, the way createRoutesManifest emits them
5
6
  const pageRoute = {
6
- file: 'app/some-page.tsx',
7
+ file: './some-page.tsx',
7
8
  page: '/some-page',
8
9
  namedRegex: '^/some-page(?:/)?$',
9
10
  urlPath: '/some-page',
@@ -14,7 +15,7 @@ const pageRoute = {
14
15
  } satisfies One.BuildInfo['manifest']['pageRoutes'][number]
15
16
 
16
17
  const apiRoute = {
17
- file: 'app/api/status+api.ts',
18
+ file: './api/status+api.ts',
18
19
  page: '/api/status',
19
20
  namedRegex: '^/api/status(?:/)?$',
20
21
  urlPath: '/api/status',
@@ -57,15 +58,21 @@ const buildInfo = {
57
58
  loaders: {},
58
59
  } satisfies One.BuildInfo
59
60
 
60
- const lazyRoutes = {
61
- serverEntry: async () => ({
61
+ // route modules are reached through the server entry's own route map, keyed by
62
+ // the router-root prefixed path its `import.meta.glob` produces
63
+ const routeKey = `/app/${pageRoute.file.slice(2)}`
64
+
65
+ const makeServerEntry =
66
+ (importPage: () => Promise<any> = async () => ({})) =>
67
+ async () => ({
62
68
  default: {
63
69
  render: () => '<html></html>',
70
+ options: { routes: { [routeKey]: importPage } },
64
71
  },
65
- }),
66
- pages: {
67
- [pageRoute.file]: async () => ({}),
68
- },
72
+ })
73
+
74
+ const lazyRoutes = {
75
+ serverEntry: makeServerEntry(),
69
76
  api: {
70
77
  [apiRoute.page]: async () => ({
71
78
  HEAD: () => new Response(null, { status: 204 }),
@@ -77,7 +84,7 @@ const lazyRoutes = {
77
84
  function createHandler() {
78
85
  vi.stubEnv('ONE_BUFFERED_SSR', '1')
79
86
  return createWorkerHandler({
80
- oneOptions: { web: { defaultRenderMode: 'ssr' } },
87
+ oneOptions: { router: { root: 'app' }, web: { defaultRenderMode: 'ssr' } },
81
88
  buildInfo,
82
89
  lazyRoutes,
83
90
  }).handleRequest
@@ -127,17 +134,15 @@ describe('createWorkerHandler', () => {
127
134
  vi.stubEnv('ONE_BUFFERED_SSR', '1')
128
135
  let imports = 0
129
136
  const handleRequest = createWorkerHandler({
130
- oneOptions: { web: { defaultRenderMode: 'ssr' } },
137
+ oneOptions: { router: { root: 'app' }, web: { defaultRenderMode: 'ssr' } },
131
138
  buildInfo,
132
139
  disableModuleCache: true,
133
140
  lazyRoutes: {
134
141
  ...lazyRoutes,
135
- pages: {
136
- [pageRoute.file]: async () => {
137
- imports += 1
138
- return {}
139
- },
140
- },
142
+ serverEntry: makeServerEntry(async () => {
143
+ imports += 1
144
+ return {}
145
+ }),
141
146
  },
142
147
  }).handleRequest
143
148
 
@@ -15,21 +15,23 @@ import {
15
15
  } from '../createHandleRequest'
16
16
  import type { RenderAppProps } from '../types'
17
17
  import { getPathFromLoaderPath } from '../utils/cleanUrl'
18
+ import { getRouterRootFromOneOptions } from '../utils/getRouterRootFromOneOptions'
18
19
  import { isResponse } from '../utils/isResponse'
19
20
  import { resolveResponse } from '../vite/resolveResponse'
20
21
  import type { One } from '../vite/types'
21
22
  import type { RouteInfoCompiled } from './createRoutesManifest'
23
+ import { getRouteExportsFromEntry } from './routeExportsFromEntry'
22
24
  import { setSSRLoaderData } from './ssrLoaderData'
23
25
  import { getFetchStaticHtml } from './staticHtmlFetcher'
24
26
 
25
27
  export type LazyRoutes = {
28
+ pages?: Record<string, () => Promise<any>>
26
29
  serverEntry: () => Promise<{
27
30
  default: {
28
31
  render: (props: any) => any
29
32
  renderStream?: (props: any) => Promise<ReadableStream>
30
33
  }
31
34
  }>
32
- pages: Record<string, () => Promise<any>>
33
35
  api: Record<string, () => Promise<any>>
34
36
  middlewares: Record<string, () => Promise<any>>
35
37
  }
@@ -46,6 +48,7 @@ type WorkerHandlerOptions = {
46
48
  export function createWorkerHandler(options: WorkerHandlerOptions) {
47
49
  const { oneOptions } = options
48
50
  const disableModuleCache = !!options.disableModuleCache
51
+ const routerRoot = getRouterRootFromOneOptions(oneOptions)
49
52
 
50
53
  // mutable state for route swapping
51
54
  let currentLazyRoutes = options.lazyRoutes
@@ -95,13 +98,21 @@ export function createWorkerHandler(options: WorkerHandlerOptions) {
95
98
  let renderStream: ((props: RenderAppProps) => Promise<ReadableStream>) | null = null
96
99
  let renderLoading: Promise<void> | null = null
97
100
  let renderGeneration = 0
101
+ let entryLoading: Promise<any> | null = null
102
+
103
+ // route modules are resolved through the built server entry, see
104
+ // routeExportsFromEntry
105
+ function loadServerEntry(): Promise<any> {
106
+ if (!entryLoading) entryLoading = currentLazyRoutes.serverEntry()
107
+ return entryLoading
108
+ }
98
109
 
99
110
  function ensureRenderLoaded(): void | Promise<void> {
100
111
  if (render) return
101
112
  if (renderLoading) return renderLoading
102
113
  const gen = ++renderGeneration
103
114
  renderLoading = (async () => {
104
- const entry = await currentLazyRoutes.serverEntry()
115
+ const entry = await loadServerEntry()
105
116
  // if updateRoutes was called while we were loading, discard stale entry
106
117
  if (gen !== renderGeneration) return
107
118
  render = entry.default.render as any
@@ -150,13 +161,19 @@ export function createWorkerHandler(options: WorkerHandlerOptions) {
150
161
  let routeExported: any
151
162
  if (!disableModuleCache && moduleImportCache.has(cacheKey)) {
152
163
  routeExported = moduleImportCache.get(cacheKey)
153
- } else if (lazyKey && currentLazyRoutes.pages[lazyKey]) {
154
- routeExported = await currentLazyRoutes.pages[lazyKey]()
164
+ } else if (currentLazyRoutes.pages?.[cacheKey]) {
165
+ routeExported = await currentLazyRoutes.pages[cacheKey]()
155
166
  if (!disableModuleCache) moduleImportCache.set(cacheKey, routeExported)
156
167
  } else {
157
- console.warn(`[one/worker] no lazy route for ${cacheKey}`)
158
- if (!disableModuleCache) loaderCache.set(cacheKey, null)
159
- return null
168
+ routeExported = lazyKey
169
+ ? await getRouteExportsFromEntry(await loadServerEntry(), routerRoot, lazyKey)
170
+ : null
171
+ if (!routeExported) {
172
+ console.warn(`[one/worker] no lazy route for ${cacheKey}`)
173
+ if (!disableModuleCache) loaderCache.set(cacheKey, null)
174
+ return null
175
+ }
176
+ if (!disableModuleCache) moduleImportCache.set(cacheKey, routeExported)
160
177
  }
161
178
 
162
179
  const loader = routeExported?.loader || null
@@ -797,6 +814,7 @@ export function createWorkerHandler(options: WorkerHandlerOptions) {
797
814
  render = null
798
815
  renderStream = null
799
816
  renderLoading = null
817
+ entryLoading = null
800
818
  }
801
819
 
802
820
  return { handleRequest, updateRoutes }
@@ -1,6 +1,10 @@
1
1
  import { useEffect } from 'react'
2
2
  import { setLastAction } from '../router/lastAction'
3
- import { routeInfo, subscribeToLoadingState, subscribeToRootState } from '../router/router'
3
+ import {
4
+ routeInfo,
5
+ subscribeToLoadingState,
6
+ subscribeToRootState,
7
+ } from '../router/router'
4
8
 
5
9
  const KEY = 'one-sr'
6
10
  const GROUP_KEY = 'one-sr-groups'
@@ -122,9 +122,10 @@ export function hasUserWranglerConfig(root: string): boolean {
122
122
  return false
123
123
  }
124
124
 
125
- function parseUserWranglerFile(
126
- configPath: string
127
- ): { path: string; config: Record<string, JsonValue> } {
125
+ function parseUserWranglerFile(configPath: string): {
126
+ path: string
127
+ config: Record<string, JsonValue>
128
+ } {
128
129
  const contents = readFileSync(configPath, 'utf-8')
129
130
  let parsed: unknown
130
131
  try {
@@ -242,7 +243,10 @@ export function isExperimentalWorkerDevEnabled(): boolean {
242
243
 
243
244
  export function shouldEnableWorkerdDev(deploy: unknown, root: string): boolean {
244
245
  if (!isExperimentalWorkerDevEnabled()) return false
245
- const target = typeof deploy === 'string' ? deploy : (deploy as { target?: string } | undefined)?.target
246
+ const target =
247
+ typeof deploy === 'string'
248
+ ? deploy
249
+ : (deploy as { target?: string } | undefined)?.target
246
250
  if (target === 'cloudflare') return true
247
251
  return hasUserWranglerConfig(root)
248
252
  }
package/src/vite/one.ts CHANGED
@@ -34,7 +34,10 @@ import { SSRCSSPlugin } from './plugins/SSRCSSPlugin'
34
34
  import { virtualEntryId } from './plugins/virtualEntryConstants'
35
35
  import { createVirtualEntry } from './plugins/virtualEntryPlugin'
36
36
  import { environmentGuardPlugin } from './plugins/environmentGuardPlugin'
37
- import { createWorkerdDevPlugins, shouldEnableWorkerdDev } from './plugins/workerdDevPlugin'
37
+ import {
38
+ createWorkerdDevPlugins,
39
+ shouldEnableWorkerdDev,
40
+ } from './plugins/workerdDevPlugin'
38
41
  import type { One } from './types'
39
42
 
40
43
  type MetroOptions = MetroPluginOptions
@@ -837,11 +840,13 @@ export function one(options: One.PluginOptions = {}): PluginOption {
837
840
  ] satisfies Plugin[]
838
841
 
839
842
  // TODO move to single config and through environments
840
- const nativeWebDevAndProdPlugsin: Plugin[] = [clientTreeShakePlugin()]
843
+ const nativeWebDevAndProdPlugsin: Plugin[] = [clientTreeShakePlugin({ routerRoot })]
841
844
 
842
845
  // TODO make this passed into vxrn through real API
843
846
  if (!nativeDisabled) {
844
- globalThis.__vxrnAddNativePlugins = [clientTreeShakePlugin({ runtime: 'rolldown' })]
847
+ globalThis.__vxrnAddNativePlugins = [
848
+ clientTreeShakePlugin({ runtime: 'rolldown', routerRoot }),
849
+ ]
845
850
  }
846
851
  globalThis.__vxrnAddWebPluginsProd = devAndProdPlugins
847
852
 
@@ -148,6 +148,34 @@ export default function Layout() {
148
148
  expect(result!.code).not.toContain('__vxrn__loader__')
149
149
  })
150
150
 
151
+ it('should strip a custom router root from the loader stub routeId', async () => {
152
+ const code = `
153
+ import { serverOnlyModule } from 'server-only-pkg'
154
+ import { useLoader } from 'one'
155
+
156
+ export const loader = async () => {
157
+ return serverOnlyModule()
158
+ }
159
+
160
+ export default function Page() {
161
+ const data = useLoader(loader)
162
+ return data
163
+ }
164
+ `
165
+ const result = await transformTreeShakeClient(
166
+ code,
167
+ '/project/app-sootsim/(marketing)/changelog/index+ssg.tsx',
168
+ '/project',
169
+ 'app-sootsim'
170
+ )
171
+ expect(result).toBeDefined()
172
+ // buildPage looks the stub up by the route contextKey, which is relative
173
+ // to the configured router root rather than to a hardcoded app/
174
+ expect(result!.code).toContain(
175
+ 'export function loader() {return "./(marketing)/changelog/index+ssg.tsx"}'
176
+ )
177
+ })
178
+
151
179
  it('should embed routeId with render mode suffix in loader stub', async () => {
152
180
  const code = `
153
181
  import { serverOnlyModule } from 'server-only-pkg'
@@ -54,8 +54,13 @@ function restoreTypeImports(ast: t.File, typeImports: t.ImportDeclaration[]) {
54
54
  export const clientTreeShakePlugin = (opts?: {
55
55
  // 'rolldown' when used in the native rolldown DevEngine (no vite environment context)
56
56
  runtime?: 'vite' | 'rolldown'
57
+ // the configured router root (One.PluginOptions.router.root), relative to the
58
+ // project root. the loader stub's routeId is built off it, so it must be the
59
+ // real one or buildPage cannot match the stub back to the route.
60
+ routerRoot?: string
57
61
  }): Plugin => {
58
62
  const runtime = opts?.runtime ?? 'vite'
63
+ const routerRoot = opts?.routerRoot ?? 'app'
59
64
 
60
65
  return {
61
66
  name: 'one-client-tree-shake',
@@ -81,7 +86,7 @@ export const clientTreeShakePlugin = (opts?: {
81
86
  return
82
87
  }
83
88
 
84
- const out = await transformTreeShakeClient(code, id, process.cwd())
89
+ const out = await transformTreeShakeClient(code, id, process.cwd(), routerRoot)
85
90
 
86
91
  return out
87
92
  },
@@ -89,7 +94,12 @@ export const clientTreeShakePlugin = (opts?: {
89
94
  } satisfies Plugin
90
95
  }
91
96
 
92
- export async function transformTreeShakeClient(code: string, id: string, root?: string) {
97
+ export async function transformTreeShakeClient(
98
+ code: string,
99
+ id: string,
100
+ root?: string,
101
+ routerRoot = 'app'
102
+ ) {
93
103
  if (!/generateStaticParams|loader/.test(code)) {
94
104
  return
95
105
  }
@@ -199,10 +209,13 @@ export async function transformTreeShakeClient(code: string, id: string, root?:
199
209
  .map((key) => {
200
210
  if (key === 'loader') {
201
211
  if (root) {
202
- // compute routeId relative to the app/ directory to match route contextKey format
212
+ // compute routeId relative to the router root so it matches the
213
+ // route contextKey format buildPage looks the stub up by.
203
214
  // contextKeys are like "./_layout.tsx", "./matches-test/page1+ssg.tsx"
204
215
  const fromRoot = relative(root, id).replace(/\\/g, '/')
205
- const routeId = './' + fromRoot.replace(/^app\//, '')
216
+ const prefix = routerRoot.replace(/^\.\//, '').replace(/\/$/, '') + '/'
217
+ const routeId =
218
+ './' + (fromRoot.startsWith(prefix) ? fromRoot.slice(prefix.length) : fromRoot)
206
219
  return makeLoaderRouteIdStub(routeId)
207
220
  }
208
221
  return EMPTY_LOADER_STRING
@@ -18,7 +18,6 @@ import { promiseWithResolvers } from '../../utils/promiseWithResolvers'
18
18
  import type { RouteIndex } from '../../utils/routeIndex'
19
19
  import { isRouteFileWatchEvent } from '../../utils/routeFileWatch'
20
20
  import { trackLoaderDependencies } from '../../utils/trackLoaderDependencies'
21
- import { replaceLoader } from '../../vite/replaceLoader'
22
21
  import type { One, RouteInfo } from '../../vite/types'
23
22
  import { setServerContext } from '../one-server-only'
24
23
  import { virtalEntryIdClient, virtualEntryId } from './virtualEntryConstants'
@@ -561,14 +560,6 @@ export function createFileSystemRouterPlugin(
561
560
  }
562
561
  }
563
562
 
564
- if (loaderData) {
565
- // add loader back in!
566
- transformedJS = replaceLoader({
567
- code: transformedJS,
568
- loaderData,
569
- })
570
- }
571
-
572
563
  const platform = url.searchParams.get('platform')
573
564
 
574
565
  if (platform === 'ios' || platform === 'android' || platform === 'native') {
@@ -582,10 +573,13 @@ export function createFileSystemRouterPlugin(
582
573
  )
583
574
  }
584
575
 
585
- // [3] Just use a simple function to return the loader data for now.
586
- const nativeTransformedJS = `exports.loader = () => (${JSON.stringify(loaderData)});`
576
+ return `exports.loader = () => (${JSON.stringify(loaderData)});`
577
+ }
587
578
 
588
- return nativeTransformedJS
579
+ if (loaderData) {
580
+ // the client only reads loader() off this module, so serve the data
581
+ // alone instead of the whole route module with its stub rewritten
582
+ return `export function loader(){return ${JSON.stringify(loaderData)}}`
589
583
  }
590
584
 
591
585
  return transformedJS
@@ -211,12 +211,12 @@ export function createVirtualEntry(options: {
211
211
  const serverSpaBuildGlobs = serverSpaRouteFiles.map(
212
212
  (file) => `/${options.root}/${file}`
213
213
  )
214
+ const serverSpaExportGlob = (name: string) =>
215
+ ` ${name}: import.meta.glob(${JSON.stringify(serverSpaBuildGlobs)}, { exhaustive: true, import: '${name}', query: '?one-spa-build' })`
214
216
  const serverSpaBuildRoutes = serverSpaBuildGlobs.length
215
- ? `{
216
- loader: import.meta.glob(${JSON.stringify(serverSpaBuildGlobs)}, { exhaustive: true, import: 'loader', query: '?one-spa-build' }),
217
- generateStaticParams: import.meta.glob(${JSON.stringify(serverSpaBuildGlobs)}, { exhaustive: true, import: 'generateStaticParams', query: '?one-spa-build' }),
218
- sitemap: import.meta.glob(${JSON.stringify(serverSpaBuildGlobs)}, { exhaustive: true, import: 'sitemap', query: '?one-spa-build' })
219
- }`
217
+ ? `{\n${['loader', 'loaderCache', 'generateStaticParams', 'sitemap']
218
+ .map(serverSpaExportGlob)
219
+ .join(',\n')}\n}`
220
220
  : '{}'
221
221
  const webRouteGlobs = [
222
222
  ...routeGlobs,
@@ -301,9 +301,11 @@ if (typeof window !== 'undefined') {
301
301
  window.__oneRegisterPreloadedRoute = registerPreloadedRoute
302
302
  }
303
303
 
304
- // build-only spa exports generate route artifacts without exposing the page
305
- // component to the runtime server route map.
306
- export const oneBuildOnlySpaRoutes = ${serverSpaBuildRoutes}
304
+ // a route that declares spa by filename or parent directory is stubbed out of
305
+ // the route map below so the server never holds its page component. its
306
+ // server-side exports come from here instead, one glob per export so the page
307
+ // never comes with them.
308
+ export const oneServerSpaRouteExports = ${serverSpaBuildRoutes}
307
309
 
308
310
  // globbing ${JSON.stringify(webRouteGlobs)}
309
311
  export default createApp({