effect-start 0.9.0 → 0.11.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/package.json +15 -14
- package/src/BundleHttp.test.ts +1 -1
- package/src/Commander.test.ts +15 -15
- package/src/Commander.ts +58 -88
- package/src/EncryptedCookies.test.ts +4 -4
- package/src/FileHttpRouter.test.ts +85 -16
- package/src/FileHttpRouter.ts +119 -32
- package/src/FileRouter.ts +62 -166
- package/src/FileRouterCodegen.test.ts +252 -66
- package/src/FileRouterCodegen.ts +13 -56
- package/src/FileRouterPattern.test.ts +116 -0
- package/src/FileRouterPattern.ts +59 -0
- package/src/FileRouter_path.test.ts +63 -102
- package/src/FileSystemExtra.test.ts +226 -0
- package/src/FileSystemExtra.ts +24 -60
- package/src/HttpAppExtra.test.ts +84 -0
- package/src/HttpAppExtra.ts +399 -47
- package/src/HttpUtils.test.ts +68 -0
- package/src/HttpUtils.ts +15 -0
- package/src/HyperHtml.ts +24 -5
- package/src/JsModule.test.ts +1 -1
- package/src/NodeFileSystem.ts +764 -0
- package/src/Random.ts +59 -0
- package/src/Route.test.ts +515 -18
- package/src/Route.ts +321 -166
- package/src/RouteRender.ts +40 -0
- package/src/Router.test.ts +416 -0
- package/src/Router.ts +288 -31
- package/src/RouterPattern.test.ts +655 -0
- package/src/RouterPattern.ts +416 -0
- package/src/Start.ts +14 -52
- package/src/TestHttpClient.test.ts +29 -0
- package/src/TestHttpClient.ts +122 -73
- package/src/assets.d.ts +39 -0
- package/src/bun/BunBundle.test.ts +0 -3
- package/src/bun/BunHttpServer.test.ts +74 -0
- package/src/bun/BunHttpServer.ts +259 -0
- package/src/bun/BunHttpServer_web.ts +384 -0
- package/src/bun/BunRoute.test.ts +514 -0
- package/src/bun/BunRoute.ts +427 -0
- package/src/bun/BunRoute_bundles.test.ts +218 -0
- package/src/bun/BunRuntime.ts +33 -0
- package/src/bun/BunTailwindPlugin.test.ts +1 -1
- package/src/bun/_empty.html +1 -0
- package/src/bun/index.ts +2 -1
- package/src/index.ts +14 -14
- package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
- package/src/middlewares/BasicAuthMiddleware.ts +36 -0
- package/src/testing.ts +12 -3
- package/src/Datastar.test.ts +0 -267
- package/src/Datastar.ts +0 -68
- package/src/bun/BunFullstackServer.ts +0 -45
- package/src/bun/BunFullstackServer_httpServer.ts +0 -541
- package/src/jsx-datastar.d.ts +0 -63
package/src/Route.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as HttpMethod from "@effect/platform/HttpMethod"
|
|
2
|
+
import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
|
|
2
3
|
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
3
|
-
import * as HttpServerRespondable from "@effect/platform/HttpServerRespondable"
|
|
4
4
|
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
5
5
|
import * as Effect from "effect/Effect"
|
|
6
6
|
import * as Pipeable from "effect/Pipeable"
|
|
7
7
|
import * as Predicate from "effect/Predicate"
|
|
8
8
|
import * as Schema from "effect/Schema"
|
|
9
9
|
import type { YieldWrap } from "effect/Utils"
|
|
10
|
-
import * as
|
|
10
|
+
import * as RouteRender from "./RouteRender.ts"
|
|
11
11
|
|
|
12
12
|
export {
|
|
13
13
|
pipe,
|
|
@@ -63,12 +63,14 @@ type Self =
|
|
|
63
63
|
|
|
64
64
|
const TypeId: unique symbol = Symbol.for("effect-start/Route")
|
|
65
65
|
const RouteSetTypeId: unique symbol = Symbol.for("effect-start/RouteSet")
|
|
66
|
+
const RouteLayerTypeId: unique symbol = Symbol.for("effect-start/RouteLayer")
|
|
66
67
|
|
|
67
68
|
export type RouteMethod =
|
|
68
69
|
| "*"
|
|
69
70
|
| HttpMethod.HttpMethod
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
// TODO: This should be a RouterPattern and moved to its file?
|
|
73
|
+
export type RoutePattern = `/${string}`
|
|
72
74
|
|
|
73
75
|
/**
|
|
74
76
|
* Route media type used for content negotiation.
|
|
@@ -76,55 +78,24 @@ export type RoutePath = `/${string}`
|
|
|
76
78
|
* for the same path & method, depending on the `Accept` header
|
|
77
79
|
* of the request.
|
|
78
80
|
*/
|
|
79
|
-
type RouteMedia =
|
|
81
|
+
export type RouteMedia =
|
|
80
82
|
| "*"
|
|
81
83
|
| "text/plain"
|
|
82
84
|
| "text/html"
|
|
83
85
|
| "application/json"
|
|
84
86
|
|
|
87
|
+
/**
|
|
88
|
+
* A handler function that produces a raw value.
|
|
89
|
+
* The value will be rendered to an HttpServerResponse by RouteRender
|
|
90
|
+
* based on the route's media type.
|
|
91
|
+
*
|
|
92
|
+
* Receives RouteContext which includes an optional next() for layers.
|
|
93
|
+
*/
|
|
85
94
|
export type RouteHandler<
|
|
86
95
|
A = unknown,
|
|
87
96
|
E = any,
|
|
88
97
|
R = any,
|
|
89
|
-
> =
|
|
90
|
-
/**
|
|
91
|
-
* A handler that contains raw value.
|
|
92
|
-
* Can be consumed from other handlers to build more complex responses.
|
|
93
|
-
* For example, a Route can render markdown for API/AI consumption
|
|
94
|
-
* and another Route can wrap it in HTML for browsers.
|
|
95
|
-
*/
|
|
96
|
-
| RouteHandler.Value<A, E, R>
|
|
97
|
-
/**
|
|
98
|
-
* A handler returns `HttpServerResponse`.
|
|
99
|
-
* Should not be consumed with caution: if body is a stream,
|
|
100
|
-
* consuming it in another handler may break the stream.
|
|
101
|
-
*/
|
|
102
|
-
| RouteHandler.Encoded<E, R>
|
|
103
|
-
|
|
104
|
-
export namespace RouteHandler {
|
|
105
|
-
export type Value<
|
|
106
|
-
A = unknown,
|
|
107
|
-
E = any,
|
|
108
|
-
R = any,
|
|
109
|
-
> = Effect.Effect<
|
|
110
|
-
{
|
|
111
|
-
[HttpServerRespondable.symbol]: () => Effect.Effect<
|
|
112
|
-
HttpServerResponse.HttpServerResponse,
|
|
113
|
-
E,
|
|
114
|
-
R
|
|
115
|
-
>
|
|
116
|
-
raw: A
|
|
117
|
-
},
|
|
118
|
-
E,
|
|
119
|
-
R
|
|
120
|
-
>
|
|
121
|
-
|
|
122
|
-
export type Encoded<E = any, R = any> = Effect.Effect<
|
|
123
|
-
HttpServerResponse.HttpServerResponse,
|
|
124
|
-
E,
|
|
125
|
-
R
|
|
126
|
-
>
|
|
127
|
-
}
|
|
98
|
+
> = (context: RouteContext) => Effect.Effect<A, E, R>
|
|
128
99
|
|
|
129
100
|
/**
|
|
130
101
|
* Helper type for a value that can be a single item or an array.
|
|
@@ -213,7 +184,7 @@ type RouteBuilder = {
|
|
|
213
184
|
get: typeof get
|
|
214
185
|
put: typeof put
|
|
215
186
|
patch: typeof patch
|
|
216
|
-
|
|
187
|
+
delete: typeof _delete
|
|
217
188
|
options: typeof options
|
|
218
189
|
head: typeof head
|
|
219
190
|
|
|
@@ -262,36 +233,84 @@ export namespace RouteSet {
|
|
|
262
233
|
& RouteBuilder
|
|
263
234
|
}
|
|
264
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Type for HTTP middleware function
|
|
238
|
+
*/
|
|
239
|
+
export type HttpMiddlewareFunction = ReturnType<typeof HttpMiddleware.make>
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Marker type for route middleware specification.
|
|
243
|
+
* Used to distinguish middleware from routes in Route.layer() arguments.
|
|
244
|
+
*/
|
|
245
|
+
export interface RouteMiddleware {
|
|
246
|
+
readonly _tag: "RouteMiddleware"
|
|
247
|
+
readonly middleware: HttpMiddlewareFunction
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export type RouteLayer<
|
|
251
|
+
M extends ReadonlyArray<Route.Default> = ReadonlyArray<Route.Default>,
|
|
252
|
+
Schemas extends RouteSchemas = RouteSchemas.Empty,
|
|
253
|
+
> =
|
|
254
|
+
& Pipeable.Pipeable
|
|
255
|
+
& {
|
|
256
|
+
[RouteLayerTypeId]: typeof RouteLayerTypeId
|
|
257
|
+
[RouteSetTypeId]: typeof RouteSetTypeId
|
|
258
|
+
set: M
|
|
259
|
+
schema: Schemas
|
|
260
|
+
httpMiddleware?: HttpMiddlewareFunction
|
|
261
|
+
}
|
|
262
|
+
& RouteBuilder
|
|
263
|
+
|
|
264
|
+
export const isRouteLayer = (u: unknown): u is RouteLayer =>
|
|
265
|
+
Predicate.hasProperty(u, RouteLayerTypeId)
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check if two routes match based on method and media type.
|
|
269
|
+
* Returns true if both method and media type match, accounting for wildcards.
|
|
270
|
+
*/
|
|
271
|
+
export function matches(
|
|
272
|
+
a: Route.Default,
|
|
273
|
+
b: Route.Default,
|
|
274
|
+
): boolean {
|
|
275
|
+
const methodMatches = a.method === "*"
|
|
276
|
+
|| b.method === "*"
|
|
277
|
+
|| a.method === b.method
|
|
278
|
+
|
|
279
|
+
const mediaMatches = a.media === "*"
|
|
280
|
+
|| b.media === "*"
|
|
281
|
+
|| a.media === b.media
|
|
282
|
+
|
|
283
|
+
return methodMatches && mediaMatches
|
|
284
|
+
}
|
|
285
|
+
|
|
265
286
|
export const post = makeMethodModifier("POST")
|
|
266
287
|
export const get = makeMethodModifier("GET")
|
|
267
288
|
export const put = makeMethodModifier("PUT")
|
|
268
289
|
export const patch = makeMethodModifier("PATCH")
|
|
269
|
-
export const del = makeMethodModifier("DELETE")
|
|
270
290
|
export const options = makeMethodModifier("OPTIONS")
|
|
271
291
|
export const head = makeMethodModifier("HEAD")
|
|
292
|
+
const _delete = makeMethodModifier("DELETE")
|
|
293
|
+
export {
|
|
294
|
+
_delete as delete,
|
|
295
|
+
}
|
|
272
296
|
|
|
273
|
-
export const text = makeMediaFunction(
|
|
297
|
+
export const text = makeMediaFunction<"GET", "text/plain", string>(
|
|
274
298
|
"GET",
|
|
275
299
|
"text/plain",
|
|
276
|
-
makeValueHandler<string>(HttpServerResponse.text),
|
|
277
300
|
)
|
|
278
301
|
|
|
279
|
-
export const html = makeMediaFunction
|
|
302
|
+
export const html = makeMediaFunction<
|
|
303
|
+
"GET",
|
|
304
|
+
"text/html",
|
|
305
|
+
string | GenericJsxObject
|
|
306
|
+
>(
|
|
280
307
|
"GET",
|
|
281
308
|
"text/html",
|
|
282
|
-
makeValueHandler<string | JsxObject>((raw) => {
|
|
283
|
-
// Check if it's a JSX element (has type and props properties)
|
|
284
|
-
if (isJsxObject(raw)) {
|
|
285
|
-
return HttpServerResponse.html(HyperHtml.renderToString(raw))
|
|
286
|
-
}
|
|
287
|
-
return HttpServerResponse.html(raw as string)
|
|
288
|
-
}),
|
|
289
309
|
)
|
|
290
310
|
|
|
291
|
-
export const json = makeMediaFunction(
|
|
311
|
+
export const json = makeMediaFunction<"GET", "application/json", JsonValue>(
|
|
292
312
|
"GET",
|
|
293
313
|
"application/json",
|
|
294
|
-
makeValueHandler<JsonValue>((raw) => HttpServerResponse.unsafeJson(raw)),
|
|
295
314
|
)
|
|
296
315
|
|
|
297
316
|
/**
|
|
@@ -379,7 +398,7 @@ function makeSingleStringSchemaModifier<
|
|
|
379
398
|
{
|
|
380
399
|
const baseRoutes = isRouteSet(this)
|
|
381
400
|
? this.set
|
|
382
|
-
: []
|
|
401
|
+
: [] as const
|
|
383
402
|
const baseSchema = isRouteSet(this)
|
|
384
403
|
? this.schema
|
|
385
404
|
: {} as RouteSchemas.Empty
|
|
@@ -389,12 +408,12 @@ function makeSingleStringSchemaModifier<
|
|
|
389
408
|
: Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
|
|
390
409
|
|
|
391
410
|
return makeSet(
|
|
392
|
-
baseRoutes as
|
|
411
|
+
baseRoutes as ReadonlyArray<Route.Default>,
|
|
393
412
|
{
|
|
394
413
|
...baseSchema,
|
|
395
414
|
[key]: schema,
|
|
396
|
-
}
|
|
397
|
-
) as
|
|
415
|
+
},
|
|
416
|
+
) as never
|
|
398
417
|
}
|
|
399
418
|
}
|
|
400
419
|
|
|
@@ -430,7 +449,7 @@ function makeMultiStringSchemaModifier<
|
|
|
430
449
|
{
|
|
431
450
|
const baseRoutes = isRouteSet(this)
|
|
432
451
|
? this.set
|
|
433
|
-
: []
|
|
452
|
+
: [] as const
|
|
434
453
|
const baseSchema = isRouteSet(this)
|
|
435
454
|
? this.schema
|
|
436
455
|
: {} as RouteSchemas.Empty
|
|
@@ -440,12 +459,12 @@ function makeMultiStringSchemaModifier<
|
|
|
440
459
|
: Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
|
|
441
460
|
|
|
442
461
|
return makeSet(
|
|
443
|
-
baseRoutes as
|
|
462
|
+
baseRoutes as ReadonlyArray<Route.Default>,
|
|
444
463
|
{
|
|
445
464
|
...baseSchema,
|
|
446
465
|
[key]: schema,
|
|
447
|
-
}
|
|
448
|
-
) as
|
|
466
|
+
},
|
|
467
|
+
) as never
|
|
449
468
|
}
|
|
450
469
|
}
|
|
451
470
|
|
|
@@ -478,7 +497,7 @@ function makeUnionSchemaModifier<
|
|
|
478
497
|
{
|
|
479
498
|
const baseRoutes = isRouteSet(this)
|
|
480
499
|
? this.set
|
|
481
|
-
: []
|
|
500
|
+
: [] as const
|
|
482
501
|
const baseSchema = isRouteSet(this)
|
|
483
502
|
? this.schema
|
|
484
503
|
: {} as RouteSchemas.Empty
|
|
@@ -488,12 +507,12 @@ function makeUnionSchemaModifier<
|
|
|
488
507
|
: Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
|
|
489
508
|
|
|
490
509
|
return makeSet(
|
|
491
|
-
baseRoutes as
|
|
510
|
+
baseRoutes as ReadonlyArray<Route.Default>,
|
|
492
511
|
{
|
|
493
512
|
...baseSchema,
|
|
494
513
|
[key]: schema,
|
|
495
|
-
}
|
|
496
|
-
) as
|
|
514
|
+
},
|
|
515
|
+
) as never
|
|
497
516
|
}
|
|
498
517
|
}
|
|
499
518
|
|
|
@@ -511,7 +530,7 @@ const SetProto = {
|
|
|
511
530
|
get,
|
|
512
531
|
put,
|
|
513
532
|
patch,
|
|
514
|
-
|
|
533
|
+
delete: _delete,
|
|
515
534
|
options,
|
|
516
535
|
head,
|
|
517
536
|
|
|
@@ -538,6 +557,13 @@ const RouteProto = Object.assign(
|
|
|
538
557
|
} satisfies Route.Proto,
|
|
539
558
|
)
|
|
540
559
|
|
|
560
|
+
const RouteLayerProto = Object.assign(
|
|
561
|
+
Object.create(SetProto),
|
|
562
|
+
{
|
|
563
|
+
[RouteLayerTypeId]: RouteLayerTypeId,
|
|
564
|
+
},
|
|
565
|
+
)
|
|
566
|
+
|
|
541
567
|
export function isRoute(input: unknown): input is Route {
|
|
542
568
|
return Predicate.hasProperty(input, TypeId)
|
|
543
569
|
}
|
|
@@ -558,24 +584,6 @@ export type JsonValue =
|
|
|
558
584
|
[key: string]: JsonValue
|
|
559
585
|
}
|
|
560
586
|
|
|
561
|
-
/**
|
|
562
|
-
* Constructs a URL from HttpServerRequest.
|
|
563
|
-
* Handles relative URLs by using headers to determine the base URL.
|
|
564
|
-
*/
|
|
565
|
-
function makeUrlFromRequest(
|
|
566
|
-
request: HttpServerRequest.HttpServerRequest,
|
|
567
|
-
): URL {
|
|
568
|
-
const origin = request.headers.origin
|
|
569
|
-
?? request.headers.host
|
|
570
|
-
?? "http://localhost"
|
|
571
|
-
const protocol = request.headers["x-forwarded-proto"] ?? "http"
|
|
572
|
-
const host = request.headers.host ?? "localhost"
|
|
573
|
-
const base = origin.startsWith("http")
|
|
574
|
-
? origin
|
|
575
|
-
: `${protocol}://${host}`
|
|
576
|
-
return new URL(request.url, base)
|
|
577
|
-
}
|
|
578
|
-
|
|
579
587
|
type RouteContextDecoded = {
|
|
580
588
|
readonly pathParams?: Record<string, any>
|
|
581
589
|
readonly urlParams?: Record<string, any>
|
|
@@ -607,20 +615,22 @@ export type DecodeRouteSchemas<Schemas extends RouteSchemas> =
|
|
|
607
615
|
: {})
|
|
608
616
|
|
|
609
617
|
/**
|
|
610
|
-
* Context passed to route handler
|
|
618
|
+
* Context passed to route handler functions.
|
|
619
|
+
*
|
|
620
|
+
* @template {Input} Decoded schema values (pathParams, urlParams, etc.)
|
|
621
|
+
* @template {Next} Return type of next() based on media type
|
|
611
622
|
*/
|
|
612
623
|
export type RouteContext<
|
|
613
624
|
Input extends RouteContextDecoded = {},
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
: never
|
|
625
|
+
Next = unknown,
|
|
626
|
+
> =
|
|
627
|
+
& {
|
|
628
|
+
request: HttpServerRequest.HttpServerRequest
|
|
629
|
+
get url(): URL
|
|
630
|
+
slots: Record<string, string>
|
|
631
|
+
next: <E = unknown, R = unknown>() => Effect.Effect<Next, E, R>
|
|
632
|
+
}
|
|
633
|
+
& Input
|
|
624
634
|
|
|
625
635
|
/**
|
|
626
636
|
* Merges two RouteSchemas types.
|
|
@@ -730,7 +740,7 @@ function mergeSchemas<
|
|
|
730
740
|
return result
|
|
731
741
|
}
|
|
732
742
|
|
|
733
|
-
function make<
|
|
743
|
+
export function make<
|
|
734
744
|
Method extends RouteMethod = "*",
|
|
735
745
|
Media extends RouteMedia = "*",
|
|
736
746
|
Handler extends RouteHandler = never,
|
|
@@ -784,40 +794,62 @@ function makeSet<
|
|
|
784
794
|
) as RouteSet<M, Schemas>
|
|
785
795
|
}
|
|
786
796
|
|
|
797
|
+
type HandlerInput<A, E, R> =
|
|
798
|
+
| A
|
|
799
|
+
| Effect.Effect<A, E, R>
|
|
800
|
+
| ((context: RouteContext) =>
|
|
801
|
+
| Effect.Effect<A, E, R>
|
|
802
|
+
| Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>)
|
|
803
|
+
|
|
804
|
+
function normalizeHandler<A, E, R>(
|
|
805
|
+
handler: HandlerInput<A, E, R>,
|
|
806
|
+
): RouteHandler<A, E, R> {
|
|
807
|
+
if (typeof handler === "function") {
|
|
808
|
+
return (context): Effect.Effect<A, E, R> => {
|
|
809
|
+
const result = (handler as Function)(context)
|
|
810
|
+
if (Effect.isEffect(result)) {
|
|
811
|
+
return result as Effect.Effect<A, E, R>
|
|
812
|
+
}
|
|
813
|
+
return Effect.gen(() => result) as Effect.Effect<A, E, R>
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (Effect.isEffect(handler)) {
|
|
817
|
+
return () => handler
|
|
818
|
+
}
|
|
819
|
+
return () => Effect.succeed(handler as A)
|
|
820
|
+
}
|
|
821
|
+
|
|
787
822
|
/**
|
|
788
823
|
* Factory function that creates Route for a specific method & media.
|
|
789
|
-
*
|
|
824
|
+
* Accepts Effect, function that returns Effect, and effectful generator.
|
|
790
825
|
*/
|
|
791
826
|
function makeMediaFunction<
|
|
792
827
|
Method extends HttpMethod.HttpMethod,
|
|
793
828
|
Media extends RouteMedia,
|
|
794
|
-
|
|
795
|
-
handler: any,
|
|
796
|
-
) => any,
|
|
829
|
+
ExpectedValue,
|
|
797
830
|
>(
|
|
798
831
|
method: Method,
|
|
799
832
|
media: Media,
|
|
800
|
-
handlerFn: HandlerFn,
|
|
801
833
|
) {
|
|
802
834
|
return function<
|
|
803
835
|
S extends Self,
|
|
804
|
-
A,
|
|
836
|
+
A extends ExpectedValue,
|
|
805
837
|
E = never,
|
|
806
838
|
R = never,
|
|
807
839
|
>(
|
|
808
840
|
this: S,
|
|
809
841
|
handler: S extends RouteSet<infer _Routes, infer Schemas> ?
|
|
842
|
+
| A
|
|
810
843
|
| Effect.Effect<A, E, R>
|
|
811
844
|
| ((
|
|
812
|
-
context: RouteContext<DecodeRouteSchemas<Schemas
|
|
845
|
+
context: RouteContext<DecodeRouteSchemas<Schemas>, ExpectedValue>,
|
|
813
846
|
) =>
|
|
814
847
|
| Effect.Effect<A, E, R>
|
|
815
848
|
| Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>)
|
|
816
849
|
:
|
|
850
|
+
| A
|
|
817
851
|
| Effect.Effect<A, E, R>
|
|
818
|
-
| ((
|
|
819
|
-
context: RouteContext<{}>,
|
|
820
|
-
) =>
|
|
852
|
+
| ((context: RouteContext<{}, ExpectedValue>) =>
|
|
821
853
|
| Effect.Effect<A, E, R>
|
|
822
854
|
| Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>),
|
|
823
855
|
): S extends RouteSet<infer Routes, infer Schemas> ? RouteSet<[
|
|
@@ -825,7 +857,7 @@ function makeMediaFunction<
|
|
|
825
857
|
Route<
|
|
826
858
|
Method,
|
|
827
859
|
Media,
|
|
828
|
-
|
|
860
|
+
RouteHandler<A, E, R>,
|
|
829
861
|
Schemas
|
|
830
862
|
>,
|
|
831
863
|
], Schemas>
|
|
@@ -833,32 +865,14 @@ function makeMediaFunction<
|
|
|
833
865
|
Route<
|
|
834
866
|
Method,
|
|
835
867
|
Media,
|
|
836
|
-
|
|
868
|
+
RouteHandler<A, E, R>,
|
|
837
869
|
RouteSchemas.Empty
|
|
838
870
|
>,
|
|
839
871
|
], RouteSchemas.Empty>
|
|
840
872
|
{
|
|
841
|
-
const effect = typeof handler === "function"
|
|
842
|
-
? Effect.gen(function*() {
|
|
843
|
-
const request = yield* HttpServerRequest.HttpServerRequest
|
|
844
|
-
const context: RouteContext<any> = {
|
|
845
|
-
request,
|
|
846
|
-
get url() {
|
|
847
|
-
return makeUrlFromRequest(request)
|
|
848
|
-
},
|
|
849
|
-
}
|
|
850
|
-
const result = handler(context)
|
|
851
|
-
return yield* (typeof result === "object"
|
|
852
|
-
&& result !== null
|
|
853
|
-
&& Symbol.iterator in result
|
|
854
|
-
? Effect.gen(() => result as any)
|
|
855
|
-
: result as Effect.Effect<A, E, R>)
|
|
856
|
-
})
|
|
857
|
-
: handler
|
|
858
|
-
|
|
859
873
|
const baseRoutes = isRouteSet(this)
|
|
860
874
|
? this.set
|
|
861
|
-
: []
|
|
875
|
+
: [] as const
|
|
862
876
|
const baseSchema = isRouteSet(this)
|
|
863
877
|
? this.schema
|
|
864
878
|
: {} as RouteSchemas.Empty
|
|
@@ -869,36 +883,12 @@ function makeMediaFunction<
|
|
|
869
883
|
make({
|
|
870
884
|
method,
|
|
871
885
|
media,
|
|
872
|
-
handler:
|
|
873
|
-
schemas: baseSchema
|
|
886
|
+
handler: normalizeHandler(handler),
|
|
887
|
+
schemas: baseSchema,
|
|
874
888
|
}),
|
|
875
|
-
] as
|
|
876
|
-
baseSchema
|
|
877
|
-
) as
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/**
|
|
882
|
-
* Factory to create RouteHandler.Value.
|
|
883
|
-
* Useful for structural handlers like JSON
|
|
884
|
-
* or content that can be embedded in other formats,
|
|
885
|
-
* like text or HTML.
|
|
886
|
-
*/
|
|
887
|
-
function makeValueHandler<ExpectedRaw = string>(
|
|
888
|
-
responseFn: (raw: ExpectedRaw) => HttpServerResponse.HttpServerResponse,
|
|
889
|
-
) {
|
|
890
|
-
return <A extends ExpectedRaw, E = never, R = never>(
|
|
891
|
-
handler: Effect.Effect<A, E, R>,
|
|
892
|
-
): RouteHandler.Value<A, E, R> => {
|
|
893
|
-
return Effect.gen(function*() {
|
|
894
|
-
const raw = yield* handler
|
|
895
|
-
|
|
896
|
-
return {
|
|
897
|
-
[HttpServerRespondable.symbol]: () =>
|
|
898
|
-
Effect.succeed(responseFn(raw as ExpectedRaw)),
|
|
899
|
-
raw,
|
|
900
|
-
}
|
|
901
|
-
}) as RouteHandler.Value<A, E, R>
|
|
889
|
+
] as ReadonlyArray<Route.Default>,
|
|
890
|
+
baseSchema,
|
|
891
|
+
) as never
|
|
902
892
|
}
|
|
903
893
|
}
|
|
904
894
|
|
|
@@ -970,23 +960,188 @@ function makeMethodModifier<
|
|
|
970
960
|
return make({
|
|
971
961
|
...route,
|
|
972
962
|
method,
|
|
973
|
-
schemas: mergeSchemas(baseSchema, route.schemas)
|
|
963
|
+
schemas: mergeSchemas(baseSchema, route.schemas),
|
|
974
964
|
})
|
|
975
965
|
}),
|
|
976
|
-
] as
|
|
977
|
-
baseSchema
|
|
978
|
-
) as
|
|
966
|
+
] as ReadonlyArray<Route.Default>,
|
|
967
|
+
baseSchema,
|
|
968
|
+
) as never
|
|
979
969
|
}
|
|
980
970
|
}
|
|
981
971
|
|
|
982
|
-
type
|
|
972
|
+
export type GenericJsxObject = {
|
|
983
973
|
type: any
|
|
984
974
|
props: any
|
|
985
975
|
}
|
|
986
976
|
|
|
987
|
-
function
|
|
977
|
+
export function isGenericJsxObject(value: unknown): value is GenericJsxObject {
|
|
988
978
|
return typeof value === "object"
|
|
989
979
|
&& value !== null
|
|
990
980
|
&& "type" in value
|
|
991
981
|
&& "props" in value
|
|
992
982
|
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Create HTTP middleware spec for Route.layer().
|
|
986
|
+
* Multiple middleware can be composed by passing multiple Route.http() to Route.layer().
|
|
987
|
+
*
|
|
988
|
+
* @example
|
|
989
|
+
* Route.layer(
|
|
990
|
+
* Route.http(middleware1),
|
|
991
|
+
* Route.http(middleware2),
|
|
992
|
+
* Route.http(middleware3)
|
|
993
|
+
* )
|
|
994
|
+
*/
|
|
995
|
+
export function http(
|
|
996
|
+
middleware: HttpMiddlewareFunction,
|
|
997
|
+
): RouteMiddleware {
|
|
998
|
+
return {
|
|
999
|
+
_tag: "RouteMiddleware",
|
|
1000
|
+
middleware,
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Create a RouteLayer from routes and middleware.
|
|
1006
|
+
*
|
|
1007
|
+
* Accepts:
|
|
1008
|
+
* - Route.http(middleware) - HTTP middleware to apply to all child routes
|
|
1009
|
+
* - Route.html/text/json/etc handlers - Wrapper routes that receive props.children
|
|
1010
|
+
* - Other RouteSets - Routes to include in the layer
|
|
1011
|
+
*
|
|
1012
|
+
* Multiple middleware are composed in order - first middleware wraps second, etc.
|
|
1013
|
+
* Routes in the layer act as wrappers for child routes with matching method + media type.
|
|
1014
|
+
*
|
|
1015
|
+
* @example
|
|
1016
|
+
* Route.layer(
|
|
1017
|
+
* Route.http(loggingMiddleware),
|
|
1018
|
+
* Route.http(authMiddleware),
|
|
1019
|
+
* Route.html(function*(props) {
|
|
1020
|
+
* return <html><body>{props.children}</body></html>
|
|
1021
|
+
* })
|
|
1022
|
+
* )
|
|
1023
|
+
*/
|
|
1024
|
+
export function layer(
|
|
1025
|
+
...items: Array<RouteMiddleware | RouteSet.Default>
|
|
1026
|
+
): RouteLayer {
|
|
1027
|
+
const routeMiddleware: RouteMiddleware[] = []
|
|
1028
|
+
const routeSets: RouteSet.Default[] = []
|
|
1029
|
+
|
|
1030
|
+
for (const item of items) {
|
|
1031
|
+
if ("_tag" in item && item._tag === "RouteMiddleware") {
|
|
1032
|
+
routeMiddleware.push(item)
|
|
1033
|
+
} else if (isRouteSet(item)) {
|
|
1034
|
+
routeSets.push(item)
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const layerRoutes: Route.Default[] = []
|
|
1039
|
+
|
|
1040
|
+
for (const routeSet of routeSets) {
|
|
1041
|
+
layerRoutes.push(...routeSet.set)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const middlewareFunctions = routeMiddleware.map((spec) => spec.middleware)
|
|
1045
|
+
const httpMiddleware = middlewareFunctions.length === 0
|
|
1046
|
+
? undefined
|
|
1047
|
+
: middlewareFunctions.length === 1
|
|
1048
|
+
? middlewareFunctions[0]
|
|
1049
|
+
: (app: any) => middlewareFunctions.reduceRight((acc, mw) => mw(acc), app)
|
|
1050
|
+
|
|
1051
|
+
return Object.assign(
|
|
1052
|
+
Object.create(RouteLayerProto),
|
|
1053
|
+
{
|
|
1054
|
+
set: layerRoutes,
|
|
1055
|
+
schema: {},
|
|
1056
|
+
httpMiddleware,
|
|
1057
|
+
},
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Extract method union from a RouteSet's routes
|
|
1063
|
+
*/
|
|
1064
|
+
|
|
1065
|
+
type ExtractMethods<T extends ReadonlyArray<Route.Default>> =
|
|
1066
|
+
T[number]["method"]
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Extract media union from a RouteSet's routes
|
|
1070
|
+
*/
|
|
1071
|
+
type ExtractMedia<T extends ReadonlyArray<Route.Default>> = T[number]["media"]
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Merge two RouteSets into one with content negotiation.
|
|
1075
|
+
* Properly infers union types for method/media and merges schemas.
|
|
1076
|
+
*/
|
|
1077
|
+
export function merge<
|
|
1078
|
+
RoutesA extends ReadonlyArray<Route.Default>,
|
|
1079
|
+
SchemasA extends RouteSchemas,
|
|
1080
|
+
RoutesB extends ReadonlyArray<Route.Default>,
|
|
1081
|
+
SchemasB extends RouteSchemas,
|
|
1082
|
+
>(
|
|
1083
|
+
self: RouteSet<RoutesA, SchemasA>,
|
|
1084
|
+
other: RouteSet<RoutesB, SchemasB>,
|
|
1085
|
+
): RouteSet<
|
|
1086
|
+
[
|
|
1087
|
+
Route<
|
|
1088
|
+
ExtractMethods<RoutesA> | ExtractMethods<RoutesB>,
|
|
1089
|
+
ExtractMedia<RoutesA> | ExtractMedia<RoutesB>,
|
|
1090
|
+
RouteHandler<HttpServerResponse.HttpServerResponse, any, never>,
|
|
1091
|
+
MergeSchemas<SchemasA, SchemasB>
|
|
1092
|
+
>,
|
|
1093
|
+
],
|
|
1094
|
+
MergeSchemas<SchemasA, SchemasB>
|
|
1095
|
+
> {
|
|
1096
|
+
const allRoutes = [...self.set, ...other.set]
|
|
1097
|
+
const mergedSchemas = mergeSchemas(self.schema, other.schema)
|
|
1098
|
+
|
|
1099
|
+
const handler: RouteHandler<HttpServerResponse.HttpServerResponse> = (
|
|
1100
|
+
context,
|
|
1101
|
+
) =>
|
|
1102
|
+
Effect.gen(function*() {
|
|
1103
|
+
const accept = context.request.headers.accept ?? ""
|
|
1104
|
+
|
|
1105
|
+
if (accept.includes("application/json")) {
|
|
1106
|
+
const jsonRoute = allRoutes.find((r) => r.media === "application/json")
|
|
1107
|
+
if (jsonRoute) {
|
|
1108
|
+
return yield* RouteRender.render(jsonRoute, context)
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (accept.includes("text/plain")) {
|
|
1113
|
+
const textRoute = allRoutes.find((r) => r.media === "text/plain")
|
|
1114
|
+
if (textRoute) {
|
|
1115
|
+
return yield* RouteRender.render(textRoute, context)
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (
|
|
1120
|
+
accept.includes("text/html") || accept.includes("*/*") || !accept
|
|
1121
|
+
) {
|
|
1122
|
+
const htmlRoute = allRoutes.find((r) => r.media === "text/html")
|
|
1123
|
+
if (htmlRoute) {
|
|
1124
|
+
return yield* RouteRender.render(htmlRoute, context)
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const firstRoute = allRoutes[0]
|
|
1129
|
+
if (firstRoute) {
|
|
1130
|
+
return yield* RouteRender.render(firstRoute, context)
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return HttpServerResponse.empty({ status: 406 })
|
|
1134
|
+
})
|
|
1135
|
+
|
|
1136
|
+
return makeSet(
|
|
1137
|
+
[
|
|
1138
|
+
make({
|
|
1139
|
+
method: allRoutes[0]?.method ?? "*",
|
|
1140
|
+
media: allRoutes[0]?.media ?? "*",
|
|
1141
|
+
handler,
|
|
1142
|
+
schemas: mergedSchemas,
|
|
1143
|
+
}),
|
|
1144
|
+
] as any,
|
|
1145
|
+
mergedSchemas,
|
|
1146
|
+
) as any
|
|
1147
|
+
}
|