flamefront 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +110 -0
- package/README.md +58 -0
- package/bin/ff-loader.mjs +18 -0
- package/bin/ff.js +6 -0
- package/package.json +84 -8
- package/src/babel.ts +22 -0
- package/src/cli.ts +98 -0
- package/src/entry.ts +49 -0
- package/src/fetch.ts +280 -0
- package/src/fragment-client.ts +271 -0
- package/src/fragment-hydration-client.tsx +81 -0
- package/src/fragment-protocol.ts +39 -0
- package/src/fragment.tsx +656 -0
- package/src/glob.ts +294 -0
- package/src/identifier-prefix.ts +5 -0
- package/src/index.ts +1168 -0
- package/src/lifecycle.ts +487 -0
- package/src/octane-client-core.ts +141 -0
- package/src/octane-client.ts +48 -0
- package/src/octane-compiler.d.ts +19 -0
- package/src/octane-default-renderer.tsx +124 -0
- package/src/octane-router-document.ts +22 -0
- package/src/octane.tsx +661 -0
- package/src/output.ts +48 -0
- package/src/remix-route-data.ts +76 -0
- package/src/remix-router-core.ts +106 -0
- package/src/remix-router.ts +117 -0
- package/src/remove-exports.ts +148 -0
- package/src/route-data-client.ts +234 -0
- package/src/route-prefetch.ts +79 -0
- package/src/server.ts +338 -0
- package/src/srvx.ts +174 -0
- package/src/static-fragment-artifacts.ts +86 -0
- package/src/typegen.ts +207 -0
- package/src/virtual-remix-routes.d.ts +35 -0
- package/src/vite.ts +1030 -0
- package/readme.md +0 -1
package/src/fragment.tsx
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Hydrate,
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
} from "octane"
|
|
12
|
+
import {
|
|
13
|
+
condition,
|
|
14
|
+
idle,
|
|
15
|
+
interaction,
|
|
16
|
+
media,
|
|
17
|
+
never,
|
|
18
|
+
visible,
|
|
19
|
+
} from "octane/hydration"
|
|
20
|
+
import type { Context } from "octane"
|
|
21
|
+
import {
|
|
22
|
+
UNSAFE_DataRouterContext,
|
|
23
|
+
UNSAFE_DataRouterStateContext,
|
|
24
|
+
UNSAFE_FetchersContext,
|
|
25
|
+
UNSAFE_LocationContext,
|
|
26
|
+
UNSAFE_NavigationContext,
|
|
27
|
+
UNSAFE_RouteContext,
|
|
28
|
+
UNSAFE_ViewTransitionContext,
|
|
29
|
+
} from "@octanejs/remix-router"
|
|
30
|
+
import type { GeneratedRouteMetadata, HydrationMode } from "./index.ts"
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
getRouteFragment,
|
|
34
|
+
shouldHydrateRouteFragment,
|
|
35
|
+
type RouteFragmentCachePolicy,
|
|
36
|
+
type RouteFragmentRoutingOptions,
|
|
37
|
+
} from "./fragment-client.ts"
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
assertRouteFragmentArtifact,
|
|
41
|
+
getRouteFragment,
|
|
42
|
+
isRouteFragmentArtifact,
|
|
43
|
+
loadRouteFragment,
|
|
44
|
+
prefetchRouteFragment,
|
|
45
|
+
shouldHydrateRouteFragment,
|
|
46
|
+
routeFragmentProtocol,
|
|
47
|
+
} from "./fragment-client.ts"
|
|
48
|
+
export type {
|
|
49
|
+
RouteFragmentArtifact,
|
|
50
|
+
RouteFragmentBoundary,
|
|
51
|
+
RouteFragmentCachePolicy,
|
|
52
|
+
RouteFragmentLoadOptions,
|
|
53
|
+
RouteFragmentRoutingOptions,
|
|
54
|
+
} from "./fragment-client.ts"
|
|
55
|
+
|
|
56
|
+
export interface RouteFragmentRouteOptions {
|
|
57
|
+
readonly metadata: Pick<
|
|
58
|
+
GeneratedRouteMetadata,
|
|
59
|
+
"id" | "boundary" | "kind" | "parent" | "path" | "hydration"
|
|
60
|
+
>
|
|
61
|
+
readonly routing: RouteFragmentRoutingOptions
|
|
62
|
+
readonly policy: RouteFragmentCachePolicy
|
|
63
|
+
/** Used only for initial document hydration and post-insertion hydration. */
|
|
64
|
+
readonly fallbackComponent: RenderableComponent
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type RenderableComponent<Props = Record<string, unknown>> = (
|
|
68
|
+
props: Props,
|
|
69
|
+
) => unknown
|
|
70
|
+
|
|
71
|
+
type ContextValue<Value> = Value extends Context<infer Result> ? Result : never
|
|
72
|
+
|
|
73
|
+
const serverEnvironment = typeof document === "undefined"
|
|
74
|
+
|
|
75
|
+
const rootSlot = Symbol.for("flamefront:route-fragment:root")
|
|
76
|
+
const hydrationSlot = Symbol.for("flamefront:route-fragment:hydrate")
|
|
77
|
+
const hostSlot = Symbol.for("flamefront:route-fragment:host")
|
|
78
|
+
const initialRouteSlot = Symbol.for("flamefront:route-fragment:initial")
|
|
79
|
+
const shellRootSlot = Symbol.for("flamefront:shell-outlet:root")
|
|
80
|
+
const shellBridgeSlot = Symbol.for("flamefront:shell-outlet:bridge")
|
|
81
|
+
const shellBridgeComponentSlot = Symbol.for("flamefront:shell-outlet:component")
|
|
82
|
+
const shellRevisionSlot = Symbol.for("flamefront:shell-outlet:revision")
|
|
83
|
+
const shellHostSlot = Symbol.for("flamefront:shell-outlet:host")
|
|
84
|
+
const shellHtmlSlot = Symbol.for("flamefront:shell-outlet:html")
|
|
85
|
+
const shellSetupSlot = Symbol.for("flamefront:shell-outlet:setup")
|
|
86
|
+
const shellUpdateSlot = Symbol.for("flamefront:shell-outlet:update")
|
|
87
|
+
const shellCleanupSlot = Symbol.for("flamefront:shell-outlet:cleanup")
|
|
88
|
+
const shellLocationSlot = Symbol.for("flamefront:shell-outlet:location")
|
|
89
|
+
const shellErrorSlot = Symbol.for("flamefront:shell-outlet:error")
|
|
90
|
+
|
|
91
|
+
/** Server fragment renders omit the selected boundary's outer host element. */
|
|
92
|
+
export const routeFragmentBoundaryTarget = createContext<string | null>(null)
|
|
93
|
+
|
|
94
|
+
/** Server-only HTML for the independently-owned routed outlet. */
|
|
95
|
+
export const routeOutletHtmlContext = createContext<string | null>(null)
|
|
96
|
+
|
|
97
|
+
function boundaryProps(
|
|
98
|
+
metadata: Pick<GeneratedRouteMetadata, "boundary" | "kind">,
|
|
99
|
+
): Record<string, unknown> {
|
|
100
|
+
return {
|
|
101
|
+
"data-flamefront-boundary": metadata.boundary,
|
|
102
|
+
"data-flamefront-boundary-kind": metadata.kind,
|
|
103
|
+
style: "display:contents",
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface NestedOutletRoot {
|
|
108
|
+
unmount(): void
|
|
109
|
+
readonly render?: (
|
|
110
|
+
Component: RenderableComponent,
|
|
111
|
+
props?: Record<string, unknown>,
|
|
112
|
+
) => void
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface ContextBridgeState {
|
|
116
|
+
component: RenderableComponent
|
|
117
|
+
contexts: ContextBridgeContexts
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface ClientOutletProps {
|
|
121
|
+
readonly dangerouslySetInnerHTML: { readonly __html: string }
|
|
122
|
+
readonly suppressHydrationWarning: true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const emptyOutletProps = Object.freeze({})
|
|
126
|
+
const routeOutletHostPropsSlot = Symbol.for("flamefront:shell-outlet:props")
|
|
127
|
+
|
|
128
|
+
interface ContextBridgeContexts {
|
|
129
|
+
readonly dataRouter: ContextValue<typeof UNSAFE_DataRouterContext>
|
|
130
|
+
readonly dataRouterState: ContextValue<typeof UNSAFE_DataRouterStateContext>
|
|
131
|
+
readonly fetchers: ContextValue<typeof UNSAFE_FetchersContext>
|
|
132
|
+
readonly location: ContextValue<typeof UNSAFE_LocationContext>
|
|
133
|
+
readonly navigation: ContextValue<typeof UNSAFE_NavigationContext>
|
|
134
|
+
readonly route: ContextValue<typeof UNSAFE_RouteContext>
|
|
135
|
+
readonly viewTransition: ContextValue<typeof UNSAFE_ViewTransitionContext>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function shellOutletHost(boundary: string): HTMLDivElement | null {
|
|
139
|
+
if (typeof document === "undefined") {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const candidate of document.querySelectorAll<HTMLElement>(
|
|
144
|
+
'[data-flamefront-region="outlet"]',
|
|
145
|
+
)) {
|
|
146
|
+
const owner = candidate.closest("[data-flamefront-boundary]")
|
|
147
|
+
|
|
148
|
+
if (owner?.getAttribute("data-flamefront-boundary") === boundary) {
|
|
149
|
+
return candidate as HTMLDivElement
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function shellOutletHtml(boundary: string): string {
|
|
157
|
+
return shellOutletHost(boundary)?.innerHTML ?? ""
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function shellHydrationBoundary(
|
|
161
|
+
hydration: HydrationMode,
|
|
162
|
+
children: unknown,
|
|
163
|
+
deferredActivation = false,
|
|
164
|
+
): unknown {
|
|
165
|
+
if (hydration === "full") {
|
|
166
|
+
return children
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (hydration === "deferred") {
|
|
170
|
+
// A deferred shell is router-aware but dormant until the first location
|
|
171
|
+
// change; this is intentionally a condition trigger, not idle hydration.
|
|
172
|
+
return <Hydrate when={condition(deferredActivation)}>{children}</Hydrate>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (hydration === "none") {
|
|
176
|
+
return <Hydrate when={never()}>{children}</Hydrate>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
switch (hydration.when) {
|
|
180
|
+
case "idle":
|
|
181
|
+
return (
|
|
182
|
+
<Hydrate when={idle({ timeout: hydration.timeout })}>
|
|
183
|
+
{children}
|
|
184
|
+
</Hydrate>
|
|
185
|
+
)
|
|
186
|
+
case "visible":
|
|
187
|
+
return (
|
|
188
|
+
<Hydrate
|
|
189
|
+
when={visible({
|
|
190
|
+
rootMargin: hydration.rootMargin,
|
|
191
|
+
threshold:
|
|
192
|
+
hydration.threshold === undefined
|
|
193
|
+
? undefined
|
|
194
|
+
: [
|
|
195
|
+
...(Array.isArray(hydration.threshold)
|
|
196
|
+
? hydration.threshold
|
|
197
|
+
: [hydration.threshold]),
|
|
198
|
+
],
|
|
199
|
+
})}
|
|
200
|
+
>
|
|
201
|
+
{children}
|
|
202
|
+
</Hydrate>
|
|
203
|
+
)
|
|
204
|
+
case "interaction":
|
|
205
|
+
return (
|
|
206
|
+
<Hydrate when={interaction({ events: hydration.events })}>
|
|
207
|
+
{children}
|
|
208
|
+
</Hydrate>
|
|
209
|
+
)
|
|
210
|
+
case "media":
|
|
211
|
+
return <Hydrate when={media(hydration.query)}>{children}</Hydrate>
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function RouteOutletHost(props: {
|
|
216
|
+
readonly initialHtml: string
|
|
217
|
+
readonly serverOutlet: unknown
|
|
218
|
+
}): unknown {
|
|
219
|
+
const serverOutletHtml = useContext(routeOutletHtmlContext)
|
|
220
|
+
const attributes = {
|
|
221
|
+
"data-flamefront-region": "outlet",
|
|
222
|
+
"data-flamefront-outlet": "true",
|
|
223
|
+
style: "display:contents",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const clientProps = useMemo<ClientOutletProps | typeof emptyOutletProps>(
|
|
227
|
+
() =>
|
|
228
|
+
!serverEnvironment && props.initialHtml !== ""
|
|
229
|
+
? Object.freeze({
|
|
230
|
+
dangerouslySetInnerHTML: Object.freeze({
|
|
231
|
+
__html: props.initialHtml,
|
|
232
|
+
}),
|
|
233
|
+
suppressHydrationWarning: true,
|
|
234
|
+
})
|
|
235
|
+
: emptyOutletProps,
|
|
236
|
+
[props.initialHtml],
|
|
237
|
+
routeOutletHostPropsSlot,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div {...attributes} {...clientProps}>
|
|
242
|
+
{serverEnvironment ? (
|
|
243
|
+
<div data-flamefront-outlet-root="true">
|
|
244
|
+
<div
|
|
245
|
+
data-flamefront-outlet-content="true"
|
|
246
|
+
{...(serverOutletHtml === null
|
|
247
|
+
? {}
|
|
248
|
+
: { dangerouslySetInnerHTML: { __html: serverOutletHtml } })}
|
|
249
|
+
>
|
|
250
|
+
{serverOutletHtml === null ? props.serverOutlet : null}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
) : null}
|
|
254
|
+
</div>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function createShellBoundary(
|
|
259
|
+
Component: RenderableComponent,
|
|
260
|
+
metadata: Pick<GeneratedRouteMetadata, "boundary" | "kind" | "hydration">,
|
|
261
|
+
): (props: Record<string, unknown>) => unknown {
|
|
262
|
+
return function ShellBoundary(props) {
|
|
263
|
+
const dataRouter = useContext(UNSAFE_DataRouterContext)
|
|
264
|
+
const dataRouterState = useContext(UNSAFE_DataRouterStateContext)
|
|
265
|
+
const fetchers = useContext(UNSAFE_FetchersContext)
|
|
266
|
+
const location = useContext(UNSAFE_LocationContext)
|
|
267
|
+
const navigation = useContext(UNSAFE_NavigationContext)
|
|
268
|
+
const route = useContext(UNSAFE_RouteContext)
|
|
269
|
+
const viewTransition = useContext(UNSAFE_ViewTransitionContext)
|
|
270
|
+
const rootRef = useRef<NestedOutletRoot | null>(null, shellRootSlot)
|
|
271
|
+
const hostRef = useRef<HTMLDivElement | null>(null, shellHostSlot)
|
|
272
|
+
const bridgeStateRef = useRef<ContextBridgeState | null>(
|
|
273
|
+
null,
|
|
274
|
+
shellBridgeSlot,
|
|
275
|
+
)
|
|
276
|
+
const bridgeRef = useRef<RenderableComponent | null>(
|
|
277
|
+
null,
|
|
278
|
+
shellBridgeComponentSlot,
|
|
279
|
+
)
|
|
280
|
+
const revisionRef = useRef(0, shellRevisionSlot)
|
|
281
|
+
const initialHtmlRef = useRef<string | null>(null, shellHtmlSlot)
|
|
282
|
+
const initialLocationRef = useRef<string | null>(null, shellLocationSlot)
|
|
283
|
+
const initialErrorRef = useRef<boolean | null>(null, shellErrorSlot)
|
|
284
|
+
|
|
285
|
+
const currentLocation = location?.location
|
|
286
|
+
const locationIdentity = currentLocation
|
|
287
|
+
? `${currentLocation.pathname}${currentLocation.search}${currentLocation.hash}:${currentLocation.key}`
|
|
288
|
+
: ""
|
|
289
|
+
|
|
290
|
+
if (initialLocationRef.current === null) {
|
|
291
|
+
initialLocationRef.current = locationIdentity
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (initialHtmlRef.current === null) {
|
|
295
|
+
initialHtmlRef.current = shellOutletHtml(metadata.boundary)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (initialErrorRef.current === null) {
|
|
299
|
+
initialErrorRef.current = dataRouterState?.errors != null
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const routerErrors = dataRouterState?.errors
|
|
303
|
+
const routerErrorIdentity = routerErrors
|
|
304
|
+
? Object.keys(routerErrors).join("\u0000")
|
|
305
|
+
: ""
|
|
306
|
+
|
|
307
|
+
if (bridgeStateRef.current === null) {
|
|
308
|
+
const state = {
|
|
309
|
+
component: (() => null) as RenderableComponent,
|
|
310
|
+
contexts: {
|
|
311
|
+
dataRouter,
|
|
312
|
+
dataRouterState,
|
|
313
|
+
fetchers,
|
|
314
|
+
location,
|
|
315
|
+
navigation,
|
|
316
|
+
route,
|
|
317
|
+
viewTransition,
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
state.component = () => state.contexts.route.outlet
|
|
322
|
+
bridgeStateRef.current = state
|
|
323
|
+
} else {
|
|
324
|
+
bridgeStateRef.current.contexts = {
|
|
325
|
+
dataRouter,
|
|
326
|
+
dataRouterState,
|
|
327
|
+
fetchers,
|
|
328
|
+
location,
|
|
329
|
+
navigation,
|
|
330
|
+
route,
|
|
331
|
+
viewTransition,
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
bridgeRef.current ??= createContextBridge(
|
|
336
|
+
bridgeStateRef as {
|
|
337
|
+
current: ContextBridgeState
|
|
338
|
+
},
|
|
339
|
+
)
|
|
340
|
+
revisionRef.current += 1
|
|
341
|
+
const bridge = bridgeRef.current
|
|
342
|
+
const revision = revisionRef.current
|
|
343
|
+
|
|
344
|
+
useLayoutEffect(
|
|
345
|
+
() => {
|
|
346
|
+
if (import.meta.env.SSR) {
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const host = shellOutletHost(metadata.boundary)
|
|
351
|
+
|
|
352
|
+
if (!host) {
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The outer router can render the default error element without
|
|
357
|
+
// Remix's private RouteErrorContext provider. Keep a server-rendered
|
|
358
|
+
// initial error opaque until the router leaves the error location;
|
|
359
|
+
// replacing it from the nested root would turn its payload into null.
|
|
360
|
+
if (
|
|
361
|
+
initialErrorRef.current &&
|
|
362
|
+
initialHtmlRef.current !== "" &&
|
|
363
|
+
rootRef.current === null &&
|
|
364
|
+
routerErrorIdentity !== ""
|
|
365
|
+
) {
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const outletRoot =
|
|
370
|
+
host.querySelector<HTMLDivElement>(
|
|
371
|
+
'[data-flamefront-outlet-root="true"]',
|
|
372
|
+
) ?? host
|
|
373
|
+
const outletContent =
|
|
374
|
+
outletRoot.querySelector<HTMLDivElement>(
|
|
375
|
+
'[data-flamefront-outlet-content="true"]',
|
|
376
|
+
) ?? outletRoot
|
|
377
|
+
|
|
378
|
+
hostRef.current = outletContent
|
|
379
|
+
let active = true
|
|
380
|
+
|
|
381
|
+
void import("./fragment-hydration-client.tsx").then(
|
|
382
|
+
({ renderRouteOutlet }) => {
|
|
383
|
+
if (!active || hostRef.current !== outletContent) {
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (rootRef.current) {
|
|
388
|
+
rootRef.current.render?.(bridge, {
|
|
389
|
+
revision: revisionRef.current,
|
|
390
|
+
})
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
rootRef.current = renderRouteOutlet(
|
|
395
|
+
outletContent,
|
|
396
|
+
bridge,
|
|
397
|
+
outletContent.hasChildNodes() && routerErrors == null,
|
|
398
|
+
)
|
|
399
|
+
},
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return () => {
|
|
403
|
+
active = false
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
[bridge, routerErrorIdentity, routerErrors],
|
|
407
|
+
shellSetupSlot,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
useEffect(
|
|
411
|
+
() => {
|
|
412
|
+
rootRef.current?.render?.(bridge, {
|
|
413
|
+
revision,
|
|
414
|
+
})
|
|
415
|
+
},
|
|
416
|
+
[bridge, revision],
|
|
417
|
+
shellUpdateSlot,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
useLayoutEffect(
|
|
421
|
+
() => () => {
|
|
422
|
+
rootRef.current?.unmount()
|
|
423
|
+
rootRef.current = null
|
|
424
|
+
hostRef.current = null
|
|
425
|
+
},
|
|
426
|
+
[],
|
|
427
|
+
shellCleanupSlot,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
const shellRoute = {
|
|
431
|
+
...route,
|
|
432
|
+
outlet: (
|
|
433
|
+
<RouteOutletHost
|
|
434
|
+
initialHtml={initialHtmlRef.current ?? ""}
|
|
435
|
+
serverOutlet={bridge}
|
|
436
|
+
/>
|
|
437
|
+
),
|
|
438
|
+
}
|
|
439
|
+
const shell = (
|
|
440
|
+
<UNSAFE_RouteContext.Provider value={shellRoute}>
|
|
441
|
+
<Component {...props} />
|
|
442
|
+
</UNSAFE_RouteContext.Provider>
|
|
443
|
+
)
|
|
444
|
+
const body = shellHydrationBoundary(
|
|
445
|
+
metadata.hydration ?? "full",
|
|
446
|
+
shell,
|
|
447
|
+
locationIdentity !== initialLocationRef.current,
|
|
448
|
+
)
|
|
449
|
+
const target = useContext(routeFragmentBoundaryTarget)
|
|
450
|
+
|
|
451
|
+
return target === metadata.boundary ? (
|
|
452
|
+
body
|
|
453
|
+
) : (
|
|
454
|
+
<div {...boundaryProps(metadata)}>{body}</div>
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Add a stable DOM boundary around every generated shell/layout/route node. */
|
|
460
|
+
export function createRouteBoundary(
|
|
461
|
+
Component: RenderableComponent,
|
|
462
|
+
metadata: Pick<GeneratedRouteMetadata, "boundary" | "kind" | "hydration">,
|
|
463
|
+
): (props: Record<string, unknown>) => unknown {
|
|
464
|
+
if (metadata.kind === "shell") {
|
|
465
|
+
return createShellBoundary(Component, metadata)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return (props) => {
|
|
469
|
+
const target = useContext(routeFragmentBoundaryTarget)
|
|
470
|
+
const children = <Component {...props} />
|
|
471
|
+
|
|
472
|
+
return target === metadata.boundary ? (
|
|
473
|
+
children
|
|
474
|
+
) : (
|
|
475
|
+
<div {...boundaryProps(metadata)}>{children}</div>
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function createContextBridge(stateRef: {
|
|
481
|
+
readonly current: ContextBridgeState
|
|
482
|
+
}): (props: Record<string, unknown>) => unknown {
|
|
483
|
+
const DataRouterProvider = UNSAFE_DataRouterContext.Provider
|
|
484
|
+
const DataRouterStateProvider = UNSAFE_DataRouterStateContext.Provider
|
|
485
|
+
const FetchersProvider = UNSAFE_FetchersContext.Provider
|
|
486
|
+
const LocationProvider = UNSAFE_LocationContext.Provider
|
|
487
|
+
const NavigationProvider = UNSAFE_NavigationContext.Provider
|
|
488
|
+
const RouteProvider = UNSAFE_RouteContext.Provider
|
|
489
|
+
const ViewTransitionProvider = UNSAFE_ViewTransitionContext.Provider
|
|
490
|
+
|
|
491
|
+
return () => {
|
|
492
|
+
const { component, contexts } = stateRef.current
|
|
493
|
+
const Component = component
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
<DataRouterProvider value={contexts.dataRouter}>
|
|
497
|
+
<DataRouterStateProvider value={contexts.dataRouterState}>
|
|
498
|
+
<FetchersProvider value={contexts.fetchers}>
|
|
499
|
+
<LocationProvider value={contexts.location}>
|
|
500
|
+
<NavigationProvider value={contexts.navigation}>
|
|
501
|
+
<RouteProvider value={contexts.route}>
|
|
502
|
+
<ViewTransitionProvider value={contexts.viewTransition}>
|
|
503
|
+
<Component />
|
|
504
|
+
</ViewTransitionProvider>
|
|
505
|
+
</RouteProvider>
|
|
506
|
+
</NavigationProvider>
|
|
507
|
+
</LocationProvider>
|
|
508
|
+
</FetchersProvider>
|
|
509
|
+
</DataRouterStateProvider>
|
|
510
|
+
</DataRouterProvider>
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function routeLocationUrl(): string {
|
|
516
|
+
const location = useContext(UNSAFE_LocationContext)
|
|
517
|
+
const currentLocation = location.location
|
|
518
|
+
|
|
519
|
+
return `${currentLocation.pathname}${currentLocation.search}${currentLocation.hash}`
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function unmountNestedRoot(root: { unmount(): void } | null): void {
|
|
523
|
+
root?.unmount()
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function hasServerBoundary(boundary: string): boolean {
|
|
527
|
+
if (typeof document === "undefined") {
|
|
528
|
+
return false
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return Boolean(
|
|
532
|
+
document.querySelector(`[data-flamefront-boundary="${boundary}"]`),
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Route component used by generated browser routes. Navigation renders the
|
|
538
|
+
* fetched artifact into a boundary first; only the later layout effect may
|
|
539
|
+
* hydrate that boundary. The fallback component is used for direct-document
|
|
540
|
+
* hydration when no fragment was fetched by the browser router.
|
|
541
|
+
*/
|
|
542
|
+
export function createRouteFragmentRoute(
|
|
543
|
+
options: RouteFragmentRouteOptions,
|
|
544
|
+
): (props: Record<string, unknown>) => unknown {
|
|
545
|
+
const fallbackComponent = options.fallbackComponent
|
|
546
|
+
const basename = options.routing.basename ?? "/"
|
|
547
|
+
|
|
548
|
+
return function RouteFragmentRoute(props) {
|
|
549
|
+
const FallbackComponent = fallbackComponent
|
|
550
|
+
const routeUrl = routeLocationUrl()
|
|
551
|
+
const loadedArtifact = getRouteFragment(routeUrl, { basename })
|
|
552
|
+
const hostRef = useRef<HTMLDivElement | null>(null, hostSlot)
|
|
553
|
+
const initialServerUrlRef = useRef<string | null>(null, initialRouteSlot)
|
|
554
|
+
const hasServerMarkup = hasServerBoundary(options.metadata.boundary)
|
|
555
|
+
|
|
556
|
+
if (initialServerUrlRef.current === null && hasServerMarkup) {
|
|
557
|
+
initialServerUrlRef.current = routeUrl
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const isInitialServerRoute = initialServerUrlRef.current === routeUrl
|
|
561
|
+
const artifact = isInitialServerRoute ? undefined : loadedArtifact
|
|
562
|
+
const nestedRootRef = useRef<{ unmount(): void } | null>(null, rootSlot)
|
|
563
|
+
const dataRouter = useContext(UNSAFE_DataRouterContext)
|
|
564
|
+
const dataRouterState = useContext(UNSAFE_DataRouterStateContext)
|
|
565
|
+
const fetchers = useContext(UNSAFE_FetchersContext)
|
|
566
|
+
const location = useContext(UNSAFE_LocationContext)
|
|
567
|
+
const navigation = useContext(UNSAFE_NavigationContext)
|
|
568
|
+
const route = useContext(UNSAFE_RouteContext)
|
|
569
|
+
const viewTransition = useContext(UNSAFE_ViewTransitionContext)
|
|
570
|
+
|
|
571
|
+
useLayoutEffect(
|
|
572
|
+
() => {
|
|
573
|
+
if (
|
|
574
|
+
!artifact ||
|
|
575
|
+
!shouldHydrateRouteFragment(options.metadata.hydration)
|
|
576
|
+
) {
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const host = hostRef.current
|
|
581
|
+
|
|
582
|
+
if (!host) {
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let active = true
|
|
587
|
+
const hydratedComponent = fallbackComponent
|
|
588
|
+
const bridge = createContextBridge({
|
|
589
|
+
current: {
|
|
590
|
+
component: hydratedComponent,
|
|
591
|
+
contexts: {
|
|
592
|
+
dataRouter,
|
|
593
|
+
dataRouterState,
|
|
594
|
+
fetchers,
|
|
595
|
+
location,
|
|
596
|
+
navigation,
|
|
597
|
+
route,
|
|
598
|
+
viewTransition,
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
unmountNestedRoot(nestedRootRef.current)
|
|
604
|
+
nestedRootRef.current = null
|
|
605
|
+
|
|
606
|
+
if (!import.meta.env.SSR) {
|
|
607
|
+
void import("./fragment-hydration-client.tsx").then(
|
|
608
|
+
({ hydrateRouteFragment }) => {
|
|
609
|
+
if (!active || hostRef.current !== host) {
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
nestedRootRef.current = hydrateRouteFragment(host, bridge)
|
|
614
|
+
},
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return () => {
|
|
619
|
+
active = false
|
|
620
|
+
unmountNestedRoot(nestedRootRef.current)
|
|
621
|
+
nestedRootRef.current = null
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
[
|
|
625
|
+
artifact,
|
|
626
|
+
dataRouter,
|
|
627
|
+
dataRouterState,
|
|
628
|
+
fetchers,
|
|
629
|
+
location,
|
|
630
|
+
navigation,
|
|
631
|
+
route,
|
|
632
|
+
viewTransition,
|
|
633
|
+
],
|
|
634
|
+
hydrationSlot,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
return (
|
|
638
|
+
<div
|
|
639
|
+
{...boundaryProps(options.metadata)}
|
|
640
|
+
ref={hostRef}
|
|
641
|
+
{...(artifact
|
|
642
|
+
? { dangerouslySetInnerHTML: { __html: artifact.html } }
|
|
643
|
+
: isInitialServerRoute
|
|
644
|
+
? { children: <FallbackComponent {...props} /> }
|
|
645
|
+
: {})}
|
|
646
|
+
/>
|
|
647
|
+
)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export {
|
|
652
|
+
isRouteFragmentRequest,
|
|
653
|
+
stripFlamefrontProtocolParams,
|
|
654
|
+
stripFlamefrontProtocolRequest,
|
|
655
|
+
withRouteFragmentProtocol,
|
|
656
|
+
} from "./fragment-protocol.ts"
|