flamefront 0.1.0-alpha.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/README.md +35 -620
- package/package.json +45 -22
- package/src/cli.ts +16 -1
- package/src/entry.ts +49 -0
- package/src/fetch.ts +280 -0
- package/src/fragment-client.ts +127 -87
- package/src/fragment-hydration-client.tsx +81 -0
- package/src/fragment-protocol.ts +3 -3
- package/src/fragment.tsx +656 -0
- package/src/glob.ts +294 -0
- package/src/identifier-prefix.ts +5 -0
- package/src/index.ts +569 -49
- package/src/lifecycle.ts +7 -6
- package/src/octane-client-core.ts +10 -3
- package/src/octane-client.ts +12 -6
- package/src/octane-compiler.d.ts +19 -0
- package/src/octane-default-renderer.tsx +124 -0
- package/src/octane-router-document.ts +19 -2
- package/src/{octane.ts → octane.tsx} +152 -145
- package/src/output.ts +48 -0
- package/src/remix-route-data.ts +28 -20
- package/src/remix-router.ts +10 -4
- package/src/route-data-client.ts +22 -12
- package/src/route-prefetch.ts +19 -12
- package/src/server.ts +122 -24
- package/src/srvx.ts +43 -183
- package/src/typegen.ts +207 -0
- package/src/virtual-remix-routes.d.ts +16 -1
- package/src/vite.ts +364 -27
- package/src/fragment.ts +0 -260
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
AppDefinition,
|
|
3
3
|
GeneratedRouteMetadata,
|
|
4
4
|
RouteDefinition,
|
|
5
|
+
RouteParams,
|
|
5
6
|
} from "./index.ts"
|
|
6
7
|
import type {
|
|
7
8
|
DocumentMode,
|
|
@@ -15,16 +16,22 @@ import type {
|
|
|
15
16
|
} from "./remix-router-core.ts"
|
|
16
17
|
import { stripFlamefrontProtocolRequest } from "./fragment-protocol.ts"
|
|
17
18
|
import {
|
|
18
|
-
|
|
19
|
-
type
|
|
20
|
-
type
|
|
19
|
+
routeFragmentProtocol,
|
|
20
|
+
type RouteFragmentArtifact,
|
|
21
|
+
type RouteFragmentBoundary,
|
|
21
22
|
} from "./fragment-client.ts"
|
|
23
|
+
import {
|
|
24
|
+
outletIdentifierPrefix,
|
|
25
|
+
shellIdentifierPrefix,
|
|
26
|
+
} from "./identifier-prefix.ts"
|
|
22
27
|
|
|
23
28
|
export type { DocumentMode, RenderedDocument } from "./server.ts"
|
|
24
29
|
|
|
25
30
|
export interface RouterDocumentProps {
|
|
26
31
|
readonly router: unknown
|
|
27
32
|
readonly context: unknown
|
|
33
|
+
/** Server-only markup for the independently-owned routed outlet. */
|
|
34
|
+
readonly outletHtml?: string | null
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export type RouterDocument = (props: RouterDocumentProps) => unknown
|
|
@@ -43,7 +50,7 @@ export interface DocumentCompositionContext<
|
|
|
43
50
|
readonly request: Request
|
|
44
51
|
readonly mode: DocumentMode
|
|
45
52
|
readonly route: Route | null
|
|
46
|
-
readonly params: Readonly<
|
|
53
|
+
readonly params: Readonly<RouteParams<Route["path"]>>
|
|
47
54
|
readonly status: number
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -71,14 +78,19 @@ export interface OctaneRenderer {
|
|
|
71
78
|
context: unknown,
|
|
72
79
|
) => unknown
|
|
73
80
|
readonly renderToString: (
|
|
74
|
-
component:
|
|
81
|
+
component: RouterDocument,
|
|
75
82
|
props: RouterDocumentProps,
|
|
83
|
+
options?: { readonly identifierPrefix?: string },
|
|
76
84
|
) => OctaneRenderResult
|
|
77
85
|
/** Render one generated route boundary directly from the static router tree. */
|
|
78
86
|
readonly renderRouteFragment?: (
|
|
79
87
|
router: unknown,
|
|
80
88
|
context: unknown,
|
|
81
89
|
boundary: string,
|
|
90
|
+
options?: {
|
|
91
|
+
readonly identifierPrefix?: string
|
|
92
|
+
readonly includeBoundary?: boolean
|
|
93
|
+
},
|
|
82
94
|
) => OctaneRenderResult
|
|
83
95
|
readonly defaultRouterDocument: RouterDocument
|
|
84
96
|
}
|
|
@@ -113,7 +125,7 @@ export interface OctaneDocuments {
|
|
|
113
125
|
options?: RenderDocumentOptions,
|
|
114
126
|
) => Promise<RenderedDocument>
|
|
115
127
|
readonly loadRouteData: (request: Request) => Promise<Response>
|
|
116
|
-
readonly renderFragment: (request: Request) => Promise<
|
|
128
|
+
readonly renderFragment: (request: Request) => Promise<RouteFragmentArtifact>
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
interface StaticDocumentContext {
|
|
@@ -228,132 +240,12 @@ async function loadDefaultRouter(): Promise<DocumentRouter> {
|
|
|
228
240
|
}
|
|
229
241
|
|
|
230
242
|
async function loadDefaultRenderer(): Promise<OctaneRenderer> {
|
|
231
|
-
|
|
232
|
-
import("@octanejs/remix-router"),
|
|
233
|
-
import("./octane-router-document.ts"),
|
|
234
|
-
import("octane/server"),
|
|
235
|
-
import("./fragment.ts"),
|
|
236
|
-
])
|
|
237
|
-
|
|
238
|
-
const createStaticNavigator = (router: {
|
|
239
|
-
readonly createHref: (to: unknown) => string
|
|
240
|
-
readonly encodeLocation: (to: unknown) => unknown
|
|
241
|
-
}) => ({
|
|
242
|
-
createHref: router.createHref,
|
|
243
|
-
encodeLocation: router.encodeLocation,
|
|
244
|
-
push() {
|
|
245
|
-
throw new Error(
|
|
246
|
-
"Static fragment rendering cannot navigate on the server.",
|
|
247
|
-
)
|
|
248
|
-
},
|
|
249
|
-
replace() {
|
|
250
|
-
throw new Error(
|
|
251
|
-
"Static fragment rendering cannot navigate on the server.",
|
|
252
|
-
)
|
|
253
|
-
},
|
|
254
|
-
go() {
|
|
255
|
-
throw new Error(
|
|
256
|
-
"Static fragment rendering cannot navigate on the server.",
|
|
257
|
-
)
|
|
258
|
-
},
|
|
259
|
-
back() {
|
|
260
|
-
throw new Error(
|
|
261
|
-
"Static fragment rendering cannot navigate on the server.",
|
|
262
|
-
)
|
|
263
|
-
},
|
|
264
|
-
forward() {
|
|
265
|
-
throw new Error(
|
|
266
|
-
"Static fragment rendering cannot navigate on the server.",
|
|
267
|
-
)
|
|
268
|
-
},
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
createStaticRouter: (routes, context) =>
|
|
273
|
-
remix.createStaticRouter(routes as any[], context as any),
|
|
274
|
-
renderToString: (component, props) =>
|
|
275
|
-
octane.renderToString(component as never, props as never),
|
|
276
|
-
renderRouteFragment: (router, context, boundary) => {
|
|
277
|
-
const dataRouter = router as {
|
|
278
|
-
readonly state: {
|
|
279
|
-
readonly location: unknown
|
|
280
|
-
readonly matches: readonly {
|
|
281
|
-
readonly route?: { readonly id?: string }
|
|
282
|
-
}[]
|
|
283
|
-
}
|
|
284
|
-
readonly createHref: (to: unknown) => string
|
|
285
|
-
readonly encodeLocation: (to: unknown) => unknown
|
|
286
|
-
}
|
|
287
|
-
const staticContext = context as { readonly basename?: string }
|
|
288
|
-
const state = dataRouter.state
|
|
289
|
-
const matchIndex = state.matches.findIndex(
|
|
290
|
-
(match) => match.route?.id === boundary,
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
if (matchIndex < 0) {
|
|
294
|
-
throw new Error(
|
|
295
|
-
`No static router match exists for fragment boundary ${JSON.stringify(boundary)}.`,
|
|
296
|
-
)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const navigator = createStaticNavigator(dataRouter)
|
|
300
|
-
const dataRouterContext = {
|
|
301
|
-
router: dataRouter,
|
|
302
|
-
navigator,
|
|
303
|
-
static: true,
|
|
304
|
-
staticContext: context,
|
|
305
|
-
basename: staticContext.basename ?? "/",
|
|
306
|
-
}
|
|
307
|
-
const fragmentTree = remix.renderMatches(
|
|
308
|
-
state.matches.slice(matchIndex) as never,
|
|
309
|
-
)
|
|
310
|
-
const fragmentRoot = octane.createElement(
|
|
311
|
-
remix.UNSAFE_DataRouterContext.Provider as never,
|
|
312
|
-
{
|
|
313
|
-
value: dataRouterContext,
|
|
314
|
-
children: octane.createElement(
|
|
315
|
-
remix.UNSAFE_DataRouterStateContext.Provider as never,
|
|
316
|
-
{
|
|
317
|
-
value: state,
|
|
318
|
-
children: octane.createElement(
|
|
319
|
-
remix.UNSAFE_FetchersContext.Provider as never,
|
|
320
|
-
{
|
|
321
|
-
value: new Map(),
|
|
322
|
-
children: octane.createElement(
|
|
323
|
-
remix.UNSAFE_ViewTransitionContext.Provider as never,
|
|
324
|
-
{
|
|
325
|
-
value: { isTransitioning: false },
|
|
326
|
-
children: octane.createElement(
|
|
327
|
-
remix.StaticRouter as never,
|
|
328
|
-
{
|
|
329
|
-
basename: staticContext.basename ?? "/",
|
|
330
|
-
location: state.location,
|
|
331
|
-
children: octane.createElement(
|
|
332
|
-
fragment.staticFragmentBoundaryTarget
|
|
333
|
-
.Provider as never,
|
|
334
|
-
{ value: boundary, children: fragmentTree },
|
|
335
|
-
),
|
|
336
|
-
},
|
|
337
|
-
),
|
|
338
|
-
},
|
|
339
|
-
),
|
|
340
|
-
},
|
|
341
|
-
),
|
|
342
|
-
},
|
|
343
|
-
),
|
|
344
|
-
},
|
|
345
|
-
)
|
|
346
|
-
const FragmentRoot = () => fragmentRoot
|
|
347
|
-
|
|
348
|
-
return octane.renderToString(FragmentRoot, {})
|
|
349
|
-
},
|
|
350
|
-
defaultRouterDocument: routerDocument.RouterDocument,
|
|
351
|
-
}
|
|
243
|
+
return (await import("./octane-default-renderer.tsx")).defaultOctaneRenderer
|
|
352
244
|
}
|
|
353
245
|
|
|
354
|
-
function createShellRouter(
|
|
246
|
+
function createShellRouter<Route extends RouteDefinition>(
|
|
355
247
|
request: Request,
|
|
356
|
-
app: AppDefinition
|
|
248
|
+
app: AppDefinition<Route>,
|
|
357
249
|
routeGraph: readonly unknown[],
|
|
358
250
|
renderer: OctaneRenderer,
|
|
359
251
|
): { readonly context: StaticDocumentContext; readonly router: unknown } {
|
|
@@ -432,6 +324,47 @@ function routeData(context: StaticDocumentContext): unknown {
|
|
|
432
324
|
return routeId ? (context.loaderData?.[routeId] ?? null) : null
|
|
433
325
|
}
|
|
434
326
|
|
|
327
|
+
function remapShellRouteError(
|
|
328
|
+
route: RouteDefinition | null,
|
|
329
|
+
context: StaticDocumentContext,
|
|
330
|
+
metadata: readonly GeneratedRouteMetadata[] | undefined,
|
|
331
|
+
): StaticDocumentContext {
|
|
332
|
+
if (!route || !context.errors) {
|
|
333
|
+
return context
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const chain = fragmentMetadataChain(route, metadata)
|
|
337
|
+
const shell = chain.find((item) => item.kind === "shell")
|
|
338
|
+
const outlet = shell ? chain[chain.indexOf(shell) + 1] : undefined
|
|
339
|
+
|
|
340
|
+
if (!shell || !outlet || !context.errors[shell.id]) {
|
|
341
|
+
return context
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const errors = { ...context.errors }
|
|
345
|
+
const shellError = errors[shell.id]
|
|
346
|
+
|
|
347
|
+
delete errors[shell.id]
|
|
348
|
+
if (errors[outlet.id] === undefined) {
|
|
349
|
+
errors[outlet.id] = shellError
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
...context,
|
|
354
|
+
errors,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function routerRoutes(value: unknown): readonly unknown[] | undefined {
|
|
359
|
+
if (!value || typeof value !== "object") {
|
|
360
|
+
return undefined
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const routes = (value as { readonly routes?: unknown }).routes
|
|
364
|
+
|
|
365
|
+
return Array.isArray(routes) ? routes : undefined
|
|
366
|
+
}
|
|
367
|
+
|
|
435
368
|
function fragmentMetadataChain(
|
|
436
369
|
route: RouteDefinition,
|
|
437
370
|
metadata: readonly GeneratedRouteMetadata[] | undefined,
|
|
@@ -470,20 +403,21 @@ function fragmentMetadataChain(
|
|
|
470
403
|
return chain
|
|
471
404
|
}
|
|
472
405
|
|
|
473
|
-
function
|
|
406
|
+
function createRouteFragmentArtifact(
|
|
474
407
|
route: RouteDefinition,
|
|
475
408
|
context: StaticDocumentContext,
|
|
476
409
|
router: unknown,
|
|
477
410
|
renderer: OctaneRenderer,
|
|
478
411
|
fallbackBody: string,
|
|
479
412
|
metadata: readonly GeneratedRouteMetadata[] | undefined,
|
|
480
|
-
):
|
|
413
|
+
): RouteFragmentArtifact {
|
|
481
414
|
const chain = fragmentMetadataChain(route, metadata)
|
|
482
|
-
const boundaries:
|
|
415
|
+
const boundaries: RouteFragmentBoundary[] = chain.map((item) => {
|
|
483
416
|
const rendered = renderer.renderRouteFragment?.(
|
|
484
417
|
router,
|
|
485
418
|
context,
|
|
486
419
|
item.boundary,
|
|
420
|
+
{ identifierPrefix: outletIdentifierPrefix },
|
|
487
421
|
)
|
|
488
422
|
|
|
489
423
|
return {
|
|
@@ -497,7 +431,7 @@ function createStaticFragmentArtifact(
|
|
|
497
431
|
const fragmentHtml = boundaries.at(-1)?.html || fallbackBody
|
|
498
432
|
|
|
499
433
|
return {
|
|
500
|
-
protocol:
|
|
434
|
+
protocol: routeFragmentProtocol,
|
|
501
435
|
route: route.path,
|
|
502
436
|
boundary: chain.at(-1)?.boundary ?? route.entry,
|
|
503
437
|
html: fragmentHtml,
|
|
@@ -508,6 +442,48 @@ function createStaticFragmentArtifact(
|
|
|
508
442
|
}
|
|
509
443
|
}
|
|
510
444
|
|
|
445
|
+
function appendCss(documentCss: string, outletCss: string | undefined): string {
|
|
446
|
+
if (!outletCss || outletCss === documentCss) {
|
|
447
|
+
return documentCss
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const tags = new Set(
|
|
451
|
+
`${documentCss}${outletCss}`.match(/<style\b[^>]*>[\s\S]*?<\/style>/g) ??
|
|
452
|
+
[],
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
return tags.size === 0 ? `${documentCss}${outletCss}` : [...tags].join("")
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function renderDocumentOutlet(
|
|
459
|
+
route: RouteDefinition | null,
|
|
460
|
+
context: StaticDocumentContext,
|
|
461
|
+
router: unknown,
|
|
462
|
+
renderer: OctaneRenderer,
|
|
463
|
+
metadata: readonly GeneratedRouteMetadata[] | undefined,
|
|
464
|
+
): { readonly html: string; readonly css: string } | undefined {
|
|
465
|
+
if (
|
|
466
|
+
!route ||
|
|
467
|
+
(route.render !== "server" && route.render !== "static") ||
|
|
468
|
+
!renderer.renderRouteFragment
|
|
469
|
+
) {
|
|
470
|
+
return undefined
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const chain = fragmentMetadataChain(route, metadata)
|
|
474
|
+
const shellIndex = chain.findIndex((item) => item.kind === "shell")
|
|
475
|
+
const outlet = shellIndex < 0 ? undefined : chain[shellIndex + 1]
|
|
476
|
+
|
|
477
|
+
if (!outlet) {
|
|
478
|
+
return undefined
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return renderer.renderRouteFragment(router, context, outlet.boundary, {
|
|
482
|
+
identifierPrefix: outletIdentifierPrefix,
|
|
483
|
+
includeBoundary: true,
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
511
487
|
export function createOctaneDocuments<
|
|
512
488
|
Context = unknown,
|
|
513
489
|
Route extends RouteDefinition = RouteDefinition,
|
|
@@ -534,6 +510,8 @@ export function createOctaneDocuments<
|
|
|
534
510
|
const renderRoute = async (
|
|
535
511
|
request: Request,
|
|
536
512
|
mode: DocumentMode,
|
|
513
|
+
route: RouteDefinition | null,
|
|
514
|
+
renderOutlet = false,
|
|
537
515
|
): Promise<{
|
|
538
516
|
readonly router: DocumentRouter
|
|
539
517
|
readonly dataRouter: unknown
|
|
@@ -564,17 +542,43 @@ export function createOctaneDocuments<
|
|
|
564
542
|
throw result
|
|
565
543
|
}
|
|
566
544
|
|
|
567
|
-
const context =
|
|
545
|
+
const context = remapShellRouteError(
|
|
546
|
+
route,
|
|
547
|
+
result.context as StaticDocumentContext,
|
|
548
|
+
router.routeMetadata,
|
|
549
|
+
)
|
|
550
|
+
const dataRouter =
|
|
551
|
+
context === result.context
|
|
552
|
+
? result.router
|
|
553
|
+
: renderer.createStaticRouter(
|
|
554
|
+
routerRoutes(result.router) ?? router.routes,
|
|
555
|
+
context,
|
|
556
|
+
)
|
|
557
|
+
const outlet = renderDocumentOutlet(
|
|
558
|
+
renderOutlet ? route : null,
|
|
559
|
+
context,
|
|
560
|
+
dataRouter,
|
|
561
|
+
renderer,
|
|
562
|
+
router.routeMetadata,
|
|
563
|
+
)
|
|
564
|
+
const documentProps: RouterDocumentProps = {
|
|
565
|
+
router: dataRouter,
|
|
566
|
+
context,
|
|
567
|
+
...(outlet ? { outletHtml: outlet.html } : {}),
|
|
568
|
+
}
|
|
569
|
+
const rendered = renderer.renderToString(routerDocument, documentProps, {
|
|
570
|
+
identifierPrefix: shellIdentifierPrefix,
|
|
571
|
+
})
|
|
568
572
|
|
|
569
573
|
return {
|
|
570
574
|
router,
|
|
571
|
-
dataRouter
|
|
575
|
+
dataRouter,
|
|
572
576
|
renderer,
|
|
573
577
|
context,
|
|
574
|
-
rendered:
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
+
rendered: {
|
|
579
|
+
...rendered,
|
|
580
|
+
css: appendCss(rendered.css, outlet?.css),
|
|
581
|
+
},
|
|
578
582
|
}
|
|
579
583
|
}
|
|
580
584
|
|
|
@@ -592,13 +596,17 @@ export function createOctaneDocuments<
|
|
|
592
596
|
const { context: staticContext, rendered } = await renderRoute(
|
|
593
597
|
sanitizedRequest,
|
|
594
598
|
mode,
|
|
599
|
+
route,
|
|
600
|
+
true,
|
|
595
601
|
)
|
|
596
602
|
const status = staticContext.statusCode ?? 200
|
|
597
603
|
const compositionContext: DocumentCompositionContext<Route> = {
|
|
598
604
|
request: sanitizedRequest,
|
|
599
605
|
mode,
|
|
600
606
|
route,
|
|
601
|
-
params: routeMatch?.params ?? {}
|
|
607
|
+
params: (routeMatch?.params ?? {}) as Readonly<
|
|
608
|
+
RouteParams<Route["path"]>
|
|
609
|
+
>,
|
|
602
610
|
status,
|
|
603
611
|
}
|
|
604
612
|
const parts: DocumentParts = {
|
|
@@ -623,20 +631,19 @@ export function createOctaneDocuments<
|
|
|
623
631
|
|
|
624
632
|
const renderFragment = async (
|
|
625
633
|
request: Request,
|
|
626
|
-
): Promise<
|
|
634
|
+
): Promise<RouteFragmentArtifact> => {
|
|
627
635
|
const sanitizedRequest = stripFlamefrontProtocolRequest(request)
|
|
628
636
|
const routeMatch = options.app.match(sanitizedRequest.url)
|
|
629
637
|
const route = routeMatch?.data ?? null
|
|
630
638
|
|
|
631
|
-
|
|
632
|
-
if (!route) {
|
|
639
|
+
if (!route || route.render === "client") {
|
|
633
640
|
throw new Response("Not found", { status: 404 })
|
|
634
641
|
}
|
|
635
642
|
|
|
636
643
|
const { router, dataRouter, renderer, rendered, context } =
|
|
637
|
-
await renderRoute(sanitizedRequest,
|
|
644
|
+
await renderRoute(sanitizedRequest, route.render, route)
|
|
638
645
|
|
|
639
|
-
return
|
|
646
|
+
return createRouteFragmentArtifact(
|
|
640
647
|
route,
|
|
641
648
|
context,
|
|
642
649
|
dataRouter,
|
package/src/output.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type FlamefrontTarget = "node" | "deno" | "bun"
|
|
2
|
+
export type FlamefrontAdapter = "fetch" | "srvx"
|
|
3
|
+
|
|
4
|
+
export interface FlamefrontOutputOptions {
|
|
5
|
+
/** Select the runtime family used by the built-in srvx adapter. */
|
|
6
|
+
readonly target?: FlamefrontTarget
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ResolvedFlamefrontOutput =
|
|
10
|
+
| {
|
|
11
|
+
readonly adapter: "fetch"
|
|
12
|
+
readonly target?: undefined
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
readonly adapter: "srvx"
|
|
16
|
+
readonly target: FlamefrontTarget
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const targets: ReadonlySet<FlamefrontTarget> = new Set(["node", "deno", "bun"])
|
|
20
|
+
|
|
21
|
+
/** Resolve the public output options into the generated server entry kind. */
|
|
22
|
+
export function resolveFlamefrontOutput(
|
|
23
|
+
options: FlamefrontOutputOptions = {},
|
|
24
|
+
): ResolvedFlamefrontOutput {
|
|
25
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
26
|
+
throw new TypeError("flamefront output options must be an object.")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.target !== undefined && !targets.has(options.target)) {
|
|
30
|
+
throw new TypeError(
|
|
31
|
+
`flamefront target must be one of "node", "deno", or "bun"; received ${JSON.stringify(options.target)}.`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const requestedAdapter = (options as { readonly adapter?: unknown }).adapter
|
|
36
|
+
|
|
37
|
+
if (requestedAdapter !== undefined) {
|
|
38
|
+
throw new TypeError(
|
|
39
|
+
`flamefront adapter is not supported; omit "adapter" and use "target" for srvx or the default Web Fetch entry.`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (options.target !== undefined) {
|
|
44
|
+
return Object.freeze({ adapter: "srvx", target: options.target })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Object.freeze({ adapter: "fetch" })
|
|
48
|
+
}
|
package/src/remix-route-data.ts
CHANGED
|
@@ -3,10 +3,12 @@ import {
|
|
|
3
3
|
type RouteDataLoadOptions,
|
|
4
4
|
type RouteDataRoutingOptions,
|
|
5
5
|
} from "./route-data-client.ts"
|
|
6
|
+
import type { RouteDataForPath } from "./index.ts"
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
type
|
|
8
|
+
loadRouteFragment as loadFragmentArtifact,
|
|
9
|
+
type RouteFragmentCachePolicy,
|
|
10
|
+
type RouteFragmentLoadOptions,
|
|
11
|
+
type RouteFragmentRoutingOptions,
|
|
10
12
|
} from "./fragment-client.ts"
|
|
11
13
|
|
|
12
14
|
export {
|
|
@@ -17,7 +19,7 @@ export {
|
|
|
17
19
|
type RouteDataSource,
|
|
18
20
|
} from "./route-data-client.ts"
|
|
19
21
|
|
|
20
|
-
export interface ClientLoaderArgs {
|
|
22
|
+
export interface ClientLoaderArgs<_Path extends string = string> {
|
|
21
23
|
readonly request: Request
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -31,38 +33,44 @@ function client(options: RouteDataOptions) {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/** Load route data through Flamefront's server endpoint during browser navigation. */
|
|
34
|
-
export async function loadRouteData(
|
|
35
|
-
{ request }: ClientLoaderArgs
|
|
36
|
+
export async function loadRouteData<const Path extends string = string>(
|
|
37
|
+
{ request }: ClientLoaderArgs<Path>,
|
|
36
38
|
options: RouteDataOptions = {},
|
|
37
|
-
): Promise<
|
|
39
|
+
): Promise<RouteDataForPath<Path>> {
|
|
38
40
|
const loadOptions: RouteDataLoadOptions = { signal: request.signal }
|
|
39
41
|
|
|
40
|
-
return client(options).load(request.url, "live", loadOptions)
|
|
42
|
+
return client(options).load(request.url, "live", loadOptions) as Promise<
|
|
43
|
+
RouteDataForPath<Path>
|
|
44
|
+
>
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/** Load a build-time static route artifact during browser navigation. */
|
|
44
|
-
export async function loadStaticRouteData(
|
|
45
|
-
{ request }: ClientLoaderArgs
|
|
48
|
+
export async function loadStaticRouteData<const Path extends string = string>(
|
|
49
|
+
{ request }: ClientLoaderArgs<Path>,
|
|
46
50
|
options: RouteDataOptions = {},
|
|
47
|
-
): Promise<
|
|
51
|
+
): Promise<RouteDataForPath<Path>> {
|
|
48
52
|
const loadOptions: RouteDataLoadOptions = { signal: request.signal }
|
|
49
53
|
|
|
50
|
-
return client(options).load(request.url, "static", loadOptions)
|
|
54
|
+
return client(options).load(request.url, "static", loadOptions) as Promise<
|
|
55
|
+
RouteDataForPath<Path>
|
|
56
|
+
>
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
/** Load a
|
|
54
|
-
export async function
|
|
55
|
-
{ request }: ClientLoaderArgs
|
|
59
|
+
/** Load a route fragment and expose only its route data to the router. */
|
|
60
|
+
export async function loadRouteFragment<const Path extends string = string>(
|
|
61
|
+
{ request }: ClientLoaderArgs<Path>,
|
|
56
62
|
options: RouteDataOptions = {},
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
policy: RouteFragmentCachePolicy = "static",
|
|
64
|
+
): Promise<RouteDataForPath<Path>> {
|
|
65
|
+
const fragmentOptions: RouteFragmentLoadOptions = {
|
|
66
|
+
policy,
|
|
59
67
|
signal: request.signal,
|
|
60
68
|
}
|
|
61
|
-
const artifact = await
|
|
69
|
+
const artifact = await loadFragmentArtifact(
|
|
62
70
|
request.url,
|
|
63
|
-
options satisfies
|
|
71
|
+
options satisfies RouteFragmentRoutingOptions,
|
|
64
72
|
fragmentOptions,
|
|
65
73
|
)
|
|
66
74
|
|
|
67
|
-
return artifact.routeData
|
|
75
|
+
return artifact.routeData as RouteDataForPath<Path>
|
|
68
76
|
}
|
package/src/remix-router.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
type RoutePrefetchCallback,
|
|
21
21
|
type RoutePrefetchResources,
|
|
22
22
|
} from "./route-prefetch.ts"
|
|
23
|
-
import {
|
|
23
|
+
import { prefetchRouteFragment } from "./fragment-client.ts"
|
|
24
24
|
import {
|
|
25
25
|
preloadRoute as preloadGeneratedRoute,
|
|
26
26
|
RouterDocument,
|
|
@@ -72,9 +72,15 @@ function withDefaultPrefetchResources<
|
|
|
72
72
|
): RoutePrefetchResources<Route> {
|
|
73
73
|
return {
|
|
74
74
|
...resources,
|
|
75
|
-
|
|
76
|
-
resources.
|
|
77
|
-
((url,
|
|
75
|
+
routeFragment:
|
|
76
|
+
resources.routeFragment ??
|
|
77
|
+
((url, route, options) => {
|
|
78
|
+
if (route.render === "client") {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return prefetchRouteFragment(url, route.render, routing, options)
|
|
83
|
+
}),
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
|
package/src/route-data-client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stripFlamefrontProtocolParams } from "./fragment-protocol.ts"
|
|
2
|
+
import type { RouteDataForPath } from "./index.ts"
|
|
2
3
|
|
|
3
4
|
export type RouteDataSource = "live" | "static"
|
|
4
5
|
|
|
@@ -13,16 +14,25 @@ export interface RouteDataLoadOptions {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface RouteDataClient {
|
|
16
|
-
readonly load:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
readonly load: {
|
|
18
|
+
<const Url extends string>(
|
|
19
|
+
url: Url,
|
|
20
|
+
source: RouteDataSource,
|
|
21
|
+
options?: RouteDataLoadOptions,
|
|
22
|
+
): Promise<RouteDataForPath<Url>>
|
|
23
|
+
<Data = unknown>(
|
|
24
|
+
url: string | URL,
|
|
25
|
+
source: RouteDataSource,
|
|
26
|
+
options?: RouteDataLoadOptions,
|
|
27
|
+
): Promise<Data>
|
|
28
|
+
}
|
|
29
|
+
readonly prefetch: {
|
|
30
|
+
(
|
|
31
|
+
url: string | URL,
|
|
32
|
+
source: RouteDataSource,
|
|
33
|
+
options?: RouteDataLoadOptions,
|
|
34
|
+
): Promise<void>
|
|
35
|
+
}
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
const defaultRouting = Object.freeze({
|
|
@@ -140,7 +150,7 @@ function createIsolatedRouteDataClient(routing: {
|
|
|
140
150
|
}): RouteDataClient {
|
|
141
151
|
const cache = new Map<string, Promise<unknown>>()
|
|
142
152
|
|
|
143
|
-
const load = <Data = unknown>(
|
|
153
|
+
const load = (<Data = unknown>(
|
|
144
154
|
url: string | URL,
|
|
145
155
|
source: RouteDataSource,
|
|
146
156
|
options: RouteDataLoadOptions = {},
|
|
@@ -191,7 +201,7 @@ function createIsolatedRouteDataClient(routing: {
|
|
|
191
201
|
}
|
|
192
202
|
})
|
|
193
203
|
return abortable(pending, options.signal)
|
|
194
|
-
}
|
|
204
|
+
}) as RouteDataClient["load"]
|
|
195
205
|
|
|
196
206
|
return {
|
|
197
207
|
load,
|