one 1.21.7 → 1.21.8
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.
- package/dist/cjs/cli/dev.native.js.map +1 -1
- package/dist/cjs/cli.cjs +0 -3
- package/dist/cjs/cli.native.js +0 -3
- package/dist/cjs/cli.native.js.map +1 -1
- package/dist/cjs/image/getImageData.cjs +1 -1
- package/dist/cjs/image/getImageData.native.js +1 -1
- package/dist/cjs/image/getImageData.native.js.map +1 -1
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/layouts/Stack.cjs +5 -5
- package/dist/cjs/layouts/Stack.native.js +5 -5
- package/dist/cjs/layouts/Stack.native.js.map +1 -1
- package/dist/cjs/layouts/stack-utils/StackScreen.cjs +3 -0
- package/dist/cjs/layouts/stack-utils/StackScreen.native.js +3 -0
- package/dist/cjs/layouts/stack-utils/StackScreen.native.js.map +1 -1
- package/dist/cjs/layouts/stack-utils/StackToolbar.cjs +419 -0
- package/dist/cjs/layouts/stack-utils/StackToolbar.native.js +453 -0
- package/dist/cjs/layouts/stack-utils/StackToolbar.native.js.map +1 -0
- package/dist/cjs/layouts/stack-utils/StackToolbar.types.cjs +18 -0
- package/dist/cjs/layouts/stack-utils/StackToolbar.types.native.js +21 -0
- package/dist/cjs/layouts/stack-utils/StackToolbar.types.native.js.map +1 -0
- package/dist/cjs/layouts/stack-utils/__tests__/StackToolbar.runtime.test.cjs +63 -0
- package/dist/cjs/layouts/stack-utils/__tests__/StackToolbar.runtime.test.native.js +73 -0
- package/dist/cjs/layouts/stack-utils/__tests__/StackToolbar.runtime.test.native.js.map +1 -0
- package/dist/cjs/layouts/stack-utils/__tests__/composition.test.cjs +205 -0
- package/dist/cjs/layouts/stack-utils/__tests__/composition.test.native.js +220 -0
- package/dist/cjs/layouts/stack-utils/__tests__/composition.test.native.js.map +1 -1
- package/dist/cjs/layouts/stack-utils/index.cjs +2 -0
- package/dist/cjs/layouts/stack-utils/index.native.js +2 -0
- package/dist/cjs/layouts/stack-utils/index.native.js.map +1 -1
- package/dist/cjs/layouts/withLayoutContext.cjs +39 -8
- package/dist/cjs/layouts/withLayoutContext.native.js +43 -8
- package/dist/cjs/layouts/withLayoutContext.native.js.map +1 -1
- package/dist/cjs/layouts/withLayoutContext.test.cjs +96 -0
- package/dist/cjs/layouts/withLayoutContext.test.native.js +100 -0
- package/dist/cjs/layouts/withLayoutContext.test.native.js.map +1 -0
- package/dist/cjs/router/Route.cjs +3 -0
- package/dist/cjs/router/Route.native.js +3 -0
- package/dist/cjs/router/Route.native.js.map +1 -1
- package/dist/cjs/router/protectedRoutes.test.cjs +22 -0
- package/dist/cjs/router/protectedRoutes.test.native.js +25 -0
- package/dist/cjs/router/protectedRoutes.test.native.js.map +1 -0
- package/dist/cjs/router/router.cjs +26 -8
- package/dist/cjs/router/router.native.js +26 -8
- package/dist/cjs/router/router.native.js.map +1 -1
- package/dist/cjs/router/useScreens.cjs +38 -11
- package/dist/cjs/router/useScreens.native.js +39 -11
- package/dist/cjs/router/useScreens.native.js.map +1 -1
- package/dist/cjs/router/useScreens.test.cjs +134 -0
- package/dist/cjs/router/useScreens.test.native.js +163 -0
- package/dist/cjs/router/useScreens.test.native.js.map +1 -0
- package/dist/cjs/views/Navigator.cjs +14 -6
- package/dist/cjs/views/Navigator.native.js +14 -6
- package/dist/cjs/views/Navigator.native.js.map +1 -1
- package/dist/cjs/views/Protected.native.js.map +1 -1
- package/dist/cjs/views/SuspenseFallback.cjs +18 -0
- package/dist/cjs/views/SuspenseFallback.native.js +21 -0
- package/dist/cjs/views/SuspenseFallback.native.js.map +1 -0
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.cjs +47 -7
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js +47 -7
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.test.cjs +15 -6
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.test.native.js +15 -6
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.test.native.js.map +1 -1
- package/dist/esm/cli/dev.mjs.map +1 -1
- package/dist/esm/cli/dev.native.js.map +1 -1
- package/dist/esm/cli.mjs +0 -3
- package/dist/esm/cli.mjs.map +1 -1
- package/dist/esm/cli.native.js +0 -3
- package/dist/esm/cli.native.js.map +1 -1
- package/dist/esm/image/getImageData.mjs +1 -1
- package/dist/esm/image/getImageData.mjs.map +1 -1
- package/dist/esm/image/getImageData.native.js +1 -1
- package/dist/esm/image/getImageData.native.js.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/layouts/Stack.mjs +6 -6
- package/dist/esm/layouts/Stack.mjs.map +1 -1
- package/dist/esm/layouts/Stack.native.js +6 -6
- package/dist/esm/layouts/Stack.native.js.map +1 -1
- package/dist/esm/layouts/stack-utils/StackScreen.mjs +3 -0
- package/dist/esm/layouts/stack-utils/StackScreen.mjs.map +1 -1
- package/dist/esm/layouts/stack-utils/StackScreen.native.js +3 -0
- package/dist/esm/layouts/stack-utils/StackScreen.native.js.map +1 -1
- package/dist/esm/layouts/stack-utils/StackToolbar.mjs +384 -0
- package/dist/esm/layouts/stack-utils/StackToolbar.mjs.map +1 -0
- package/dist/esm/layouts/stack-utils/StackToolbar.native.js +416 -0
- package/dist/esm/layouts/stack-utils/StackToolbar.native.js.map +1 -0
- package/dist/esm/layouts/stack-utils/StackToolbar.types.mjs +2 -0
- package/dist/esm/layouts/stack-utils/StackToolbar.types.mjs.map +1 -0
- package/dist/esm/layouts/stack-utils/StackToolbar.types.native.js +2 -0
- package/dist/esm/layouts/stack-utils/StackToolbar.types.native.js.map +1 -0
- package/dist/esm/layouts/stack-utils/__tests__/StackToolbar.runtime.test.mjs +40 -0
- package/dist/esm/layouts/stack-utils/__tests__/StackToolbar.runtime.test.mjs.map +1 -0
- package/dist/esm/layouts/stack-utils/__tests__/StackToolbar.runtime.test.native.js +47 -0
- package/dist/esm/layouts/stack-utils/__tests__/StackToolbar.runtime.test.native.js.map +1 -0
- package/dist/esm/layouts/stack-utils/__tests__/composition.test.mjs +205 -0
- package/dist/esm/layouts/stack-utils/__tests__/composition.test.mjs.map +1 -1
- package/dist/esm/layouts/stack-utils/__tests__/composition.test.native.js +220 -0
- package/dist/esm/layouts/stack-utils/__tests__/composition.test.native.js.map +1 -1
- package/dist/esm/layouts/stack-utils/index.mjs +2 -1
- package/dist/esm/layouts/stack-utils/index.mjs.map +1 -1
- package/dist/esm/layouts/stack-utils/index.native.js +2 -1
- package/dist/esm/layouts/stack-utils/index.native.js.map +1 -1
- package/dist/esm/layouts/withLayoutContext.mjs +41 -11
- package/dist/esm/layouts/withLayoutContext.mjs.map +1 -1
- package/dist/esm/layouts/withLayoutContext.native.js +45 -11
- package/dist/esm/layouts/withLayoutContext.native.js.map +1 -1
- package/dist/esm/layouts/withLayoutContext.test.mjs +73 -0
- package/dist/esm/layouts/withLayoutContext.test.mjs.map +1 -0
- package/dist/esm/layouts/withLayoutContext.test.native.js +74 -0
- package/dist/esm/layouts/withLayoutContext.test.native.js.map +1 -0
- package/dist/esm/router/Route.mjs +3 -1
- package/dist/esm/router/Route.mjs.map +1 -1
- package/dist/esm/router/Route.native.js +3 -1
- package/dist/esm/router/Route.native.js.map +1 -1
- package/dist/esm/router/protectedRoutes.test.mjs +23 -0
- package/dist/esm/router/protectedRoutes.test.mjs.map +1 -0
- package/dist/esm/router/protectedRoutes.test.native.js +23 -0
- package/dist/esm/router/protectedRoutes.test.native.js.map +1 -0
- package/dist/esm/router/router.mjs +26 -9
- package/dist/esm/router/router.mjs.map +1 -1
- package/dist/esm/router/router.native.js +26 -9
- package/dist/esm/router/router.native.js.map +1 -1
- package/dist/esm/router/useScreens.mjs +39 -11
- package/dist/esm/router/useScreens.mjs.map +1 -1
- package/dist/esm/router/useScreens.native.js +40 -11
- package/dist/esm/router/useScreens.native.js.map +1 -1
- package/dist/esm/router/useScreens.test.mjs +105 -0
- package/dist/esm/router/useScreens.test.mjs.map +1 -0
- package/dist/esm/router/useScreens.test.native.js +130 -0
- package/dist/esm/router/useScreens.test.native.js.map +1 -0
- package/dist/esm/views/Navigator.mjs +15 -7
- package/dist/esm/views/Navigator.mjs.map +1 -1
- package/dist/esm/views/Navigator.native.js +15 -7
- package/dist/esm/views/Navigator.native.js.map +1 -1
- package/dist/esm/views/Protected.mjs.map +1 -1
- package/dist/esm/views/Protected.native.js.map +1 -1
- package/dist/esm/views/SuspenseFallback.mjs +2 -0
- package/dist/esm/views/SuspenseFallback.mjs.map +1 -0
- package/dist/esm/views/SuspenseFallback.native.js +2 -0
- package/dist/esm/views/SuspenseFallback.native.js.map +1 -0
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs +47 -7
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs.map +1 -1
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js +47 -7
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.test.mjs +15 -6
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.test.mjs.map +1 -1
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.test.native.js +15 -6
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.test.native.js.map +1 -1
- package/package.json +12 -11
- package/src/cli/dev.ts +0 -1
- package/src/cli.ts +0 -3
- package/src/image/getImageData.ts +2 -2
- package/src/index.ts +12 -0
- package/src/layouts/Stack.tsx +6 -4
- package/src/layouts/stack-utils/README.md +1 -0
- package/src/layouts/stack-utils/StackScreen.tsx +7 -0
- package/src/layouts/stack-utils/StackToolbar.tsx +550 -0
- package/src/layouts/stack-utils/StackToolbar.types.ts +98 -0
- package/src/layouts/stack-utils/__tests__/StackToolbar.runtime.test.tsx +46 -0
- package/src/layouts/stack-utils/__tests__/composition.test.tsx +176 -0
- package/src/layouts/stack-utils/index.tsx +16 -0
- package/src/layouts/withLayoutContext.test.tsx +74 -0
- package/src/layouts/withLayoutContext.tsx +71 -12
- package/src/router/Route.tsx +8 -0
- package/src/router/protectedRoutes.test.ts +35 -0
- package/src/router/router.ts +48 -20
- package/src/router/useScreens.test.tsx +139 -0
- package/src/router/useScreens.tsx +59 -13
- package/src/views/Navigator.tsx +25 -5
- package/src/views/Protected.tsx +3 -0
- package/src/views/SuspenseFallback.ts +7 -0
- package/src/vite/plugins/fileSystemRouterPlugin.test.ts +15 -6
- package/src/vite/plugins/fileSystemRouterPlugin.tsx +52 -7
- package/types/cli/dev.d.ts +0 -1
- package/types/cli/dev.d.ts.map +1 -1
- package/types/image/getImageData.d.ts +1 -1
- package/types/image/getImageData.d.ts.map +1 -1
- package/types/index.d.ts +2 -1
- package/types/index.d.ts.map +1 -1
- package/types/layouts/Stack.d.ts +11 -0
- package/types/layouts/Stack.d.ts.map +1 -1
- package/types/layouts/stack-utils/StackScreen.d.ts.map +1 -1
- package/types/layouts/stack-utils/StackToolbar.d.ts +26 -0
- package/types/layouts/stack-utils/StackToolbar.d.ts.map +1 -0
- package/types/layouts/stack-utils/StackToolbar.types.d.ts +82 -0
- package/types/layouts/stack-utils/StackToolbar.types.d.ts.map +1 -0
- package/types/layouts/stack-utils/index.d.ts +3 -1
- package/types/layouts/stack-utils/index.d.ts.map +1 -1
- package/types/layouts/withLayoutContext.d.ts +7 -0
- package/types/layouts/withLayoutContext.d.ts.map +1 -1
- package/types/layouts/withLayoutContext.test.d.ts +2 -0
- package/types/layouts/withLayoutContext.test.d.ts.map +1 -0
- package/types/router/Route.d.ts +4 -0
- package/types/router/Route.d.ts.map +1 -1
- package/types/router/protectedRoutes.test.d.ts +2 -0
- package/types/router/protectedRoutes.test.d.ts.map +1 -0
- package/types/router/router.d.ts +4 -6
- package/types/router/router.d.ts.map +1 -1
- package/types/router/useScreens.d.ts.map +1 -1
- package/types/router/useScreens.test.d.ts +2 -0
- package/types/router/useScreens.test.d.ts.map +1 -0
- package/types/views/Navigator.d.ts.map +1 -1
- package/types/views/Protected.d.ts +3 -0
- package/types/views/Protected.d.ts.map +1 -1
- package/types/views/SuspenseFallback.d.ts +8 -0
- package/types/views/SuspenseFallback.d.ts.map +1 -0
- package/types/vite/plugins/fileSystemRouterPlugin.d.ts.map +1 -1
package/src/router/router.ts
CHANGED
|
@@ -75,9 +75,8 @@ export { getSafeWindowPathname } from './path'
|
|
|
75
75
|
export let routeNode: RouteNode | null = null
|
|
76
76
|
export let rootComponent: ComponentType
|
|
77
77
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
const protectedRouteRegistry = new Map<string, Set<string>>()
|
|
78
|
+
// key: navigator context, value: guarded route names and their optional redirect targets.
|
|
79
|
+
const protectedRouteRegistry = new Map<string, Map<string, OneRouter.Href | undefined>>()
|
|
81
80
|
|
|
82
81
|
/**
|
|
83
82
|
* Register protected routes for a navigator context.
|
|
@@ -85,12 +84,12 @@ const protectedRouteRegistry = new Map<string, Set<string>>()
|
|
|
85
84
|
*/
|
|
86
85
|
export function registerProtectedRoutes(
|
|
87
86
|
contextKey: string,
|
|
88
|
-
|
|
87
|
+
guardedRedirects: Map<string, OneRouter.Href | undefined>
|
|
89
88
|
) {
|
|
90
|
-
if (
|
|
89
|
+
if (guardedRedirects.size === 0) {
|
|
91
90
|
protectedRouteRegistry.delete(contextKey)
|
|
92
91
|
} else {
|
|
93
|
-
protectedRouteRegistry.set(contextKey,
|
|
92
|
+
protectedRouteRegistry.set(contextKey, guardedRedirects)
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
95
|
|
|
@@ -106,26 +105,28 @@ export function unregisterProtectedRoutes(contextKey: string) {
|
|
|
106
105
|
* Check if a route path is protected and should be blocked.
|
|
107
106
|
* Returns true if the route is protected.
|
|
108
107
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const normalizedHref = href.replace(/^\/+|\/+$/g, '')
|
|
108
|
+
function getProtectedRedirect(href: string): OneRouter.Href | undefined | false {
|
|
109
|
+
const normalizedHref = extractPathnameFromHref(href).replace(/^\/+|\/+$/g, '')
|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
for (const [contextKey, protectedScreens] of protectedRouteRegistry) {
|
|
111
|
+
for (const [contextKey, guardedRedirects] of protectedRouteRegistry) {
|
|
115
112
|
const normalizedContextKey = contextKey.replace(/^\/+|\/+$/g, '')
|
|
116
113
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
if (
|
|
115
|
+
!normalizedContextKey ||
|
|
116
|
+
normalizedHref === normalizedContextKey ||
|
|
117
|
+
normalizedHref.startsWith(`${normalizedContextKey}/`)
|
|
118
|
+
) {
|
|
120
119
|
const relativePath = normalizedHref
|
|
121
120
|
.slice(normalizedContextKey.length)
|
|
122
121
|
.replace(/^\//, '')
|
|
123
122
|
const routeName = relativePath.split('/')[0] || 'index'
|
|
124
|
-
|
|
125
|
-
// normalize /index suffix so e.g. "otp/[flow]" matches "otp/[flow]/index"
|
|
126
123
|
const normalizedRouteName = routeName.replace(/\/index$/, '')
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
|
|
125
|
+
if (guardedRedirects.has(routeName)) {
|
|
126
|
+
return guardedRedirects.get(routeName)
|
|
127
|
+
}
|
|
128
|
+
if (guardedRedirects.has(normalizedRouteName)) {
|
|
129
|
+
return guardedRedirects.get(normalizedRouteName)
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
}
|
|
@@ -133,6 +134,30 @@ export function isRouteProtected(href: string): boolean {
|
|
|
133
134
|
return false
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
/** resolve a protected href to its allowed redirect target. */
|
|
138
|
+
export function resolveProtectedHref(href: string): string | undefined {
|
|
139
|
+
let target = href
|
|
140
|
+
const visited = new Set<string>()
|
|
141
|
+
|
|
142
|
+
while (!visited.has(target)) {
|
|
143
|
+
visited.add(target)
|
|
144
|
+
const redirectTo = getProtectedRedirect(target)
|
|
145
|
+
if (redirectTo === false) {
|
|
146
|
+
return target
|
|
147
|
+
}
|
|
148
|
+
if (redirectTo === undefined) {
|
|
149
|
+
return undefined
|
|
150
|
+
}
|
|
151
|
+
target = resolveHref(redirectTo)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function isRouteProtected(href: string): boolean {
|
|
158
|
+
return getProtectedRedirect(href) !== false
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
export let hasAttemptedToHideSplash = false
|
|
137
162
|
export let initialState: OneRouter.ResultState | undefined
|
|
138
163
|
export let rootState: OneRouter.ResultState | undefined
|
|
@@ -1077,10 +1102,13 @@ export async function linkTo(
|
|
|
1077
1102
|
return
|
|
1078
1103
|
}
|
|
1079
1104
|
|
|
1080
|
-
|
|
1081
|
-
if (
|
|
1105
|
+
const protectedHref = resolveProtectedHref(href)
|
|
1106
|
+
if (!protectedHref) {
|
|
1082
1107
|
return
|
|
1083
1108
|
}
|
|
1109
|
+
if (protectedHref !== href) {
|
|
1110
|
+
return linkTo(protectedHref, 'REPLACE', options)
|
|
1111
|
+
}
|
|
1084
1112
|
|
|
1085
1113
|
// Check for intercepting routes (parallel routes with @slot)
|
|
1086
1114
|
// This enables modal patterns where soft nav shows modal, hard nav shows full page
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import TestRenderer, { act } from 'react-test-renderer'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
4
|
+
import type { LoadedRoute, RouteNode } from './Route'
|
|
5
|
+
import { getQualifiedRouteComponent } from './useScreens'
|
|
6
|
+
|
|
7
|
+
type FallbackProps = {
|
|
8
|
+
route: string
|
|
9
|
+
params: Record<string, string | string[]>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const pendingRoute = new Promise<void>(() => {})
|
|
13
|
+
const previousTarget = process.env.TAMAGUI_TARGET
|
|
14
|
+
const previousSuspendRoutesNative = process.env.ONE_SUSPEND_ROUTES_NATIVE
|
|
15
|
+
const previousDisableSuspense = (globalThis as any).__ONE_DISABLE_SUSPENSE_ROUTES__
|
|
16
|
+
|
|
17
|
+
function createRouteNode(
|
|
18
|
+
type: RouteNode['type'],
|
|
19
|
+
route: string,
|
|
20
|
+
contextKey: string,
|
|
21
|
+
loadRoute: () => Partial<LoadedRoute>
|
|
22
|
+
): RouteNode {
|
|
23
|
+
return {
|
|
24
|
+
type,
|
|
25
|
+
route,
|
|
26
|
+
contextKey,
|
|
27
|
+
loadRoute,
|
|
28
|
+
children: [],
|
|
29
|
+
dynamic: null,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function SuspendingRoute(): React.ReactNode {
|
|
34
|
+
throw pendingRoute
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
process.env.TAMAGUI_TARGET = 'native'
|
|
39
|
+
delete process.env.ONE_SUSPEND_ROUTES_NATIVE
|
|
40
|
+
delete (globalThis as any).__ONE_DISABLE_SUSPENSE_ROUTES__
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
if (previousTarget === undefined) {
|
|
45
|
+
delete process.env.TAMAGUI_TARGET
|
|
46
|
+
} else {
|
|
47
|
+
process.env.TAMAGUI_TARGET = previousTarget
|
|
48
|
+
}
|
|
49
|
+
if (previousSuspendRoutesNative === undefined) {
|
|
50
|
+
delete process.env.ONE_SUSPEND_ROUTES_NATIVE
|
|
51
|
+
} else {
|
|
52
|
+
process.env.ONE_SUSPEND_ROUTES_NATIVE = previousSuspendRoutesNative
|
|
53
|
+
}
|
|
54
|
+
if (previousDisableSuspense === undefined) {
|
|
55
|
+
delete (globalThis as any).__ONE_DISABLE_SUSPENSE_ROUTES__
|
|
56
|
+
} else {
|
|
57
|
+
;(globalThis as any).__ONE_DISABLE_SUSPENSE_ROUTES__ = previousDisableSuspense
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('route SuspenseFallback', () => {
|
|
62
|
+
it('inherits the layout fallback and passes the suspended route details', async () => {
|
|
63
|
+
const childNode = createRouteNode(
|
|
64
|
+
'spa',
|
|
65
|
+
'profile/[id]',
|
|
66
|
+
'./(app)/profile/[id].tsx',
|
|
67
|
+
() => {
|
|
68
|
+
throw pendingRoute
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
const ChildRoute = getQualifiedRouteComponent(childNode)
|
|
72
|
+
const layoutNode = createRouteNode('layout', '(app)', './(app)/_layout.tsx', () => ({
|
|
73
|
+
default: () => <ChildRoute route={{ params: { id: '123' } }} />,
|
|
74
|
+
SuspenseFallback: ({ route, params }: FallbackProps) => (
|
|
75
|
+
<div data-fallback="layout">
|
|
76
|
+
{route}:{params.id}
|
|
77
|
+
</div>
|
|
78
|
+
),
|
|
79
|
+
}))
|
|
80
|
+
const LayoutRoute = getQualifiedRouteComponent(layoutNode)
|
|
81
|
+
|
|
82
|
+
let renderer: TestRenderer.ReactTestRenderer
|
|
83
|
+
await act(async () => {
|
|
84
|
+
renderer = TestRenderer.create(<LayoutRoute />)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
expect(renderer!.root.findByProps({ 'data-fallback': 'layout' }).children).toEqual([
|
|
88
|
+
'./(app)/profile/[id].tsx',
|
|
89
|
+
':',
|
|
90
|
+
'123',
|
|
91
|
+
])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('uses the nearest layout fallback', async () => {
|
|
95
|
+
const childNode = createRouteNode('spa', 'profile', './(app)/profile.tsx', () => ({
|
|
96
|
+
default: SuspendingRoute,
|
|
97
|
+
}))
|
|
98
|
+
const ChildRoute = getQualifiedRouteComponent(childNode)
|
|
99
|
+
const nestedLayoutNode = createRouteNode(
|
|
100
|
+
'layout',
|
|
101
|
+
'(app)',
|
|
102
|
+
'./(app)/_layout.tsx',
|
|
103
|
+
() => ({
|
|
104
|
+
default: () => <ChildRoute />,
|
|
105
|
+
SuspenseFallback: () => <div data-fallback="nested" />,
|
|
106
|
+
})
|
|
107
|
+
)
|
|
108
|
+
const NestedLayoutRoute = getQualifiedRouteComponent(nestedLayoutNode)
|
|
109
|
+
const rootLayoutNode = createRouteNode('layout', '', './_layout.tsx', () => ({
|
|
110
|
+
default: () => <NestedLayoutRoute />,
|
|
111
|
+
SuspenseFallback: () => <div data-fallback="root" />,
|
|
112
|
+
}))
|
|
113
|
+
const RootLayoutRoute = getQualifiedRouteComponent(rootLayoutNode)
|
|
114
|
+
|
|
115
|
+
let renderer: TestRenderer.ReactTestRenderer
|
|
116
|
+
await act(async () => {
|
|
117
|
+
renderer = TestRenderer.create(<RootLayoutRoute />)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(renderer!.root.findAllByProps({ 'data-fallback': 'nested' })).toHaveLength(1)
|
|
121
|
+
expect(renderer!.root.findAllByProps({ 'data-fallback': 'root' })).toHaveLength(0)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('ignores a SuspenseFallback exported from a leaf route', async () => {
|
|
125
|
+
const childNode = createRouteNode('spa', 'profile', './profile.tsx', () => ({
|
|
126
|
+
default: SuspendingRoute,
|
|
127
|
+
SuspenseFallback: () => <div data-fallback="leaf" />,
|
|
128
|
+
}))
|
|
129
|
+
const ChildRoute = getQualifiedRouteComponent(childNode)
|
|
130
|
+
|
|
131
|
+
let renderer: TestRenderer.ReactTestRenderer
|
|
132
|
+
await act(async () => {
|
|
133
|
+
renderer = TestRenderer.create(<ChildRoute />)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
expect(renderer!.root.findAllByProps({ 'data-fallback': 'leaf' })).toHaveLength(0)
|
|
137
|
+
expect(renderer!.toJSON()).toBeNull()
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -22,11 +22,14 @@ import {
|
|
|
22
22
|
type DynamicConvention,
|
|
23
23
|
type LoadedRoute,
|
|
24
24
|
Route,
|
|
25
|
+
RouteParamsContext,
|
|
25
26
|
type RouteNode,
|
|
27
|
+
SuspenseFallbackContext,
|
|
26
28
|
useRouteNode,
|
|
27
29
|
} from './Route'
|
|
28
30
|
import { SpaShellContext } from './SpaShellContext'
|
|
29
31
|
import { NamedSlot } from '../views/Navigator'
|
|
32
|
+
import type { SuspenseFallbackProps } from '../views/SuspenseFallback'
|
|
30
33
|
import { sortRoutesWithInitial } from './sortRoutes'
|
|
31
34
|
import { getRouteHmrEpoch, subscribeRouteHmr } from './routeHmr'
|
|
32
35
|
import { getClientMatchesSnapshot } from '../useMatches'
|
|
@@ -337,7 +340,7 @@ export function useSortedScreens(
|
|
|
337
340
|
return sortedScreens
|
|
338
341
|
}
|
|
339
342
|
|
|
340
|
-
function fromImport({ ErrorBoundary, ...component }: LoadedRoute) {
|
|
343
|
+
function fromImport({ ErrorBoundary, SuspenseFallback, ...component }: LoadedRoute) {
|
|
341
344
|
if (ErrorBoundary) {
|
|
342
345
|
return {
|
|
343
346
|
default: React.forwardRef((props: any, ref: any) => {
|
|
@@ -347,16 +350,30 @@ function fromImport({ ErrorBoundary, ...component }: LoadedRoute) {
|
|
|
347
350
|
})
|
|
348
351
|
return <Try catch={ErrorBoundary}>{children}</Try>
|
|
349
352
|
}),
|
|
353
|
+
SuspenseFallback,
|
|
350
354
|
}
|
|
351
355
|
}
|
|
352
356
|
if (process.env.NODE_ENV !== 'production') {
|
|
353
357
|
const exported = getPageExport(component)
|
|
354
358
|
if (exported && typeof exported === 'object' && Object.keys(exported).length === 0) {
|
|
355
|
-
return { default: EmptyRoute }
|
|
359
|
+
return { default: EmptyRoute, SuspenseFallback }
|
|
356
360
|
}
|
|
357
361
|
}
|
|
358
362
|
|
|
359
|
-
return { default: getPageExport(component) }
|
|
363
|
+
return { default: getPageExport(component), SuspenseFallback }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function RouteSuspenseFallback({
|
|
367
|
+
component: Component,
|
|
368
|
+
route,
|
|
369
|
+
}: {
|
|
370
|
+
component?: React.ComponentType<SuspenseFallbackProps>
|
|
371
|
+
route: string
|
|
372
|
+
}) {
|
|
373
|
+
const params = useContext(RouteParamsContext)
|
|
374
|
+
return Component ? (
|
|
375
|
+
<Component route={route} params={(params ?? {}) as SuspenseFallbackProps['params']} />
|
|
376
|
+
) : null
|
|
360
377
|
}
|
|
361
378
|
|
|
362
379
|
// TODO: Maybe there's a more React-y way to do this?
|
|
@@ -412,6 +429,7 @@ export function getQualifiedRouteComponent(value: RouteNode) {
|
|
|
412
429
|
// content after hydration.
|
|
413
430
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
414
431
|
const isSpaShell = useContext(SpaShellContext)
|
|
432
|
+
const InheritedSuspenseFallback = useContext(SuspenseFallbackContext)
|
|
415
433
|
const matches = getRouteMatchesForProps()
|
|
416
434
|
|
|
417
435
|
if (isSpaShell && props.segment !== '') {
|
|
@@ -423,8 +441,10 @@ export function getQualifiedRouteComponent(value: RouteNode) {
|
|
|
423
441
|
}
|
|
424
442
|
}
|
|
425
443
|
|
|
426
|
-
const res = value.loadRoute()
|
|
427
|
-
const Component = getPageExport(
|
|
444
|
+
const res = fromImport(value.loadRoute())
|
|
445
|
+
const Component = getPageExport(res) as React.ComponentType<any>
|
|
446
|
+
const LayoutSuspenseFallback =
|
|
447
|
+
value.type === 'layout' ? res.SuspenseFallback : undefined
|
|
428
448
|
const match =
|
|
429
449
|
matches.find((match) => match.routeId === value.contextKey) ||
|
|
430
450
|
(!value.children?.length ? matches[matches.length - 1] : undefined)
|
|
@@ -450,25 +470,48 @@ export function getQualifiedRouteComponent(value: RouteNode) {
|
|
|
450
470
|
}
|
|
451
471
|
}
|
|
452
472
|
|
|
473
|
+
let rendered: React.ReactNode
|
|
474
|
+
|
|
453
475
|
// Root layout with HTML support - use RootLayoutRenderer for proper HMR tracking
|
|
454
476
|
if (props.segment === '') {
|
|
455
|
-
|
|
477
|
+
rendered = (
|
|
456
478
|
<RootLayoutRenderer
|
|
457
479
|
LayoutComponent={Component}
|
|
458
480
|
layoutProps={{ ...props, ...slotProps, ...loaderDataProps }}
|
|
459
481
|
forwardedRef={ref}
|
|
460
482
|
/>
|
|
461
483
|
)
|
|
484
|
+
} else {
|
|
485
|
+
rendered = (
|
|
486
|
+
<RouteErrorBoundary routeName={value.route}>
|
|
487
|
+
<Component {...props} {...slotProps} {...loaderDataProps} ref={ref} />
|
|
488
|
+
</RouteErrorBoundary>
|
|
489
|
+
)
|
|
462
490
|
}
|
|
463
491
|
|
|
492
|
+
if (value.type !== 'layout') {
|
|
493
|
+
return rendered
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const providedSuspenseFallback = LayoutSuspenseFallback ?? InheritedSuspenseFallback
|
|
497
|
+
|
|
464
498
|
return (
|
|
465
|
-
<
|
|
466
|
-
|
|
467
|
-
|
|
499
|
+
<SuspenseFallbackContext.Provider value={providedSuspenseFallback}>
|
|
500
|
+
{LayoutSuspenseFallback
|
|
501
|
+
? wrapSuspense(rendered, LayoutSuspenseFallback)
|
|
502
|
+
: rendered}
|
|
503
|
+
</SuspenseFallbackContext.Provider>
|
|
468
504
|
)
|
|
469
505
|
})
|
|
470
506
|
|
|
471
|
-
const wrapSuspense = (
|
|
507
|
+
const wrapSuspense = (
|
|
508
|
+
children: React.ReactNode,
|
|
509
|
+
SuspenseFallback?: React.ComponentType<SuspenseFallbackProps>
|
|
510
|
+
) => {
|
|
511
|
+
const fallback = (
|
|
512
|
+
<RouteSuspenseFallback component={SuspenseFallback} route={value.contextKey} />
|
|
513
|
+
)
|
|
514
|
+
|
|
472
515
|
if (process.env.TAMAGUI_TARGET === 'native') {
|
|
473
516
|
// native opt-out: set native.suspendRoutes to false in your one() config
|
|
474
517
|
// OR set globalThis.__ONE_DISABLE_SUSPENSE_ROUTES__ = true at runtime
|
|
@@ -487,7 +530,7 @@ export function getQualifiedRouteComponent(value: RouteNode) {
|
|
|
487
530
|
) {
|
|
488
531
|
return children
|
|
489
532
|
}
|
|
490
|
-
return <Suspense fallback={
|
|
533
|
+
return <Suspense fallback={fallback}>{children}</Suspense>
|
|
491
534
|
}
|
|
492
535
|
|
|
493
536
|
// web opt-in: set web.suspendRoutes to true in your one() config
|
|
@@ -497,7 +540,7 @@ export function getQualifiedRouteComponent(value: RouteNode) {
|
|
|
497
540
|
process.env.ONE_SUSPEND_ROUTES === '1' &&
|
|
498
541
|
(globalThis as any).__ONE_DISABLE_SUSPENSE_ROUTES__ !== true
|
|
499
542
|
) {
|
|
500
|
-
return <Suspense fallback={
|
|
543
|
+
return <Suspense fallback={fallback}>{children}</Suspense>
|
|
501
544
|
}
|
|
502
545
|
return children
|
|
503
546
|
}
|
|
@@ -515,6 +558,8 @@ export function getQualifiedRouteComponent(value: RouteNode) {
|
|
|
515
558
|
}: any,
|
|
516
559
|
ref: any
|
|
517
560
|
) => {
|
|
561
|
+
const InheritedSuspenseFallback = useContext(SuspenseFallbackContext)
|
|
562
|
+
|
|
518
563
|
return (
|
|
519
564
|
<Route route={route} node={value}>
|
|
520
565
|
<>
|
|
@@ -527,7 +572,8 @@ export function getQualifiedRouteComponent(value: RouteNode) {
|
|
|
527
572
|
// the intention is to make it possible to deduce shared routes.
|
|
528
573
|
segment: value.route,
|
|
529
574
|
}}
|
|
530
|
-
|
|
575
|
+
/>,
|
|
576
|
+
InheritedSuspenseFallback
|
|
531
577
|
)}
|
|
532
578
|
</>
|
|
533
579
|
</Route>
|
package/src/views/Navigator.tsx
CHANGED
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
import { NavigationRouteContext } from '@react-navigation/core'
|
|
8
8
|
import * as React from 'react'
|
|
9
9
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
useFilterScreenChildren,
|
|
12
|
+
useResolvedGuardedRedirects,
|
|
13
|
+
} from '../layouts/withLayoutContext'
|
|
11
14
|
import {
|
|
12
15
|
findNearestNotFoundRoute,
|
|
13
16
|
findRouteNodeByPath,
|
|
@@ -20,8 +23,12 @@ import {
|
|
|
20
23
|
getSafeWindowPathname,
|
|
21
24
|
initialPathname,
|
|
22
25
|
lastIntendedPathname,
|
|
26
|
+
replace,
|
|
27
|
+
registerProtectedRoutes,
|
|
28
|
+
resolveProtectedHref,
|
|
29
|
+
routeInfo,
|
|
30
|
+
unregisterProtectedRoutes,
|
|
23
31
|
} from '../router/router'
|
|
24
|
-
import { registerProtectedRoutes, unregisterProtectedRoutes } from '../router/router'
|
|
25
32
|
import { useSortedScreens, getQualifiedRouteComponent } from '../router/useScreens'
|
|
26
33
|
import { Screen } from './Screen'
|
|
27
34
|
|
|
@@ -174,21 +181,34 @@ export function Navigator({
|
|
|
174
181
|
screens,
|
|
175
182
|
children: otherSlot,
|
|
176
183
|
protectedScreens,
|
|
184
|
+
guardedRedirects,
|
|
177
185
|
} = useFilterScreenChildren(children, {
|
|
178
186
|
isCustomNavigator: true,
|
|
179
187
|
contextKey,
|
|
180
188
|
})
|
|
189
|
+
const resolvedGuardedRedirects = useResolvedGuardedRedirects(guardedRedirects)
|
|
181
190
|
|
|
182
191
|
// Register protected routes globally so linkTo can block navigation to them
|
|
183
192
|
// Register immediately (not just in effect) to catch navigation attempts during first render
|
|
184
|
-
registerProtectedRoutes(contextKey,
|
|
193
|
+
registerProtectedRoutes(contextKey, resolvedGuardedRedirects)
|
|
194
|
+
|
|
195
|
+
const currentPathname = routeInfo?.pathname
|
|
196
|
+
const protectedHref = currentPathname
|
|
197
|
+
? resolveProtectedHref(currentPathname)
|
|
198
|
+
: currentPathname
|
|
185
199
|
|
|
186
200
|
React.useEffect(() => {
|
|
187
|
-
registerProtectedRoutes(contextKey,
|
|
201
|
+
registerProtectedRoutes(contextKey, resolvedGuardedRedirects)
|
|
188
202
|
return () => {
|
|
189
203
|
unregisterProtectedRoutes(contextKey)
|
|
190
204
|
}
|
|
191
|
-
}, [contextKey,
|
|
205
|
+
}, [contextKey, resolvedGuardedRedirects])
|
|
206
|
+
|
|
207
|
+
React.useEffect(() => {
|
|
208
|
+
if (currentPathname && protectedHref && protectedHref !== currentPathname) {
|
|
209
|
+
replace(protectedHref)
|
|
210
|
+
}
|
|
211
|
+
}, [currentPathname, protectedHref])
|
|
192
212
|
|
|
193
213
|
const sorted = useSortedScreens(screens ?? [], { protectedScreens })
|
|
194
214
|
|
package/src/views/Protected.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type ReactNode,
|
|
6
6
|
isValidElement,
|
|
7
7
|
} from 'react'
|
|
8
|
+
import type { OneRouter } from '../interfaces/router'
|
|
8
9
|
|
|
9
10
|
// Get Group component directly from react-navigation core
|
|
10
11
|
// (avoids pulling in @react-navigation/elements which has asset imports)
|
|
@@ -12,6 +13,8 @@ const { Group } = createNavigatorFactory({} as any)()
|
|
|
12
13
|
|
|
13
14
|
export type ProtectedProps = {
|
|
14
15
|
guard: boolean
|
|
16
|
+
/** where to redirect when `guard` is false. defaults to the navigator's first available route. */
|
|
17
|
+
redirectTo?: OneRouter.Href
|
|
15
18
|
children?: ReactNode
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Props passed to a layout's `SuspenseFallback` export. */
|
|
2
|
+
export type SuspenseFallbackProps = {
|
|
3
|
+
/** The suspended route module's context key, such as `./profile/[id].tsx`. */
|
|
4
|
+
route: string
|
|
5
|
+
/** The suspended route's URL parameters. */
|
|
6
|
+
params: Record<string, string | string[]>
|
|
7
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
2
3
|
import { tmpdir } from 'node:os'
|
|
3
4
|
import path from 'node:path'
|
|
4
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
@@ -192,23 +193,31 @@ describe('createFileSystemRouterPlugin', () => {
|
|
|
192
193
|
|
|
193
194
|
async function request(pathname: string) {
|
|
194
195
|
const chunks: string[] = []
|
|
195
|
-
const req = {
|
|
196
|
+
const req = Object.assign(new EventEmitter(), {
|
|
197
|
+
aborted: false,
|
|
198
|
+
complete: true,
|
|
196
199
|
originalUrl: pathname,
|
|
197
200
|
url: pathname,
|
|
198
201
|
headers: {
|
|
199
|
-
|
|
202
|
+
':authority': 'localhost',
|
|
203
|
+
':method': 'GET',
|
|
200
204
|
},
|
|
201
205
|
method: 'GET',
|
|
202
|
-
}
|
|
203
|
-
const res = {
|
|
206
|
+
})
|
|
207
|
+
const res = Object.assign(new EventEmitter(), {
|
|
208
|
+
destroyed: false,
|
|
209
|
+
writableFinished: false,
|
|
204
210
|
setHeader: vi.fn(),
|
|
205
211
|
appendHeader: vi.fn(),
|
|
206
212
|
writeHead: vi.fn(),
|
|
207
213
|
write: vi.fn((chunk: string) => {
|
|
208
214
|
chunks.push(chunk)
|
|
209
215
|
}),
|
|
210
|
-
end: vi.fn()
|
|
211
|
-
|
|
216
|
+
end: vi.fn(() => {
|
|
217
|
+
res.writableFinished = true
|
|
218
|
+
res.emit('finish')
|
|
219
|
+
}),
|
|
220
|
+
})
|
|
212
221
|
const next = vi.fn((error?: unknown) => {
|
|
213
222
|
if (error) {
|
|
214
223
|
throw error
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { Readable } from 'node:stream'
|
|
3
|
+
import { TLSSocket } from 'node:tls'
|
|
3
4
|
import { debounce } from 'perfect-debounce'
|
|
4
5
|
import colors from 'picocolors'
|
|
5
6
|
import type { Connect, Plugin, ViteDevServer } from 'vite'
|
|
@@ -746,10 +747,36 @@ export function createFileSystemRouterPlugin(options: One.PluginOptions): Plugin
|
|
|
746
747
|
// prevent browsers (safari) from caching stale html/js in dev
|
|
747
748
|
res.setHeader('Cache-Control', 'no-store')
|
|
748
749
|
|
|
750
|
+
const abortController = new AbortController()
|
|
751
|
+
const cleanupAbortListeners = () => {
|
|
752
|
+
req.removeListener('aborted', abortRequest)
|
|
753
|
+
req.removeListener('close', abortIncompleteRequest)
|
|
754
|
+
res.removeListener('close', abortIncompleteResponse)
|
|
755
|
+
res.removeListener('finish', cleanupAbortListeners)
|
|
756
|
+
}
|
|
757
|
+
const abortRequest = () => {
|
|
758
|
+
cleanupAbortListeners()
|
|
759
|
+
abortController.abort()
|
|
760
|
+
}
|
|
761
|
+
const abortIncompleteRequest = () => {
|
|
762
|
+
if (req.aborted || !req.complete) abortRequest()
|
|
763
|
+
}
|
|
764
|
+
const abortIncompleteResponse = () => {
|
|
765
|
+
if (!res.writableFinished) {
|
|
766
|
+
abortRequest()
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
cleanupAbortListeners()
|
|
770
|
+
}
|
|
771
|
+
req.on('aborted', abortRequest)
|
|
772
|
+
req.on('close', abortIncompleteRequest)
|
|
773
|
+
res.on('close', abortIncompleteResponse)
|
|
774
|
+
res.on('finish', cleanupAbortListeners)
|
|
775
|
+
|
|
749
776
|
try {
|
|
750
777
|
const redirects = options.web?.redirects
|
|
751
778
|
if (redirects) {
|
|
752
|
-
const url = new URL(req.url || '',
|
|
779
|
+
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
753
780
|
for (const redirect of redirects) {
|
|
754
781
|
const regexStr = `^${redirect.source.replace(/:\w+/g, '([^/]+)')}$`
|
|
755
782
|
const match = url.pathname.match(new RegExp(regexStr))
|
|
@@ -778,10 +805,11 @@ export function createFileSystemRouterPlugin(options: One.PluginOptions): Plugin
|
|
|
778
805
|
}
|
|
779
806
|
|
|
780
807
|
const reply = await handleRequest.handler(
|
|
781
|
-
convertIncomingMessageToRequest(req)
|
|
808
|
+
convertIncomingMessageToRequest(req, abortController.signal)
|
|
782
809
|
)
|
|
783
810
|
|
|
784
811
|
if (!reply) {
|
|
812
|
+
cleanupAbortListeners()
|
|
785
813
|
return next()
|
|
786
814
|
}
|
|
787
815
|
|
|
@@ -822,7 +850,6 @@ export function createFileSystemRouterPlugin(options: One.PluginOptions): Plugin
|
|
|
822
850
|
}
|
|
823
851
|
|
|
824
852
|
res.statusCode = reply.status
|
|
825
|
-
res.statusMessage = reply.statusText
|
|
826
853
|
|
|
827
854
|
if (reply.body) {
|
|
828
855
|
if (reply.body.locked) {
|
|
@@ -831,7 +858,14 @@ export function createFileSystemRouterPlugin(options: One.PluginOptions): Plugin
|
|
|
831
858
|
return
|
|
832
859
|
}
|
|
833
860
|
try {
|
|
834
|
-
Readable.fromWeb(reply.body as any)
|
|
861
|
+
const body = Readable.fromWeb(reply.body as any)
|
|
862
|
+
body.on('error', (error) => {
|
|
863
|
+
if (!abortController.signal.aborted) {
|
|
864
|
+
console.warn('Error streaming reply body to response:', error)
|
|
865
|
+
}
|
|
866
|
+
if (!res.destroyed) res.destroy()
|
|
867
|
+
})
|
|
868
|
+
body.pipe(res)
|
|
835
869
|
} catch (err) {
|
|
836
870
|
console.warn('Error piping reply body to response:', err)
|
|
837
871
|
res.end()
|
|
@@ -854,6 +888,7 @@ export function createFileSystemRouterPlugin(options: One.PluginOptions): Plugin
|
|
|
854
888
|
res.end()
|
|
855
889
|
return
|
|
856
890
|
} catch (error) {
|
|
891
|
+
cleanupAbortListeners()
|
|
857
892
|
console.error(`[one] routing error ${req.url}: ${error}`)
|
|
858
893
|
// Forward the error to Vite
|
|
859
894
|
next(error)
|
|
@@ -870,21 +905,30 @@ export function createFileSystemRouterPlugin(options: One.PluginOptions): Plugin
|
|
|
870
905
|
} satisfies Plugin
|
|
871
906
|
}
|
|
872
907
|
|
|
873
|
-
const convertIncomingMessageToRequest = (
|
|
908
|
+
const convertIncomingMessageToRequest = (
|
|
909
|
+
req: Connect.IncomingMessage,
|
|
910
|
+
signal: AbortSignal
|
|
911
|
+
): Request => {
|
|
874
912
|
if (!req.originalUrl) {
|
|
875
913
|
throw new Error(`Can't convert: originalUrl is missing`)
|
|
876
914
|
}
|
|
877
915
|
|
|
878
|
-
const
|
|
916
|
+
const protocol =
|
|
917
|
+
req.socket instanceof TLSSocket && req.socket.encrypted ? 'https' : 'http'
|
|
918
|
+
const host = `${req.headers['x-forwarded-host'] || req.headers.host || req.headers[':authority'] || '127.0.0.1'}`
|
|
919
|
+
const urlBase = `${protocol}://${host}`
|
|
879
920
|
const urlString = req.originalUrl
|
|
880
921
|
const url = new URL(urlString, urlBase)
|
|
881
922
|
|
|
882
923
|
const headers = new Headers()
|
|
883
924
|
for (const key in req.headers) {
|
|
884
|
-
if (req.headers[key]) {
|
|
925
|
+
if (!key.startsWith(':') && req.headers[key]) {
|
|
885
926
|
headers.append(key, req.headers[key] as string)
|
|
886
927
|
}
|
|
887
928
|
}
|
|
929
|
+
if (!headers.has('host')) {
|
|
930
|
+
headers.set('host', host)
|
|
931
|
+
}
|
|
888
932
|
|
|
889
933
|
const hasBody = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method || '')
|
|
890
934
|
const body = hasBody ? Readable.toWeb(req) : null
|
|
@@ -893,6 +937,7 @@ const convertIncomingMessageToRequest = (req: Connect.IncomingMessage): Request
|
|
|
893
937
|
method: req.method,
|
|
894
938
|
headers,
|
|
895
939
|
body,
|
|
940
|
+
signal,
|
|
896
941
|
// Required for streaming bodies in Node's experimental fetch:
|
|
897
942
|
duplex: 'half',
|
|
898
943
|
} as RequestInit & { duplex: 'half' })
|