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/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"
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import * as BunContext from "@effect/platform-bun/BunContext"
|
|
2
|
-
import * as BunHttpServer from "@effect/platform-bun/BunHttpServer"
|
|
3
1
|
import * as HttpRouter from "@effect/platform/HttpRouter"
|
|
4
|
-
import * as HttpServer from "@effect/platform/HttpServer"
|
|
5
2
|
import * as t from "bun:test"
|
|
6
3
|
import * as Effect from "effect/Effect"
|
|
7
4
|
import * as Layer from "effect/Layer"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as t from "bun:test"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as BunHttpServer from "./BunHttpServer.ts"
|
|
4
|
+
|
|
5
|
+
t.describe("BunHttpServer smart port selection", () => {
|
|
6
|
+
// Skip when running in TTY because the random port logic requires !isTTY && CLAUDECODE,
|
|
7
|
+
// and process.stdout.isTTY cannot be mocked
|
|
8
|
+
t.test.skipIf(process.stdout.isTTY)(
|
|
9
|
+
"uses random port when PORT not set, isTTY=false, CLAUDECODE set",
|
|
10
|
+
async () => {
|
|
11
|
+
const originalPort = process.env.PORT
|
|
12
|
+
const originalClaudeCode = process.env.CLAUDECODE
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
delete process.env.PORT
|
|
16
|
+
process.env.CLAUDECODE = "1"
|
|
17
|
+
|
|
18
|
+
const port = await Effect.runPromise(
|
|
19
|
+
Effect.scoped(
|
|
20
|
+
Effect.gen(function*() {
|
|
21
|
+
const bunServer = yield* BunHttpServer.make({})
|
|
22
|
+
return bunServer.server.port
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
t.expect(port).not.toBe(3000)
|
|
28
|
+
} finally {
|
|
29
|
+
if (originalPort !== undefined) {
|
|
30
|
+
process.env.PORT = originalPort
|
|
31
|
+
} else {
|
|
32
|
+
delete process.env.PORT
|
|
33
|
+
}
|
|
34
|
+
if (originalClaudeCode !== undefined) {
|
|
35
|
+
process.env.CLAUDECODE = originalClaudeCode
|
|
36
|
+
} else {
|
|
37
|
+
delete process.env.CLAUDECODE
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
t.test("uses explicit PORT even when CLAUDECODE is set", async () => {
|
|
44
|
+
const originalPort = process.env.PORT
|
|
45
|
+
const originalClaudeCode = process.env.CLAUDECODE
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
process.env.PORT = "5678"
|
|
49
|
+
process.env.CLAUDECODE = "1"
|
|
50
|
+
|
|
51
|
+
const port = await Effect.runPromise(
|
|
52
|
+
Effect.scoped(
|
|
53
|
+
Effect.gen(function*() {
|
|
54
|
+
const bunServer = yield* BunHttpServer.make({})
|
|
55
|
+
return bunServer.server.port
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
t.expect(port).toBe(5678)
|
|
61
|
+
} finally {
|
|
62
|
+
if (originalPort !== undefined) {
|
|
63
|
+
process.env.PORT = originalPort
|
|
64
|
+
} else {
|
|
65
|
+
delete process.env.PORT
|
|
66
|
+
}
|
|
67
|
+
if (originalClaudeCode !== undefined) {
|
|
68
|
+
process.env.CLAUDECODE = originalClaudeCode
|
|
69
|
+
} else {
|
|
70
|
+
delete process.env.CLAUDECODE
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import * as HttpApp from "@effect/platform/HttpApp"
|
|
2
|
+
import * as HttpServer from "@effect/platform/HttpServer"
|
|
3
|
+
import * as HttpServerError from "@effect/platform/HttpServerError"
|
|
4
|
+
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
5
|
+
import * as Socket from "@effect/platform/Socket"
|
|
6
|
+
import * as Bun from "bun"
|
|
7
|
+
import * as Config from "effect/Config"
|
|
8
|
+
import * as Context from "effect/Context"
|
|
9
|
+
import * as Deferred from "effect/Deferred"
|
|
10
|
+
import * as Effect from "effect/Effect"
|
|
11
|
+
import * as Exit from "effect/Exit"
|
|
12
|
+
import * as FiberSet from "effect/FiberSet"
|
|
13
|
+
import * as Layer from "effect/Layer"
|
|
14
|
+
import * as Option from "effect/Option"
|
|
15
|
+
import type * as Scope from "effect/Scope"
|
|
16
|
+
import * as Random from "../Random.ts"
|
|
17
|
+
import * as Router from "../Router.ts"
|
|
18
|
+
import EmptyHTML from "./_empty.html"
|
|
19
|
+
import {
|
|
20
|
+
makeResponse,
|
|
21
|
+
ServerRequestImpl,
|
|
22
|
+
WebSocketContext,
|
|
23
|
+
} from "./BunHttpServer_web.ts"
|
|
24
|
+
import * as BunRoute from "./BunRoute.ts"
|
|
25
|
+
|
|
26
|
+
type FetchHandler = (
|
|
27
|
+
request: Request,
|
|
28
|
+
server: Bun.Server<WebSocketContext>,
|
|
29
|
+
) => Response | Promise<Response>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Basically `Omit<Bun.Serve.Options, "fetch" | "error" | "websocket">`
|
|
33
|
+
* TypeScript 5.9 cannot verify discriminated union types used in
|
|
34
|
+
* {@link Bun.serve} so we need to define them explicitly.
|
|
35
|
+
*/
|
|
36
|
+
interface ServeOptions {
|
|
37
|
+
readonly port?: number
|
|
38
|
+
readonly hostname?: string
|
|
39
|
+
readonly reusePort?: boolean
|
|
40
|
+
readonly ipv6Only?: boolean
|
|
41
|
+
readonly idleTimeout?: number
|
|
42
|
+
readonly development?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type BunServer = {
|
|
46
|
+
readonly server: Bun.Server<WebSocketContext>
|
|
47
|
+
readonly addRoutes: (routes: BunRoute.BunRoutes) => void
|
|
48
|
+
// TODO: we probably don't want to expose these methods publicly
|
|
49
|
+
readonly pushHandler: (fetch: FetchHandler) => void
|
|
50
|
+
readonly popHandler: () => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const BunServer = Context.GenericTag<BunServer>(
|
|
54
|
+
"effect-start/BunServer",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
export const make = (
|
|
58
|
+
options: ServeOptions,
|
|
59
|
+
): Effect.Effect<
|
|
60
|
+
BunServer,
|
|
61
|
+
never,
|
|
62
|
+
Scope.Scope
|
|
63
|
+
> =>
|
|
64
|
+
Effect.gen(function*() {
|
|
65
|
+
const port = yield* Config.number("PORT").pipe(
|
|
66
|
+
Effect.catchTag("ConfigError", () => {
|
|
67
|
+
if (
|
|
68
|
+
typeof process !== "undefined"
|
|
69
|
+
&& !process.stdout.isTTY
|
|
70
|
+
&& process.env.CLAUDECODE
|
|
71
|
+
) {
|
|
72
|
+
return Effect.succeed(0)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Effect.succeed(3000)
|
|
76
|
+
}),
|
|
77
|
+
)
|
|
78
|
+
const hostname = yield* Config.string("HOSTNAME").pipe(
|
|
79
|
+
Effect.catchTag("ConfigError", () => Effect.succeed(undefined)),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const handlerStack: Array<FetchHandler> = [
|
|
83
|
+
function(_request, _server) {
|
|
84
|
+
return new Response("not found", { status: 404 })
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
let currentRoutes: BunRoute.BunRoutes = {}
|
|
89
|
+
|
|
90
|
+
// Bun HMR doesn't work on successive calls to `server.reload` if there are no routes
|
|
91
|
+
// on server start. We workaround that by passing a dummy HTMLBundle [2025-11-26]
|
|
92
|
+
// see: https://github.com/oven-sh/bun/issues/23564
|
|
93
|
+
currentRoutes[`/.BunEmptyHtml-${Random.token(6)}`] = EmptyHTML
|
|
94
|
+
|
|
95
|
+
const websocket: Bun.WebSocketHandler<WebSocketContext> = {
|
|
96
|
+
open(ws) {
|
|
97
|
+
Deferred.unsafeDone(ws.data.deferred, Exit.succeed(ws))
|
|
98
|
+
},
|
|
99
|
+
message(ws, message) {
|
|
100
|
+
ws.data.run(message)
|
|
101
|
+
},
|
|
102
|
+
close(ws, code, closeReason) {
|
|
103
|
+
Deferred.unsafeDone(
|
|
104
|
+
ws.data.closeDeferred,
|
|
105
|
+
Socket.defaultCloseCodeIsError(code)
|
|
106
|
+
? Exit.fail(
|
|
107
|
+
new Socket.SocketCloseError({
|
|
108
|
+
reason: "Close",
|
|
109
|
+
code,
|
|
110
|
+
closeReason,
|
|
111
|
+
}),
|
|
112
|
+
)
|
|
113
|
+
: Exit.void,
|
|
114
|
+
)
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const server = Bun.serve({
|
|
119
|
+
port,
|
|
120
|
+
hostname,
|
|
121
|
+
...options,
|
|
122
|
+
routes: currentRoutes,
|
|
123
|
+
fetch: handlerStack[0],
|
|
124
|
+
websocket,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
yield* Effect.addFinalizer(() =>
|
|
128
|
+
Effect.sync(() => {
|
|
129
|
+
server.stop()
|
|
130
|
+
})
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const reload = () => {
|
|
134
|
+
server.reload({
|
|
135
|
+
fetch: handlerStack[handlerStack.length - 1],
|
|
136
|
+
routes: currentRoutes,
|
|
137
|
+
websocket,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return BunServer.of({
|
|
142
|
+
server,
|
|
143
|
+
pushHandler(fetch) {
|
|
144
|
+
handlerStack.push(fetch)
|
|
145
|
+
reload()
|
|
146
|
+
},
|
|
147
|
+
popHandler() {
|
|
148
|
+
handlerStack.pop()
|
|
149
|
+
reload()
|
|
150
|
+
},
|
|
151
|
+
addRoutes(routes) {
|
|
152
|
+
currentRoutes = {
|
|
153
|
+
...currentRoutes,
|
|
154
|
+
...routes,
|
|
155
|
+
}
|
|
156
|
+
reload()
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
export const layer = (
|
|
162
|
+
options?: ServeOptions,
|
|
163
|
+
): Layer.Layer<BunServer> => Layer.scoped(BunServer, make(options ?? {}))
|
|
164
|
+
|
|
165
|
+
export const makeHttpServer: Effect.Effect<
|
|
166
|
+
HttpServer.HttpServer,
|
|
167
|
+
never,
|
|
168
|
+
Scope.Scope | BunServer
|
|
169
|
+
> = Effect.gen(function*() {
|
|
170
|
+
const bunServer = yield* BunServer
|
|
171
|
+
|
|
172
|
+
return HttpServer.make({
|
|
173
|
+
address: {
|
|
174
|
+
_tag: "TcpAddress",
|
|
175
|
+
port: bunServer.server.port!,
|
|
176
|
+
hostname: bunServer.server.hostname!,
|
|
177
|
+
},
|
|
178
|
+
serve(httpApp, middleware) {
|
|
179
|
+
return Effect.gen(function*() {
|
|
180
|
+
const runFork = yield* FiberSet.makeRuntime<never>()
|
|
181
|
+
const runtime = yield* Effect.runtime<never>()
|
|
182
|
+
const app = HttpApp.toHandled(
|
|
183
|
+
httpApp,
|
|
184
|
+
(request, response) =>
|
|
185
|
+
Effect.sync(() => {
|
|
186
|
+
;(request as ServerRequestImpl).resolve(
|
|
187
|
+
makeResponse(request, response, runtime),
|
|
188
|
+
)
|
|
189
|
+
}),
|
|
190
|
+
middleware,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
function handler(
|
|
194
|
+
request: Request,
|
|
195
|
+
server: Bun.Server<WebSocketContext>,
|
|
196
|
+
) {
|
|
197
|
+
return new Promise<Response>((resolve, _reject) => {
|
|
198
|
+
const fiber = runFork(Effect.provideService(
|
|
199
|
+
app,
|
|
200
|
+
HttpServerRequest.HttpServerRequest,
|
|
201
|
+
new ServerRequestImpl(
|
|
202
|
+
request,
|
|
203
|
+
resolve,
|
|
204
|
+
removeHost(request.url),
|
|
205
|
+
server,
|
|
206
|
+
),
|
|
207
|
+
))
|
|
208
|
+
request.signal.addEventListener("abort", () => {
|
|
209
|
+
runFork(
|
|
210
|
+
fiber.interruptAsFork(HttpServerError.clientAbortFiberId),
|
|
211
|
+
)
|
|
212
|
+
}, { once: true })
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
yield* Effect.acquireRelease(
|
|
217
|
+
Effect.sync(() => {
|
|
218
|
+
bunServer.pushHandler(handler)
|
|
219
|
+
}),
|
|
220
|
+
() =>
|
|
221
|
+
Effect.sync(() => {
|
|
222
|
+
bunServer.popHandler()
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
225
|
+
})
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
export const layerServer = (
|
|
231
|
+
options?: ServeOptions,
|
|
232
|
+
): Layer.Layer<HttpServer.HttpServer | BunServer> =>
|
|
233
|
+
Layer.provideMerge(
|
|
234
|
+
Layer.scoped(HttpServer.HttpServer, makeHttpServer),
|
|
235
|
+
layer(options ?? {}),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
export function layerRoutes() {
|
|
239
|
+
return Layer.effectDiscard(
|
|
240
|
+
Effect.gen(function*() {
|
|
241
|
+
const bunServer = yield* BunServer
|
|
242
|
+
const routerContext = yield* Effect.serviceOption(Router.Router)
|
|
243
|
+
|
|
244
|
+
if (Option.isSome(routerContext)) {
|
|
245
|
+
const router = yield* Router.fromManifest(routerContext.value)
|
|
246
|
+
const bunRoutes = yield* BunRoute.routesFromRouter(router)
|
|
247
|
+
bunServer.addRoutes(bunRoutes)
|
|
248
|
+
}
|
|
249
|
+
}),
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const removeHost = (url: string) => {
|
|
254
|
+
if (url[0] === "/") {
|
|
255
|
+
return url
|
|
256
|
+
}
|
|
257
|
+
const index = url.indexOf("/", url.indexOf("//") + 2)
|
|
258
|
+
return index === -1 ? "/" : url.slice(index)
|
|
259
|
+
}
|