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.
@@ -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
- staticFragmentProtocol,
19
- type StaticFragmentArtifact,
20
- type StaticFragmentBoundary,
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<Record<string, string | undefined>>
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: unknown,
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<StaticFragmentArtifact>
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
- const [remix, routerDocument, octane, fragment] = await Promise.all([
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 createStaticFragmentArtifact(
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
- ): StaticFragmentArtifact {
413
+ ): RouteFragmentArtifact {
481
414
  const chain = fragmentMetadataChain(route, metadata)
482
- const boundaries: StaticFragmentBoundary[] = chain.map((item) => {
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: staticFragmentProtocol,
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 = result.context as StaticDocumentContext
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: result.router,
575
+ dataRouter,
572
576
  renderer,
573
577
  context,
574
- rendered: renderer.renderToString(routerDocument, {
575
- router: result.router,
576
- context: result.context,
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<StaticFragmentArtifact> => {
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
- assertRouteMode(route, "static")
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, "static")
644
+ await renderRoute(sanitizedRequest, route.render, route)
638
645
 
639
- return createStaticFragmentArtifact(
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
+ }
@@ -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
- loadStaticFragment,
8
- type StaticFragmentLoadOptions,
9
- type StaticFragmentRoutingOptions,
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<unknown> {
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<unknown> {
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 static fragment and expose only its route data to the router. */
54
- export async function loadStaticRouteFragment(
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
- ): Promise<unknown> {
58
- const fragmentOptions: StaticFragmentLoadOptions = {
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 loadStaticFragment(
69
+ const artifact = await loadFragmentArtifact(
62
70
  request.url,
63
- options satisfies StaticFragmentRoutingOptions,
71
+ options satisfies RouteFragmentRoutingOptions,
64
72
  fragmentOptions,
65
73
  )
66
74
 
67
- return artifact.routeData
75
+ return artifact.routeData as RouteDataForPath<Path>
68
76
  }
@@ -20,7 +20,7 @@ import {
20
20
  type RoutePrefetchCallback,
21
21
  type RoutePrefetchResources,
22
22
  } from "./route-prefetch.ts"
23
- import { prefetchStaticFragment } from "./fragment-client.ts"
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
- staticFragment:
76
- resources.staticFragment ??
77
- ((url, _route, options) => prefetchStaticFragment(url, routing, options)),
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
 
@@ -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: <Data = unknown>(
17
- url: string | URL,
18
- source: RouteDataSource,
19
- options?: RouteDataLoadOptions,
20
- ) => Promise<Data>
21
- readonly prefetch: (
22
- url: string | URL,
23
- source: RouteDataSource,
24
- options?: RouteDataLoadOptions,
25
- ) => Promise<void>
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,