flamefront 0.0.0 → 0.1.0-alpha.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.
@@ -0,0 +1,260 @@
1
+ import {
2
+ createContext,
3
+ createElement,
4
+ useContext,
5
+ useLayoutEffect,
6
+ useRef,
7
+ } from "octane"
8
+ import {
9
+ UNSAFE_DataRouterContext,
10
+ UNSAFE_DataRouterStateContext,
11
+ UNSAFE_FetchersContext,
12
+ UNSAFE_LocationContext,
13
+ UNSAFE_NavigationContext,
14
+ UNSAFE_RouteContext,
15
+ UNSAFE_ViewTransitionContext,
16
+ useLocation as useRouterLocation,
17
+ } from "@octanejs/remix-router"
18
+ import type { GeneratedRouteMetadata } from "./index.ts"
19
+ import { isStaticFragmentRequest } from "./fragment-protocol.ts"
20
+
21
+ import {
22
+ getStaticFragment,
23
+ shouldHydrateStaticFragment,
24
+ type StaticFragmentArtifact,
25
+ type StaticFragmentRoutingOptions,
26
+ } from "./fragment-client.ts"
27
+
28
+ export {
29
+ assertStaticFragmentArtifact,
30
+ getStaticFragment,
31
+ isStaticFragmentArtifact,
32
+ loadStaticFragment,
33
+ prefetchStaticFragment,
34
+ shouldHydrateStaticFragment,
35
+ staticFragmentProtocol,
36
+ } from "./fragment-client.ts"
37
+ export type {
38
+ StaticFragmentArtifact,
39
+ StaticFragmentBoundary,
40
+ StaticFragmentLoadOptions,
41
+ StaticFragmentRoutingOptions,
42
+ } from "./fragment-client.ts"
43
+
44
+ export interface StaticFragmentRouteOptions {
45
+ readonly metadata: Pick<
46
+ GeneratedRouteMetadata,
47
+ "id" | "boundary" | "kind" | "parent" | "path" | "hydration"
48
+ >
49
+ readonly routing: StaticFragmentRoutingOptions
50
+ /** Used only for initial document hydration and post-insertion hydration. */
51
+ readonly fallbackComponent: unknown
52
+ }
53
+
54
+ const rootSlot = Symbol.for("flamefront:static-fragment:root")
55
+ const hydrationSlot = Symbol.for("flamefront:static-fragment:hydrate")
56
+ const hostSlot = Symbol.for("flamefront:static-fragment:host")
57
+
58
+ /** Server fragment renders omit the selected boundary's outer host element. */
59
+ export const staticFragmentBoundaryTarget = createContext<string | null>(null)
60
+
61
+ function boundaryProps(
62
+ metadata: Pick<GeneratedRouteMetadata, "boundary" | "kind">,
63
+ ): Record<string, unknown> {
64
+ return {
65
+ "data-flamefront-boundary": metadata.boundary,
66
+ "data-flamefront-boundary-kind": metadata.kind,
67
+ style: "display:contents",
68
+ }
69
+ }
70
+
71
+ /** Add a stable DOM boundary around every generated shell/layout/route node. */
72
+ export function createRouteBoundary(
73
+ Component: unknown,
74
+ metadata: Pick<GeneratedRouteMetadata, "boundary" | "kind">,
75
+ ): (props: Record<string, unknown>) => unknown {
76
+ return (props) => {
77
+ const target = useContext(staticFragmentBoundaryTarget)
78
+ const children = createElement(Component as never, props)
79
+
80
+ return target === metadata.boundary
81
+ ? children
82
+ : createElement("div", boundaryProps(metadata), children)
83
+ }
84
+ }
85
+
86
+ function createContextBridge(
87
+ Component: unknown,
88
+ contexts: {
89
+ readonly dataRouter: unknown
90
+ readonly dataRouterState: unknown
91
+ readonly fetchers: unknown
92
+ readonly location: unknown
93
+ readonly navigation: unknown
94
+ readonly route: unknown
95
+ readonly viewTransition: unknown
96
+ },
97
+ ): (props: Record<string, unknown>) => unknown {
98
+ return () =>
99
+ createElement(UNSAFE_DataRouterContext.Provider as never, {
100
+ value: contexts.dataRouter,
101
+ children: createElement(UNSAFE_DataRouterStateContext.Provider as never, {
102
+ value: contexts.dataRouterState,
103
+ children: createElement(UNSAFE_FetchersContext.Provider as never, {
104
+ value: contexts.fetchers,
105
+ children: createElement(UNSAFE_LocationContext.Provider as never, {
106
+ value: contexts.location,
107
+ children: createElement(
108
+ UNSAFE_NavigationContext.Provider as never,
109
+ {
110
+ value: contexts.navigation,
111
+ children: createElement(UNSAFE_RouteContext.Provider as never, {
112
+ value: contexts.route,
113
+ children: createElement(
114
+ UNSAFE_ViewTransitionContext.Provider as never,
115
+ {
116
+ value: contexts.viewTransition,
117
+ children: createElement(Component as never, {}),
118
+ },
119
+ ),
120
+ }),
121
+ },
122
+ ),
123
+ }),
124
+ }),
125
+ }),
126
+ })
127
+ }
128
+
129
+ function routeLocationUrl(): string {
130
+ const location = useRouterLocation()
131
+
132
+ return `${location.pathname}${location.search}${location.hash}`
133
+ }
134
+
135
+ function unmountNestedRoot(root: { unmount(): void } | null): void {
136
+ root?.unmount()
137
+ }
138
+
139
+ function hasServerBoundary(boundary: string): boolean {
140
+ if (typeof document === "undefined") {
141
+ return false
142
+ }
143
+
144
+ return Boolean(
145
+ document.querySelector(`[data-flamefront-boundary="${boundary}"]`),
146
+ )
147
+ }
148
+
149
+ /**
150
+ * Route component used by generated browser routes. Navigation renders the
151
+ * fetched artifact into a boundary first; only the later layout effect may
152
+ * hydrate that boundary. The fallback component is used for direct-document
153
+ * hydration when no fragment was fetched by the browser router.
154
+ */
155
+ export function createStaticFragmentRoute(
156
+ options: StaticFragmentRouteOptions,
157
+ ): (props: Record<string, unknown>) => unknown {
158
+ const fallbackComponent = options.fallbackComponent
159
+ const fallbackBoundary = createRouteBoundary(
160
+ fallbackComponent,
161
+ options.metadata,
162
+ )
163
+ const basename = options.routing.basename ?? "/"
164
+
165
+ return function StaticFragmentRoute(props) {
166
+ const routeUrl = routeLocationUrl()
167
+ const artifact = getStaticFragment(routeUrl, { basename })
168
+ const hostRef = useRef<Element | null>(null, hostSlot)
169
+ const nestedRootRef = useRef<{ unmount(): void } | null>(null, rootSlot)
170
+ const dataRouter = useContext(UNSAFE_DataRouterContext)
171
+ const dataRouterState = useContext(UNSAFE_DataRouterStateContext)
172
+ const fetchers = useContext(UNSAFE_FetchersContext)
173
+ const location = useContext(UNSAFE_LocationContext)
174
+ const navigation = useContext(UNSAFE_NavigationContext)
175
+ const route = useContext(UNSAFE_RouteContext)
176
+ const viewTransition = useContext(UNSAFE_ViewTransitionContext)
177
+
178
+ useLayoutEffect(
179
+ () => {
180
+ if (
181
+ !artifact ||
182
+ !shouldHydrateStaticFragment(options.metadata.hydration)
183
+ ) {
184
+ return
185
+ }
186
+
187
+ const host = hostRef.current
188
+
189
+ if (!host) {
190
+ return
191
+ }
192
+
193
+ let active = true
194
+ const hydratedComponent = fallbackComponent
195
+ const bridge = createContextBridge(hydratedComponent, {
196
+ dataRouter,
197
+ dataRouterState,
198
+ fetchers,
199
+ location,
200
+ navigation,
201
+ route,
202
+ viewTransition,
203
+ })
204
+
205
+ unmountNestedRoot(nestedRootRef.current)
206
+ nestedRootRef.current = null
207
+ void import("octane").then(
208
+ ({ hydrateRoot, setDangerouslySetInnerHTML, setHTML }) => {
209
+ if (!active || hostRef.current !== host) {
210
+ return
211
+ }
212
+
213
+ // The outer document root used dangerouslySetInnerHTML to adopt the
214
+ // static fragment. Release that ownership before the nested root
215
+ // starts reconciling the same children.
216
+ const html = host.innerHTML
217
+
218
+ setDangerouslySetInnerHTML(host, null)
219
+ setHTML(host, html)
220
+ nestedRootRef.current = hydrateRoot(host, createElement(bridge, {}))
221
+ },
222
+ )
223
+
224
+ return () => {
225
+ active = false
226
+ unmountNestedRoot(nestedRootRef.current)
227
+ nestedRootRef.current = null
228
+ }
229
+ },
230
+ [
231
+ artifact,
232
+ dataRouter,
233
+ dataRouterState,
234
+ fetchers,
235
+ location,
236
+ navigation,
237
+ route,
238
+ viewTransition,
239
+ options.metadata.hydration,
240
+ ],
241
+ hydrationSlot,
242
+ )
243
+
244
+ if (!artifact && hasServerBoundary(options.metadata.boundary)) {
245
+ return fallbackBoundary(props)
246
+ }
247
+
248
+ return createElement("div", {
249
+ ...boundaryProps(options.metadata),
250
+ ...(artifact
251
+ ? {
252
+ ref: hostRef,
253
+ dangerouslySetInnerHTML: { __html: artifact.html },
254
+ }
255
+ : {}),
256
+ })
257
+ }
258
+ }
259
+
260
+ export { isStaticFragmentRequest }