effect-start 0.10.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 +5 -3
- package/src/FileHttpRouter.test.ts +4 -4
- package/src/FileHttpRouter.ts +6 -8
- package/src/FileRouter.ts +4 -6
- package/src/FileRouterCodegen.test.ts +2 -2
- package/src/HttpAppExtra.test.ts +84 -0
- package/src/HttpAppExtra.ts +399 -47
- package/src/Route.test.ts +59 -33
- package/src/Route.ts +59 -49
- package/src/RouteRender.ts +6 -4
- package/src/Router.test.ts +416 -0
- package/src/Router.ts +279 -0
- package/src/RouterPattern.test.ts +29 -3
- package/src/RouterPattern.ts +30 -5
- package/src/TestHttpClient.test.ts +29 -0
- package/src/TestHttpClient.ts +122 -73
- package/src/assets.d.ts +39 -0
- package/src/bun/BunHttpServer.test.ts +74 -0
- package/src/bun/BunHttpServer.ts +22 -9
- package/src/bun/BunRoute.test.ts +307 -134
- package/src/bun/BunRoute.ts +240 -139
- package/src/bun/BunRoute_bundles.test.ts +181 -181
- package/src/index.ts +14 -14
- package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
- package/src/middlewares/BasicAuthMiddleware.ts +36 -0
package/src/Router.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import * as Context from "effect/Context"
|
|
2
|
+
import * as Data from "effect/Data"
|
|
2
3
|
import * as Effect from "effect/Effect"
|
|
3
4
|
import * as Function from "effect/Function"
|
|
4
5
|
import * as Layer from "effect/Layer"
|
|
6
|
+
import * as Pipeable from "effect/Pipeable"
|
|
7
|
+
import * as Predicate from "effect/Predicate"
|
|
5
8
|
import * as FileRouter from "./FileRouter.ts"
|
|
6
9
|
import * as Route from "./Route"
|
|
7
10
|
|
|
11
|
+
export type RouterErrorReason =
|
|
12
|
+
| "UnsupportedPattern"
|
|
13
|
+
| "ProxyError"
|
|
14
|
+
|
|
15
|
+
export class RouterError extends Data.TaggedError("RouterError")<{
|
|
16
|
+
reason: RouterErrorReason
|
|
17
|
+
pattern: string
|
|
18
|
+
message: string
|
|
19
|
+
}> {}
|
|
20
|
+
|
|
8
21
|
export type ServerModule = {
|
|
9
22
|
default: Route.RouteSet.Default
|
|
10
23
|
}
|
|
@@ -56,3 +69,269 @@ export function layerPromise(
|
|
|
56
69
|
}),
|
|
57
70
|
)
|
|
58
71
|
}
|
|
72
|
+
|
|
73
|
+
const RouterBuilderTypeId: unique symbol = Symbol.for(
|
|
74
|
+
"effect-start/RouterBuilder",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
type RouterModule = typeof import("./Router.ts")
|
|
78
|
+
|
|
79
|
+
type Self =
|
|
80
|
+
| RouterBuilder<any, any>
|
|
81
|
+
| RouterModule
|
|
82
|
+
| undefined
|
|
83
|
+
|
|
84
|
+
export type RouterEntry = {
|
|
85
|
+
path: `/${string}`
|
|
86
|
+
route: Route.RouteSet.Default
|
|
87
|
+
layers: Route.RouteLayer[]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type RouterBuilderMethods = {
|
|
91
|
+
use: typeof use
|
|
92
|
+
mount: typeof mount
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface RouterBuilder<
|
|
96
|
+
out E = never,
|
|
97
|
+
out R = never,
|
|
98
|
+
> extends Pipeable.Pipeable, RouterBuilderMethods {
|
|
99
|
+
[RouterBuilderTypeId]: typeof RouterBuilderTypeId
|
|
100
|
+
readonly entries: readonly RouterEntry[]
|
|
101
|
+
readonly globalLayers: readonly Route.RouteLayer[]
|
|
102
|
+
readonly mounts: Record<`/${string}`, Route.RouteSet.Default>
|
|
103
|
+
readonly _E: () => E
|
|
104
|
+
readonly _R: () => R
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export namespace RouterBuilder {
|
|
108
|
+
export type Any = RouterBuilder<any, any>
|
|
109
|
+
|
|
110
|
+
export type Error<T> = T extends RouterBuilder<infer E, any> ? E : never
|
|
111
|
+
export type Context<T> = T extends RouterBuilder<any, infer R> ? R : never
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const RouterBuilderProto: RouterBuilderMethods & {
|
|
115
|
+
[RouterBuilderTypeId]: typeof RouterBuilderTypeId
|
|
116
|
+
pipe: Pipeable.Pipeable["pipe"]
|
|
117
|
+
} = {
|
|
118
|
+
[RouterBuilderTypeId]: RouterBuilderTypeId,
|
|
119
|
+
|
|
120
|
+
pipe() {
|
|
121
|
+
return Pipeable.pipeArguments(this, arguments)
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
use,
|
|
125
|
+
mount,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type ExtractRouteSetError<T> = T extends Route.RouteSet<infer Routes, any>
|
|
129
|
+
? Routes[number] extends Route.Route<any, any, infer H, any>
|
|
130
|
+
? H extends Route.RouteHandler<any, infer E, any> ? E : never
|
|
131
|
+
: never
|
|
132
|
+
: never
|
|
133
|
+
|
|
134
|
+
type ExtractRouteSetContext<T> = T extends Route.RouteSet<infer Routes, any>
|
|
135
|
+
? Routes[number] extends Route.Route<any, any, infer H, any>
|
|
136
|
+
? H extends Route.RouteHandler<any, any, infer R> ? R : never
|
|
137
|
+
: never
|
|
138
|
+
: never
|
|
139
|
+
|
|
140
|
+
function addRoute<
|
|
141
|
+
E,
|
|
142
|
+
R,
|
|
143
|
+
RouteE,
|
|
144
|
+
RouteR,
|
|
145
|
+
>(
|
|
146
|
+
builder: RouterBuilder<E, R>,
|
|
147
|
+
path: `/${string}`,
|
|
148
|
+
route: Route.RouteSet.Default,
|
|
149
|
+
): RouterBuilder<E | RouteE, R | RouteR> {
|
|
150
|
+
const existingEntry = builder.entries.find((e) => e.path === path)
|
|
151
|
+
if (existingEntry) {
|
|
152
|
+
const updatedEntry: RouterEntry = {
|
|
153
|
+
...existingEntry,
|
|
154
|
+
route: Route.merge(existingEntry.route, route),
|
|
155
|
+
}
|
|
156
|
+
return makeBuilder(
|
|
157
|
+
builder.entries.map((e) => (e.path === path ? updatedEntry : e)),
|
|
158
|
+
builder.globalLayers,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const newEntry: RouterEntry = {
|
|
163
|
+
path,
|
|
164
|
+
route,
|
|
165
|
+
layers: [...builder.globalLayers],
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return makeBuilder([...builder.entries, newEntry], builder.globalLayers)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function addGlobalLayer<E, R>(
|
|
172
|
+
builder: RouterBuilder<E, R>,
|
|
173
|
+
layerRoute: Route.RouteLayer,
|
|
174
|
+
): RouterBuilder<E, R> {
|
|
175
|
+
const newGlobalLayers = [...builder.globalLayers, layerRoute]
|
|
176
|
+
return makeBuilder(builder.entries, newGlobalLayers)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
function findMatchingLayerRoutes(
|
|
181
|
+
route: Route.Route.Default,
|
|
182
|
+
layers: readonly Route.RouteLayer[],
|
|
183
|
+
): Route.Route.Default[] {
|
|
184
|
+
const matchingRoutes: Route.Route.Default[] = []
|
|
185
|
+
for (const layer of layers) {
|
|
186
|
+
for (const layerRoute of layer.set) {
|
|
187
|
+
if (Route.matches(layerRoute, route)) {
|
|
188
|
+
matchingRoutes.push(layerRoute)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return matchingRoutes
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function wrapWithLayerRoute(
|
|
196
|
+
innerRoute: Route.Route.Default,
|
|
197
|
+
layerRoute: Route.Route.Default,
|
|
198
|
+
): Route.Route.Default {
|
|
199
|
+
const handler: Route.RouteHandler = (context) => {
|
|
200
|
+
const contextWithNext: Route.RouteContext = {
|
|
201
|
+
...context,
|
|
202
|
+
next: () => innerRoute.handler(context),
|
|
203
|
+
}
|
|
204
|
+
return layerRoute.handler(contextWithNext)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return Route.make({
|
|
208
|
+
method: layerRoute.method,
|
|
209
|
+
media: layerRoute.media,
|
|
210
|
+
handler,
|
|
211
|
+
schemas: {},
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function applyLayersToRoute(
|
|
216
|
+
route: Route.Route.Default,
|
|
217
|
+
layers: readonly Route.RouteLayer[],
|
|
218
|
+
): Route.Route.Default {
|
|
219
|
+
const matchingLayerRoutes = findMatchingLayerRoutes(route, layers)
|
|
220
|
+
let wrappedRoute = route
|
|
221
|
+
|
|
222
|
+
for (const layerRoute of matchingLayerRoutes.reverse()) {
|
|
223
|
+
wrappedRoute = wrapWithLayerRoute(wrappedRoute, layerRoute)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return wrappedRoute
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function applyLayersToRouteSet(
|
|
230
|
+
routeSet: Route.RouteSet.Default,
|
|
231
|
+
layers: readonly Route.RouteLayer[],
|
|
232
|
+
): Route.RouteSet.Default {
|
|
233
|
+
if (layers.length === 0) {
|
|
234
|
+
return routeSet
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const wrappedRoutes = routeSet.set.map((route) =>
|
|
238
|
+
applyLayersToRoute(route, layers)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
set: wrappedRoutes,
|
|
243
|
+
schema: routeSet.schema,
|
|
244
|
+
} as unknown as Route.RouteSet.Default
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function makeBuilder<E, R>(
|
|
248
|
+
entries: readonly RouterEntry[],
|
|
249
|
+
globalLayers: readonly Route.RouteLayer[] = [],
|
|
250
|
+
): RouterBuilder<E, R> {
|
|
251
|
+
const mounts: Record<`/${string}`, Route.RouteSet.Default> = {}
|
|
252
|
+
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
if (entry.route.set.length > 0) {
|
|
255
|
+
mounts[entry.path] = applyLayersToRouteSet(entry.route, entry.layers)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return Object.assign(Object.create(RouterBuilderProto), {
|
|
260
|
+
entries,
|
|
261
|
+
globalLayers,
|
|
262
|
+
mounts,
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function isRouterBuilder(input: unknown): input is RouterBuilder.Any {
|
|
267
|
+
return Predicate.hasProperty(input, RouterBuilderTypeId)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function use<
|
|
271
|
+
S extends Self,
|
|
272
|
+
>(
|
|
273
|
+
this: S,
|
|
274
|
+
layerRoute: Route.RouteLayer,
|
|
275
|
+
): S extends RouterBuilder<infer E, infer R> ? RouterBuilder<E, R>
|
|
276
|
+
: RouterBuilder<never, never>
|
|
277
|
+
{
|
|
278
|
+
const builder = isRouterBuilder(this)
|
|
279
|
+
? this
|
|
280
|
+
: makeBuilder<never, never>([], [])
|
|
281
|
+
return addGlobalLayer(builder, layerRoute) as any
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function mount<
|
|
285
|
+
S extends Self,
|
|
286
|
+
Routes extends Route.Route.Tuple,
|
|
287
|
+
Schemas extends Route.RouteSchemas,
|
|
288
|
+
>(
|
|
289
|
+
this: S,
|
|
290
|
+
path: `/${string}`,
|
|
291
|
+
route: Route.RouteSet<Routes, Schemas>,
|
|
292
|
+
): S extends RouterBuilder<infer E, infer R> ? RouterBuilder<
|
|
293
|
+
E | ExtractRouteSetError<Route.RouteSet<Routes, Schemas>>,
|
|
294
|
+
R | ExtractRouteSetContext<Route.RouteSet<Routes, Schemas>>
|
|
295
|
+
>
|
|
296
|
+
: RouterBuilder<
|
|
297
|
+
ExtractRouteSetError<Route.RouteSet<Routes, Schemas>>,
|
|
298
|
+
ExtractRouteSetContext<Route.RouteSet<Routes, Schemas>>
|
|
299
|
+
>
|
|
300
|
+
{
|
|
301
|
+
const builder = isRouterBuilder(this)
|
|
302
|
+
? this
|
|
303
|
+
: makeBuilder<never, never>([], [])
|
|
304
|
+
return addRoute(builder, path, route as Route.RouteSet.Default) as any
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function fromManifest(
|
|
308
|
+
manifest: RouterManifest,
|
|
309
|
+
): Effect.Effect<RouterBuilder.Any> {
|
|
310
|
+
return Effect.gen(function*() {
|
|
311
|
+
const loadedEntries = yield* Effect.forEach(
|
|
312
|
+
manifest.routes,
|
|
313
|
+
(lazyRoute) =>
|
|
314
|
+
Effect.gen(function*() {
|
|
315
|
+
const routeModule = yield* Effect.promise(() => lazyRoute.load())
|
|
316
|
+
const layerModules = lazyRoute.layers
|
|
317
|
+
? yield* Effect.forEach(
|
|
318
|
+
lazyRoute.layers,
|
|
319
|
+
(loadLayer) => Effect.promise(() => loadLayer()),
|
|
320
|
+
)
|
|
321
|
+
: []
|
|
322
|
+
|
|
323
|
+
const layers = layerModules
|
|
324
|
+
.map((m: any) => m.default)
|
|
325
|
+
.filter(Route.isRouteLayer)
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
path: lazyRoute.path,
|
|
329
|
+
route: routeModule.default,
|
|
330
|
+
layers,
|
|
331
|
+
}
|
|
332
|
+
}),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return makeBuilder(loadedEntries, [])
|
|
336
|
+
})
|
|
337
|
+
}
|
|
@@ -213,10 +213,36 @@ t.describe(`${RouterPattern.toColon.name}`, () => {
|
|
|
213
213
|
"/files/file_:id.txt",
|
|
214
214
|
])
|
|
215
215
|
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
t.describe(`${RouterPattern.toBun.name}`, () => {
|
|
219
|
+
t.test("literal path unchanged", () => {
|
|
220
|
+
t.expect(RouterPattern.toBun("/")).toEqual(["/"])
|
|
221
|
+
t.expect(RouterPattern.toBun("/about")).toEqual(["/about"])
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
t.test("param [param] -> :param", () => {
|
|
225
|
+
t.expect(RouterPattern.toBun("/users/[id]")).toEqual(["/users/:id"])
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
t.test("optional param [[param]] -> two routes", () => {
|
|
229
|
+
t.expect(RouterPattern.toBun("/users/[[id]]")).toEqual([
|
|
230
|
+
"/users",
|
|
231
|
+
"/users/:id",
|
|
232
|
+
])
|
|
233
|
+
t.expect(RouterPattern.toBun("/[[id]]")).toEqual(["/", "/:id"])
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
t.test("rest param [...param] -> *", () => {
|
|
237
|
+
t.expect(RouterPattern.toBun("/docs/[...path]")).toEqual(["/docs/*"])
|
|
238
|
+
})
|
|
216
239
|
|
|
217
|
-
t.test("
|
|
218
|
-
t.expect(RouterPattern.
|
|
219
|
-
|
|
240
|
+
t.test("optional rest param [[...param]] -> two routes", () => {
|
|
241
|
+
t.expect(RouterPattern.toBun("/docs/[[...path]]")).toEqual([
|
|
242
|
+
"/docs",
|
|
243
|
+
"/docs/*",
|
|
244
|
+
])
|
|
245
|
+
t.expect(RouterPattern.toBun("/[[...path]]")).toEqual(["/", "/*"])
|
|
220
246
|
})
|
|
221
247
|
})
|
|
222
248
|
|
package/src/RouterPattern.ts
CHANGED
|
@@ -365,13 +365,38 @@ export function toRemix(path: Route.RoutePattern): string[] {
|
|
|
365
365
|
return [joined ? "/" + joined : "/"]
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
export const toBun = toColon
|
|
369
|
-
|
|
370
368
|
/**
|
|
371
|
-
*
|
|
369
|
+
* Converts to Bun.serve path pattern.
|
|
370
|
+
*
|
|
371
|
+
* Since Bun doesn't support optional params (`:param?`), optional segments
|
|
372
|
+
* are expanded into multiple routes recursively.
|
|
373
|
+
*
|
|
374
|
+
* - `[param]` → `:param`
|
|
375
|
+
* - `[[param]]` → `/`, `/:param` (two routes)
|
|
376
|
+
* - `[...param]` → `*`
|
|
377
|
+
* - `[[...param]]` → `/`, `/*` (two routes)
|
|
378
|
+
* - `pk_[id]` → `pk_:id`
|
|
372
379
|
*/
|
|
373
|
-
export function
|
|
374
|
-
|
|
380
|
+
export function toBun(path: Route.RoutePattern): string[] {
|
|
381
|
+
const segments = parse(path)
|
|
382
|
+
|
|
383
|
+
const optionalIndex = segments.findIndex(
|
|
384
|
+
(s) =>
|
|
385
|
+
(s._tag === "ParamSegment" || s._tag === "RestSegment") && s.optional,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if (optionalIndex === -1) {
|
|
389
|
+
return buildPaths(segments, colonParamSegment, "/*")
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const before = segments.slice(0, optionalIndex)
|
|
393
|
+
const optional = { ...segments[optionalIndex], optional: false }
|
|
394
|
+
const after = segments.slice(optionalIndex + 1)
|
|
395
|
+
|
|
396
|
+
return [
|
|
397
|
+
...toBun(format(before)),
|
|
398
|
+
...toBun(format([...before, optional, ...after])),
|
|
399
|
+
]
|
|
375
400
|
}
|
|
376
401
|
|
|
377
402
|
type ExtractSegment<S extends string> = S extends `[[...${infer Name}]]`
|
|
@@ -52,3 +52,32 @@ t.it("not found", () =>
|
|
|
52
52
|
)
|
|
53
53
|
.toEqual("Not Found")
|
|
54
54
|
}))
|
|
55
|
+
|
|
56
|
+
t.describe("FetchHandler", () => {
|
|
57
|
+
const FetchClient = TestHttpClient.make((req) =>
|
|
58
|
+
new Response(`Hello from ${req.url}`, { status: 200 })
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
t.it("works with sync handler", () =>
|
|
62
|
+
effect(function*() {
|
|
63
|
+
const res = yield* FetchClient.get("/test")
|
|
64
|
+
|
|
65
|
+
t.expect(res.status).toEqual(200)
|
|
66
|
+
t.expect(yield* res.text).toContain("/test")
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
const AsyncFetchClient = TestHttpClient.make(async (req) => {
|
|
70
|
+
await Promise.resolve()
|
|
71
|
+
return new Response(`Async: ${req.method} ${new URL(req.url).pathname}`, {
|
|
72
|
+
status: 201,
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
t.it("works with async handler", () =>
|
|
77
|
+
effect(function*() {
|
|
78
|
+
const res = yield* AsyncFetchClient.post("/async-path")
|
|
79
|
+
|
|
80
|
+
t.expect(res.status).toEqual(201)
|
|
81
|
+
t.expect(yield* res.text).toEqual("Async: POST /async-path")
|
|
82
|
+
}))
|
|
83
|
+
})
|
package/src/TestHttpClient.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import * as HttpApp from "@effect/platform/HttpApp"
|
|
3
2
|
import * as HttpClient from "@effect/platform/HttpClient"
|
|
4
3
|
import * as HttpClientError from "@effect/platform/HttpClientError"
|
|
@@ -7,15 +6,38 @@ import * as HttpClientResponse from "@effect/platform/HttpClientResponse"
|
|
|
7
6
|
import { RouteNotFound } from "@effect/platform/HttpServerError"
|
|
8
7
|
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
9
8
|
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
9
|
+
import * as UrlParams from "@effect/platform/UrlParams"
|
|
10
10
|
import * as Effect from "effect/Effect"
|
|
11
|
+
import * as Either from "effect/Either"
|
|
11
12
|
import * as Function from "effect/Function"
|
|
12
13
|
import * as Scope from "effect/Scope"
|
|
13
14
|
import * as Stream from "effect/Stream"
|
|
14
15
|
|
|
15
16
|
const WebHeaders = globalThis.Headers
|
|
16
17
|
|
|
17
|
-
export
|
|
18
|
-
|
|
18
|
+
export type FetchHandler = (req: Request) => Response | Promise<Response>
|
|
19
|
+
|
|
20
|
+
export const isFetchHandler = (
|
|
21
|
+
app: unknown,
|
|
22
|
+
): app is FetchHandler => typeof app === "function" && !Effect.isEffect(app)
|
|
23
|
+
|
|
24
|
+
const fromFetchHandler = (
|
|
25
|
+
handler: FetchHandler,
|
|
26
|
+
): HttpApp.Default<never, never> =>
|
|
27
|
+
Effect.gen(function*() {
|
|
28
|
+
const serverRequest = yield* HttpServerRequest.HttpServerRequest
|
|
29
|
+
const webRequest = serverRequest.source as Request
|
|
30
|
+
const response = yield* Effect.promise(async () => handler(webRequest))
|
|
31
|
+
const body = yield* Effect.promise(() => response.arrayBuffer())
|
|
32
|
+
return HttpServerResponse.raw(new Uint8Array(body), {
|
|
33
|
+
status: response.status,
|
|
34
|
+
statusText: response.statusText,
|
|
35
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export const make = <E, R>(
|
|
40
|
+
appOrHandler: HttpApp.Default<E, R> | FetchHandler,
|
|
19
41
|
opts?: {
|
|
20
42
|
baseUrl?: string | null
|
|
21
43
|
handleRouteNotFound?: (
|
|
@@ -24,77 +46,104 @@ export const make = <E = any, R = any>(
|
|
|
24
46
|
},
|
|
25
47
|
): HttpClient.HttpClient.With<
|
|
26
48
|
HttpClientError.HttpClientError | E,
|
|
27
|
-
Exclude<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
HttpClient
|
|
33
|
-
.make(
|
|
34
|
-
(request, url, signal) => {
|
|
35
|
-
const send = (
|
|
36
|
-
body: BodyInit | undefined,
|
|
37
|
-
) => {
|
|
38
|
-
const app = httpApp
|
|
39
|
-
const serverRequest = HttpServerRequest.fromWeb(
|
|
40
|
-
new Request(url.toString(), {
|
|
41
|
-
method: request.method,
|
|
42
|
-
headers: new WebHeaders(request.headers),
|
|
43
|
-
body,
|
|
44
|
-
duplex: request.body._tag === "Stream" ? "half" : undefined,
|
|
45
|
-
signal,
|
|
46
|
-
} as any),
|
|
47
|
-
)
|
|
49
|
+
Exclude<R, HttpServerRequest.HttpServerRequest>
|
|
50
|
+
> => {
|
|
51
|
+
const httpApp: HttpApp.Default<E, R> = isFetchHandler(appOrHandler)
|
|
52
|
+
? fromFetchHandler(appOrHandler) as HttpApp.Default<E, R>
|
|
53
|
+
: appOrHandler
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
68
|
-
}
|
|
55
|
+
const execute = (
|
|
56
|
+
request: HttpClientRequest.HttpClientRequest,
|
|
57
|
+
): Effect.Effect<
|
|
58
|
+
HttpClientResponse.HttpClientResponse,
|
|
59
|
+
HttpClientError.HttpClientError | E,
|
|
60
|
+
Exclude<R, HttpServerRequest.HttpServerRequest>
|
|
61
|
+
> => {
|
|
62
|
+
const urlResult = UrlParams.makeUrl(
|
|
63
|
+
request.url,
|
|
64
|
+
request.urlParams,
|
|
65
|
+
request.hash,
|
|
66
|
+
)
|
|
67
|
+
if (Either.isLeft(urlResult)) {
|
|
68
|
+
return Effect.die(urlResult.left)
|
|
69
|
+
}
|
|
70
|
+
const url = urlResult.right
|
|
71
|
+
const controller = new AbortController()
|
|
72
|
+
const signal = controller.signal
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Stream.toReadableStreamEffect(request.body.stream),
|
|
87
|
-
send,
|
|
88
|
-
)
|
|
89
|
-
}
|
|
74
|
+
const send = (
|
|
75
|
+
body: BodyInit | undefined,
|
|
76
|
+
): Effect.Effect<
|
|
77
|
+
HttpClientResponse.HttpClientResponse,
|
|
78
|
+
E,
|
|
79
|
+
Exclude<R, HttpServerRequest.HttpServerRequest>
|
|
80
|
+
> => {
|
|
81
|
+
const serverRequest = HttpServerRequest.fromWeb(
|
|
82
|
+
new Request(url.toString(), {
|
|
83
|
+
method: request.method,
|
|
84
|
+
headers: new WebHeaders(request.headers),
|
|
85
|
+
body,
|
|
86
|
+
duplex: request.body._tag === "Stream" ? "half" : undefined,
|
|
87
|
+
signal,
|
|
88
|
+
} as RequestInit),
|
|
89
|
+
)
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
? Function.identity
|
|
97
|
-
: HttpClient.mapRequest(
|
|
98
|
-
HttpClientRequest.prependUrl(opts?.baseUrl ?? "http://localhost"),
|
|
91
|
+
return Function.pipe(
|
|
92
|
+
httpApp,
|
|
93
|
+
Effect.provideService(
|
|
94
|
+
HttpServerRequest.HttpServerRequest,
|
|
95
|
+
serverRequest,
|
|
99
96
|
),
|
|
100
|
-
|
|
97
|
+
Effect.andThen(HttpServerResponse.toWeb),
|
|
98
|
+
Effect.andThen(res => HttpClientResponse.fromWeb(request, res)),
|
|
99
|
+
opts?.handleRouteNotFound === null
|
|
100
|
+
? Function.identity
|
|
101
|
+
: Effect.catchAll((e) =>
|
|
102
|
+
e instanceof RouteNotFound
|
|
103
|
+
? Effect.succeed(HttpClientResponse.fromWeb(
|
|
104
|
+
request,
|
|
105
|
+
new Response("Failed with RouteNotFound", {
|
|
106
|
+
status: 404,
|
|
107
|
+
}),
|
|
108
|
+
))
|
|
109
|
+
: Effect.fail(e)
|
|
110
|
+
),
|
|
111
|
+
) as Effect.Effect<
|
|
112
|
+
HttpClientResponse.HttpClientResponse,
|
|
113
|
+
E,
|
|
114
|
+
Exclude<R, HttpServerRequest.HttpServerRequest>
|
|
115
|
+
>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
switch (request.body._tag) {
|
|
119
|
+
case "Raw":
|
|
120
|
+
case "Uint8Array":
|
|
121
|
+
return send(request.body.body as BodyInit)
|
|
122
|
+
case "FormData":
|
|
123
|
+
return send(request.body.formData)
|
|
124
|
+
case "Stream":
|
|
125
|
+
return Effect.flatMap(
|
|
126
|
+
Stream.toReadableStreamEffect(request.body.stream),
|
|
127
|
+
send,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return send(undefined)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const client = HttpClient.makeWith(
|
|
135
|
+
(requestEffect) => Effect.flatMap(requestEffect, execute),
|
|
136
|
+
(request) => Effect.succeed(request),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return client.pipe(
|
|
140
|
+
opts?.baseUrl === null
|
|
141
|
+
? Function.identity
|
|
142
|
+
: HttpClient.mapRequest(
|
|
143
|
+
HttpClientRequest.prependUrl(opts?.baseUrl ?? "http://localhost"),
|
|
144
|
+
),
|
|
145
|
+
) as HttpClient.HttpClient.With<
|
|
146
|
+
HttpClientError.HttpClientError | E,
|
|
147
|
+
Exclude<R, HttpServerRequest.HttpServerRequest>
|
|
148
|
+
>
|
|
149
|
+
}
|
package/src/assets.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allow importing static assets as modules.
|
|
3
|
+
* Bun/Vite resolves these to file paths.
|
|
4
|
+
* For WASM you're better of
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
declare module "*.png"
|
|
8
|
+
declare module "*.gif"
|
|
9
|
+
declare module "*.webp"
|
|
10
|
+
declare module "*.avif"
|
|
11
|
+
declare module "*.ico"
|
|
12
|
+
declare module "*.bmp"
|
|
13
|
+
declare module "*.tiff"
|
|
14
|
+
declare module "*.svg"
|
|
15
|
+
declare module "*.jpeg"
|
|
16
|
+
declare module "*.jpg"
|
|
17
|
+
|
|
18
|
+
declare module "*.woff"
|
|
19
|
+
declare module "*.woff2"
|
|
20
|
+
declare module "*.ttf"
|
|
21
|
+
declare module "*.otf"
|
|
22
|
+
declare module "*.eot"
|
|
23
|
+
|
|
24
|
+
declare module "*.mp4"
|
|
25
|
+
declare module "*.webm"
|
|
26
|
+
declare module "*.ogg"
|
|
27
|
+
declare module "*.mov"
|
|
28
|
+
declare module "*.mp3"
|
|
29
|
+
declare module "*.wav"
|
|
30
|
+
declare module "*.flac"
|
|
31
|
+
declare module "*.aac"
|
|
32
|
+
|
|
33
|
+
declare module "*.pdf"
|
|
34
|
+
declare module "*.xml"
|
|
35
|
+
declare module "*.csv"
|
|
36
|
+
declare module "*.txt"
|
|
37
|
+
declare module "*.md"
|
|
38
|
+
|
|
39
|
+
declare module "*.css"
|