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/src/octane.tsx ADDED
@@ -0,0 +1,661 @@
1
+ import type {
2
+ AppDefinition,
3
+ GeneratedRouteMetadata,
4
+ RouteDefinition,
5
+ RouteParams,
6
+ } from "./index.ts"
7
+ import type {
8
+ DocumentMode,
9
+ RouteRuntime,
10
+ RenderedDocument,
11
+ RouteRuntimeContextOptions,
12
+ } from "./server.ts"
13
+ import type {
14
+ ServerRouterOptions,
15
+ ServerRouterResult,
16
+ } from "./remix-router-core.ts"
17
+ import { stripFlamefrontProtocolRequest } from "./fragment-protocol.ts"
18
+ import {
19
+ routeFragmentProtocol,
20
+ type RouteFragmentArtifact,
21
+ type RouteFragmentBoundary,
22
+ } from "./fragment-client.ts"
23
+ import {
24
+ outletIdentifierPrefix,
25
+ shellIdentifierPrefix,
26
+ } from "./identifier-prefix.ts"
27
+
28
+ export type { DocumentMode, RenderedDocument } from "./server.ts"
29
+
30
+ export interface RouterDocumentProps {
31
+ readonly router: unknown
32
+ readonly context: unknown
33
+ /** Server-only markup for the independently-owned routed outlet. */
34
+ readonly outletHtml?: string | null
35
+ }
36
+
37
+ export type RouterDocument = (props: RouterDocumentProps) => unknown
38
+
39
+ /** Framework-rendered document pieces available to the app composer. */
40
+ export interface DocumentParts {
41
+ readonly template: string
42
+ readonly body: string
43
+ readonly css: string
44
+ readonly hydrationScript: string
45
+ }
46
+
47
+ export interface DocumentCompositionContext<
48
+ Route extends RouteDefinition = RouteDefinition,
49
+ > {
50
+ readonly request: Request
51
+ readonly mode: DocumentMode
52
+ readonly route: Route | null
53
+ readonly params: Readonly<RouteParams<Route["path"]>>
54
+ readonly status: number
55
+ }
56
+
57
+ export type ComposeDocument<Route extends RouteDefinition = RouteDefinition> = (
58
+ parts: DocumentParts,
59
+ context: DocumentCompositionContext<Route>,
60
+ ) => string | Promise<string>
61
+
62
+ export interface RenderDocumentOptions {
63
+ readonly mode?: DocumentMode
64
+ }
65
+
66
+ export interface OctaneRenderResult {
67
+ readonly html: string
68
+ readonly css: string
69
+ }
70
+
71
+ /**
72
+ * The renderer is injectable so document behavior can be tested without
73
+ * loading the compiler-only Octane and Remix Router source modules in Node.
74
+ */
75
+ export interface OctaneRenderer {
76
+ readonly createStaticRouter: (
77
+ routes: readonly unknown[],
78
+ context: unknown,
79
+ ) => unknown
80
+ readonly renderToString: (
81
+ component: RouterDocument,
82
+ props: RouterDocumentProps,
83
+ options?: { readonly identifierPrefix?: string },
84
+ ) => OctaneRenderResult
85
+ /** Render one generated route boundary directly from the static router tree. */
86
+ readonly renderRouteFragment?: (
87
+ router: unknown,
88
+ context: unknown,
89
+ boundary: string,
90
+ options?: {
91
+ readonly identifierPrefix?: string
92
+ readonly includeBoundary?: boolean
93
+ },
94
+ ) => OctaneRenderResult
95
+ readonly defaultRouterDocument: RouterDocument
96
+ }
97
+
98
+ export interface DocumentRouter {
99
+ readonly routes: readonly unknown[]
100
+ readonly routeMetadata?: readonly GeneratedRouteMetadata[]
101
+ readonly createServerRouter: (
102
+ request: Request,
103
+ options?: ServerRouterOptions,
104
+ ) => Promise<Response | ServerRouterResult<unknown, unknown>>
105
+ }
106
+
107
+ export interface OctaneDocumentsOptions<
108
+ Context = unknown,
109
+ Route extends RouteDefinition = RouteDefinition,
110
+ > {
111
+ readonly app: AppDefinition<Route>
112
+ readonly runtime: RouteRuntime<Context, Route>
113
+ /** Wrap the framework's default RouterProvider with app providers. */
114
+ readonly routerDocument?: RouterDocument
115
+ /** Control HTML placement while retaining framework protocol pieces. */
116
+ readonly composeDocument?: ComposeDocument<Route>
117
+ readonly router?: DocumentRouter
118
+ readonly renderer?: OctaneRenderer
119
+ }
120
+
121
+ export interface OctaneDocuments {
122
+ readonly renderDocument: (
123
+ template: string,
124
+ request: Request,
125
+ options?: RenderDocumentOptions,
126
+ ) => Promise<RenderedDocument>
127
+ readonly loadRouteData: (request: Request) => Promise<Response>
128
+ readonly renderFragment: (request: Request) => Promise<RouteFragmentArtifact>
129
+ }
130
+
131
+ interface StaticDocumentContext {
132
+ readonly loaderData?: Record<string, unknown>
133
+ readonly actionData?: Record<string, unknown> | null
134
+ readonly errors?: Record<string, unknown> | null
135
+ readonly statusCode?: number
136
+ readonly matches?: readonly {
137
+ readonly params?: Readonly<Record<string, string | undefined>>
138
+ readonly pathname?: string
139
+ readonly pathnameBase?: string
140
+ readonly route?: { readonly id?: string }
141
+ }[]
142
+ }
143
+
144
+ function isRouteErrorResponse(
145
+ value: unknown,
146
+ ): value is Record<string, unknown> {
147
+ return Boolean(
148
+ value &&
149
+ typeof value === "object" &&
150
+ typeof (value as { status?: unknown }).status === "number" &&
151
+ typeof (value as { statusText?: unknown }).statusText === "string" &&
152
+ typeof (value as { internal?: unknown }).internal === "boolean" &&
153
+ "data" in value,
154
+ )
155
+ }
156
+
157
+ export function serializeErrors(
158
+ errors: Record<string, unknown> | null | undefined,
159
+ ): Record<string, unknown> | null {
160
+ if (!errors) {
161
+ return null
162
+ }
163
+
164
+ const serialized: Record<string, unknown> = {}
165
+
166
+ for (const [key, value] of Object.entries(errors)) {
167
+ if (isRouteErrorResponse(value)) {
168
+ serialized[key] = { ...value, __type: "RouteErrorResponse" }
169
+ } else if (value instanceof Error) {
170
+ serialized[key] = {
171
+ message: value.message,
172
+ __type: "Error",
173
+ ...(value.name !== "Error" ? { __subType: value.name } : {}),
174
+ }
175
+ } else {
176
+ serialized[key] = value
177
+ }
178
+ }
179
+
180
+ return serialized
181
+ }
182
+
183
+ export const staticRouterHydrationScriptId =
184
+ "flamefront-static-router-hydration"
185
+
186
+ export function staticRouterHydrationScript(
187
+ context: StaticDocumentContext,
188
+ ): string {
189
+ const data = JSON.stringify({
190
+ loaderData: context.loaderData,
191
+ actionData: context.actionData,
192
+ errors: serializeErrors(context.errors),
193
+ })
194
+ const escaped = JSON.stringify(data).replace(
195
+ /[&><\u2028\u2029]/g,
196
+ (character) => {
197
+ const escapes: Record<string, string> = {
198
+ "&": "\\u0026",
199
+ ">": "\\u003e",
200
+ "<": "\\u003c",
201
+ "
": "\\u2028",
202
+ "
": "\\u2029",
203
+ }
204
+
205
+ return escapes[character] ?? character
206
+ },
207
+ )
208
+
209
+ return `<script id="${staticRouterHydrationScriptId}">window.__staticRouterHydrationData = JSON.parse(${escaped});</script>`
210
+ }
211
+
212
+ export function composeDefaultDocument(
213
+ template: string,
214
+ body: string,
215
+ css: string,
216
+ hydrationScript: string,
217
+ ): string {
218
+ const root = '<div id="root"></div>'
219
+
220
+ if (!template.includes(root)) {
221
+ throw new Error(
222
+ 'The HTML shell must contain an empty <div id="root"></div>.',
223
+ )
224
+ }
225
+
226
+ return template
227
+ .replace(root, `<div id="root">${body}</div>`)
228
+ .replace("</head>", `${css}</head>`)
229
+ .replace("</body>", `${hydrationScript}</body>`)
230
+ }
231
+
232
+ async function loadDefaultRouter(): Promise<DocumentRouter> {
233
+ const module = await import("./remix-router.ts")
234
+
235
+ return {
236
+ routes: module.routes,
237
+ routeMetadata: module.routeMetadata,
238
+ createServerRouter: module.createServerRouter,
239
+ }
240
+ }
241
+
242
+ async function loadDefaultRenderer(): Promise<OctaneRenderer> {
243
+ return (await import("./octane-default-renderer.tsx")).defaultOctaneRenderer
244
+ }
245
+
246
+ function createShellRouter<Route extends RouteDefinition>(
247
+ request: Request,
248
+ app: AppDefinition<Route>,
249
+ routeGraph: readonly unknown[],
250
+ renderer: OctaneRenderer,
251
+ ): { readonly context: StaticDocumentContext; readonly router: unknown } {
252
+ const url = new URL(request.url)
253
+ const rootRoute =
254
+ routeGraph[0] && typeof routeGraph[0] === "object" ? routeGraph[0] : {}
255
+ const context: StaticDocumentContext & {
256
+ readonly basename: string
257
+ readonly location: Record<string, unknown>
258
+ } = {
259
+ basename: app.routing.basename,
260
+ location: {
261
+ pathname: url.pathname,
262
+ search: url.search,
263
+ hash: url.hash,
264
+ state: null,
265
+ key: "default",
266
+ },
267
+ matches: [
268
+ {
269
+ params: {},
270
+ pathname: "",
271
+ pathnameBase: app.routing.basename,
272
+ route: { ...rootRoute, id: "0" },
273
+ },
274
+ ],
275
+ loaderData: {},
276
+ actionData: null,
277
+ errors: null,
278
+ statusCode: 200,
279
+ }
280
+
281
+ return {
282
+ context,
283
+ router: renderer.createStaticRouter(routeGraph, context),
284
+ }
285
+ }
286
+
287
+ function resolveDocumentMode(
288
+ request: Request,
289
+ mode: DocumentMode | undefined,
290
+ route: RouteDefinition | null,
291
+ ): DocumentMode {
292
+ if (mode) {
293
+ return mode
294
+ }
295
+
296
+ if (new URL(request.url).searchParams.has("__flamefront_shell")) {
297
+ return "shell"
298
+ }
299
+
300
+ if (!route) {
301
+ throw new Response("Not found", { status: 404 })
302
+ }
303
+
304
+ return route.render
305
+ }
306
+
307
+ function assertRouteMode(
308
+ route: RouteDefinition | null,
309
+ mode: DocumentMode,
310
+ ): void {
311
+ if (mode === "shell") {
312
+ return
313
+ }
314
+
315
+ if (!route || route.render !== mode) {
316
+ throw new Response("Not found", { status: 404 })
317
+ }
318
+ }
319
+
320
+ function routeData(context: StaticDocumentContext): unknown {
321
+ const leaf = context.matches?.at(-1)
322
+ const routeId = leaf?.route?.id
323
+
324
+ return routeId ? (context.loaderData?.[routeId] ?? null) : null
325
+ }
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
+
368
+ function fragmentMetadataChain(
369
+ route: RouteDefinition,
370
+ metadata: readonly GeneratedRouteMetadata[] | undefined,
371
+ ): GeneratedRouteMetadata[] {
372
+ const leaf = metadata?.find(
373
+ (item) =>
374
+ item.kind === "route" &&
375
+ item.entry === route.entry &&
376
+ (item.path === route.path || item.path === undefined),
377
+ )
378
+
379
+ if (!leaf) {
380
+ return [
381
+ {
382
+ id: route.entry,
383
+ boundary: route.entry,
384
+ kind: "route",
385
+ entry: route.entry,
386
+ path: route.path,
387
+ render: route.render,
388
+ navigation: "fragment",
389
+ hydration: route.hydration,
390
+ },
391
+ ]
392
+ }
393
+
394
+ const byId = new Map((metadata ?? []).map((item) => [item.id, item]))
395
+ const chain: GeneratedRouteMetadata[] = []
396
+ let current: GeneratedRouteMetadata | undefined = leaf
397
+
398
+ while (current) {
399
+ chain.unshift(current)
400
+ current = current.parent ? byId.get(current.parent) : undefined
401
+ }
402
+
403
+ return chain
404
+ }
405
+
406
+ function createRouteFragmentArtifact(
407
+ route: RouteDefinition,
408
+ context: StaticDocumentContext,
409
+ router: unknown,
410
+ renderer: OctaneRenderer,
411
+ fallbackBody: string,
412
+ metadata: readonly GeneratedRouteMetadata[] | undefined,
413
+ ): RouteFragmentArtifact {
414
+ const chain = fragmentMetadataChain(route, metadata)
415
+ const boundaries: RouteFragmentBoundary[] = chain.map((item) => {
416
+ const rendered = renderer.renderRouteFragment?.(
417
+ router,
418
+ context,
419
+ item.boundary,
420
+ { identifierPrefix: outletIdentifierPrefix },
421
+ )
422
+
423
+ return {
424
+ id: item.id,
425
+ boundary: item.boundary,
426
+ kind: item.kind,
427
+ ...(item.parent ? { parent: item.parent } : {}),
428
+ html: rendered?.html ?? (item === chain.at(-1) ? fallbackBody : ""),
429
+ }
430
+ })
431
+ const fragmentHtml = boundaries.at(-1)?.html || fallbackBody
432
+
433
+ return {
434
+ protocol: routeFragmentProtocol,
435
+ route: route.path,
436
+ boundary: chain.at(-1)?.boundary ?? route.entry,
437
+ html: fragmentHtml,
438
+ routeData: routeData(context),
439
+ boundaries,
440
+ hydration: route.hydration,
441
+ status: context.statusCode ?? 200,
442
+ }
443
+ }
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
+
487
+ export function createOctaneDocuments<
488
+ Context = unknown,
489
+ Route extends RouteDefinition = RouteDefinition,
490
+ >(options: OctaneDocumentsOptions<Context, Route>): OctaneDocuments {
491
+ let routerPromise: Promise<DocumentRouter> | undefined
492
+ let rendererPromise: Promise<OctaneRenderer> | undefined
493
+
494
+ const getRouter = (): Promise<DocumentRouter> => {
495
+ if (options.router) {
496
+ return Promise.resolve(options.router)
497
+ }
498
+
499
+ return (routerPromise ??= loadDefaultRouter())
500
+ }
501
+
502
+ const getRenderer = (): Promise<OctaneRenderer> => {
503
+ if (options.renderer) {
504
+ return Promise.resolve(options.renderer)
505
+ }
506
+
507
+ return (rendererPromise ??= loadDefaultRenderer())
508
+ }
509
+
510
+ const renderRoute = async (
511
+ request: Request,
512
+ mode: DocumentMode,
513
+ route: RouteDefinition | null,
514
+ renderOutlet = false,
515
+ ): Promise<{
516
+ readonly router: DocumentRouter
517
+ readonly dataRouter: unknown
518
+ readonly renderer: OctaneRenderer
519
+ readonly context: StaticDocumentContext
520
+ readonly rendered: OctaneRenderResult
521
+ }> => {
522
+ const contextOptions: RouteRuntimeContextOptions = {
523
+ purpose: "document",
524
+ mode,
525
+ }
526
+ const requestContext = await options.runtime.createRequestContext(
527
+ request,
528
+ contextOptions,
529
+ )
530
+ const [router, renderer] = await Promise.all([getRouter(), getRenderer()])
531
+ const routerDocument =
532
+ options.routerDocument ?? renderer.defaultRouterDocument
533
+ const result =
534
+ mode === "shell" || mode === "client"
535
+ ? createShellRouter(request, options.app, router.routes, renderer)
536
+ : await router.createServerRouter(request, {
537
+ basename: options.app.routing.basename,
538
+ requestContext,
539
+ })
540
+
541
+ if (result instanceof Response) {
542
+ throw result
543
+ }
544
+
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
+ })
572
+
573
+ return {
574
+ router,
575
+ dataRouter,
576
+ renderer,
577
+ context,
578
+ rendered: {
579
+ ...rendered,
580
+ css: appendCss(rendered.css, outlet?.css),
581
+ },
582
+ }
583
+ }
584
+
585
+ const renderDocument = async (
586
+ template: string,
587
+ request: Request,
588
+ renderOptions: RenderDocumentOptions = {},
589
+ ): Promise<RenderedDocument> => {
590
+ const sanitizedRequest = stripFlamefrontProtocolRequest(request)
591
+ const routeMatch = options.app.match(sanitizedRequest.url)
592
+ const route = routeMatch?.data ?? null
593
+ const mode = resolveDocumentMode(request, renderOptions.mode, route)
594
+
595
+ assertRouteMode(route, mode)
596
+ const { context: staticContext, rendered } = await renderRoute(
597
+ sanitizedRequest,
598
+ mode,
599
+ route,
600
+ true,
601
+ )
602
+ const status = staticContext.statusCode ?? 200
603
+ const compositionContext: DocumentCompositionContext<Route> = {
604
+ request: sanitizedRequest,
605
+ mode,
606
+ route,
607
+ params: (routeMatch?.params ?? {}) as Readonly<
608
+ RouteParams<Route["path"]>
609
+ >,
610
+ status,
611
+ }
612
+ const parts: DocumentParts = {
613
+ template,
614
+ body: rendered.html,
615
+ css: rendered.css,
616
+ hydrationScript: staticRouterHydrationScript(staticContext),
617
+ }
618
+ const html = await (options.composeDocument
619
+ ? options.composeDocument(parts, compositionContext)
620
+ : composeDefaultDocument(
621
+ parts.template,
622
+ parts.body,
623
+ parts.css,
624
+ parts.hydrationScript,
625
+ ))
626
+
627
+ return mode === "static"
628
+ ? { html, routeData: routeData(staticContext), status }
629
+ : { html, status }
630
+ }
631
+
632
+ const renderFragment = async (
633
+ request: Request,
634
+ ): Promise<RouteFragmentArtifact> => {
635
+ const sanitizedRequest = stripFlamefrontProtocolRequest(request)
636
+ const routeMatch = options.app.match(sanitizedRequest.url)
637
+ const route = routeMatch?.data ?? null
638
+
639
+ if (!route || route.render === "client") {
640
+ throw new Response("Not found", { status: 404 })
641
+ }
642
+
643
+ const { router, dataRouter, renderer, rendered, context } =
644
+ await renderRoute(sanitizedRequest, route.render, route)
645
+
646
+ return createRouteFragmentArtifact(
647
+ route,
648
+ context,
649
+ dataRouter,
650
+ renderer,
651
+ rendered.html,
652
+ router.routeMetadata,
653
+ )
654
+ }
655
+
656
+ return {
657
+ renderDocument,
658
+ loadRouteData: options.runtime.loadRouteData,
659
+ renderFragment,
660
+ }
661
+ }
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
+ }