effect-start 0.9.0 → 0.10.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.
Files changed (44) hide show
  1. package/package.json +12 -13
  2. package/src/BundleHttp.test.ts +1 -1
  3. package/src/Commander.test.ts +15 -15
  4. package/src/Commander.ts +58 -88
  5. package/src/EncryptedCookies.test.ts +4 -4
  6. package/src/FileHttpRouter.test.ts +81 -12
  7. package/src/FileHttpRouter.ts +115 -26
  8. package/src/FileRouter.ts +60 -162
  9. package/src/FileRouterCodegen.test.ts +250 -64
  10. package/src/FileRouterCodegen.ts +13 -56
  11. package/src/FileRouterPattern.test.ts +116 -0
  12. package/src/FileRouterPattern.ts +59 -0
  13. package/src/FileRouter_path.test.ts +63 -102
  14. package/src/FileSystemExtra.test.ts +226 -0
  15. package/src/FileSystemExtra.ts +24 -60
  16. package/src/HttpUtils.test.ts +68 -0
  17. package/src/HttpUtils.ts +15 -0
  18. package/src/HyperHtml.ts +24 -5
  19. package/src/JsModule.test.ts +1 -1
  20. package/src/NodeFileSystem.ts +764 -0
  21. package/src/Random.ts +59 -0
  22. package/src/Route.test.ts +471 -0
  23. package/src/Route.ts +298 -153
  24. package/src/RouteRender.ts +38 -0
  25. package/src/Router.ts +11 -33
  26. package/src/RouterPattern.test.ts +629 -0
  27. package/src/RouterPattern.ts +391 -0
  28. package/src/Start.ts +14 -52
  29. package/src/bun/BunBundle.test.ts +0 -3
  30. package/src/bun/BunHttpServer.ts +246 -0
  31. package/src/bun/BunHttpServer_web.ts +384 -0
  32. package/src/bun/BunRoute.test.ts +341 -0
  33. package/src/bun/BunRoute.ts +326 -0
  34. package/src/bun/BunRoute_bundles.test.ts +218 -0
  35. package/src/bun/BunRuntime.ts +33 -0
  36. package/src/bun/BunTailwindPlugin.test.ts +1 -1
  37. package/src/bun/_empty.html +1 -0
  38. package/src/bun/index.ts +2 -1
  39. package/src/testing.ts +12 -3
  40. package/src/Datastar.test.ts +0 -267
  41. package/src/Datastar.ts +0 -68
  42. package/src/bun/BunFullstackServer.ts +0 -45
  43. package/src/bun/BunFullstackServer_httpServer.ts +0 -541
  44. package/src/jsx-datastar.d.ts +0 -63
@@ -0,0 +1,341 @@
1
+ import type { HTMLBundle } from "bun"
2
+ import * as t from "bun:test"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Route from "../Route.ts"
5
+ import type * as Router from "../Router.ts"
6
+ import * as BunRoute from "./BunRoute.ts"
7
+
8
+ t.describe(`${BunRoute.loadBundle.name}`, () => {
9
+ t.test("creates BunRoute from HTMLBundle", () => {
10
+ const mockBundle = { index: "index.html" } as HTMLBundle
11
+ const bunRoute = BunRoute.loadBundle(() => Promise.resolve(mockBundle))
12
+
13
+ t.expect(BunRoute.isBunRoute(bunRoute)).toBe(true)
14
+ t.expect(Route.isRouteSet(bunRoute)).toBe(true)
15
+ t.expect(bunRoute.set).toHaveLength(1)
16
+ t.expect(bunRoute.method as string).toBe("GET")
17
+ t.expect(bunRoute.media as string).toBe("text/html")
18
+ })
19
+
20
+ t.test("unwraps default export", async () => {
21
+ const mockBundle = { index: "index.html" } as HTMLBundle
22
+ const bunRoute = BunRoute.loadBundle(() =>
23
+ Promise.resolve({ default: mockBundle })
24
+ )
25
+
26
+ const loaded = await bunRoute.load()
27
+ t.expect(loaded).toBe(mockBundle)
28
+ })
29
+
30
+ t.test("returns bundle directly when no default", async () => {
31
+ const mockBundle = { index: "index.html" } as HTMLBundle
32
+ const bunRoute = BunRoute.loadBundle(() => Promise.resolve(mockBundle))
33
+
34
+ const loaded = await bunRoute.load()
35
+ t.expect(loaded).toBe(mockBundle)
36
+ })
37
+ })
38
+
39
+ t.describe(`${BunRoute.isBunRoute.name}`, () => {
40
+ t.test("returns true for BunRoute", () => {
41
+ const mockBundle = { index: "index.html" } as HTMLBundle
42
+ const bunRoute = BunRoute.loadBundle(() => Promise.resolve(mockBundle))
43
+
44
+ t.expect(BunRoute.isBunRoute(bunRoute)).toBe(true)
45
+ })
46
+
47
+ t.test("returns false for regular Route", () => {
48
+ const route = Route.text(Effect.succeed("hello"))
49
+
50
+ t.expect(BunRoute.isBunRoute(route)).toBe(false)
51
+ })
52
+
53
+ t.test("returns false for non-route values", () => {
54
+ t.expect(BunRoute.isBunRoute(null)).toBe(false)
55
+ t.expect(BunRoute.isBunRoute(undefined)).toBe(false)
56
+ t.expect(BunRoute.isBunRoute({})).toBe(false)
57
+ t.expect(BunRoute.isBunRoute("string")).toBe(false)
58
+ })
59
+ })
60
+
61
+ t.describe(`${BunRoute.routesFromRouter.name}`, () => {
62
+ t.test("converts text route to fetch handler", async () => {
63
+ const fetch = await makeFetch(
64
+ makeRouter([
65
+ { path: "/hello", routes: Route.text(Effect.succeed("Hello World")) },
66
+ ]),
67
+ )
68
+
69
+ const response = await fetch("/hello")
70
+
71
+ t.expect(response.status).toBe(200)
72
+ t.expect(await response.text()).toBe("Hello World")
73
+ })
74
+
75
+ t.test("converts json route to fetch handler", async () => {
76
+ const fetch = await makeFetch(
77
+ makeRouter([
78
+ {
79
+ path: "/api/data",
80
+ routes: Route.json(Effect.succeed({ message: "ok", count: 42 })),
81
+ },
82
+ ]),
83
+ )
84
+
85
+ const response = await fetch("/api/data")
86
+
87
+ t.expect(response.status).toBe(200)
88
+ t.expect(await response.json()).toEqual({ message: "ok", count: 42 })
89
+ })
90
+
91
+ t.test("converts html route to fetch handler", async () => {
92
+ const fetch = await makeFetch(
93
+ makeRouter([
94
+ { path: "/page", routes: Route.html(Effect.succeed("<h1>Title</h1>")) },
95
+ ]),
96
+ )
97
+
98
+ const response = await fetch("/page")
99
+
100
+ t.expect(response.status).toBe(200)
101
+ t.expect(await response.text()).toBe("<h1>Title</h1>")
102
+ })
103
+
104
+ t.test("handles method-specific routes", async () => {
105
+ const fetch = await makeFetch(
106
+ makeRouter([
107
+ {
108
+ path: "/users",
109
+ routes: Route.get(Route.json(Effect.succeed({ users: [] }))).post(
110
+ Route.json(Effect.succeed({ created: true })),
111
+ ),
112
+ },
113
+ ]),
114
+ )
115
+
116
+ const getResponse = await fetch("/users")
117
+ t.expect(await getResponse.json()).toEqual({ users: [] })
118
+
119
+ const postResponse = await fetch("/users", { method: "POST" })
120
+ t.expect(await postResponse.json()).toEqual({ created: true })
121
+ })
122
+
123
+ t.test("converts path syntax to Bun format", async () => {
124
+ const routes = await Effect.runPromise(
125
+ BunRoute.routesFromRouter(
126
+ makeRouter([
127
+ { path: "/users/[id]", routes: Route.text(Effect.succeed("user")) },
128
+ {
129
+ path: "/docs/[...path]",
130
+ routes: Route.text(Effect.succeed("docs")),
131
+ },
132
+ ]),
133
+ ),
134
+ )
135
+
136
+ t.expect(routes["/users/:id"]).toBeDefined()
137
+ t.expect(routes["/docs/*"]).toBeDefined()
138
+ t.expect(routes["/users/[id]"]).toBeUndefined()
139
+ t.expect(routes["/docs/[...path]"]).toBeUndefined()
140
+ })
141
+
142
+ t.test("creates proxy and internal routes for BunRoute", async () => {
143
+ const mockBundle = { index: "index.html" } as HTMLBundle
144
+ const bunRoute = BunRoute.loadBundle(() => Promise.resolve(mockBundle))
145
+
146
+ const routes = await Effect.runPromise(
147
+ BunRoute.routesFromRouter(
148
+ makeRouter([{ path: "/app", routes: bunRoute }]),
149
+ ),
150
+ )
151
+
152
+ const internalPath = Object.keys(routes).find((k) =>
153
+ k.includes("~BunRoute-")
154
+ )
155
+ t.expect(internalPath).toBeDefined()
156
+ t.expect(routes[internalPath!]).toBe(mockBundle)
157
+ t.expect(typeof routes["/app"]).toBe("function")
158
+ })
159
+
160
+ t.test("handles mixed BunRoute and regular routes", async () => {
161
+ const mockBundle = { index: "index.html" } as HTMLBundle
162
+ const bunRoute = BunRoute.loadBundle(() => Promise.resolve(mockBundle))
163
+
164
+ const routes = await Effect.runPromise(
165
+ BunRoute.routesFromRouter({
166
+ routes: [
167
+ {
168
+ path: "/app",
169
+ load: () => Promise.resolve({ default: bunRoute }),
170
+ },
171
+ {
172
+ path: "/api/health",
173
+ load: () =>
174
+ Promise.resolve({
175
+ default: Route.json(Effect.succeed({ ok: true })),
176
+ }),
177
+ },
178
+ ],
179
+ }),
180
+ )
181
+
182
+ const internalPath = Object.keys(routes).find((k) =>
183
+ k.includes("~BunRoute-")
184
+ )
185
+ t.expect(internalPath).toBeDefined()
186
+ t.expect(routes[internalPath!]).toBe(mockBundle)
187
+ t.expect(typeof routes["/app"]).toBe("function")
188
+ t.expect(routes["/api/health"]).toBeDefined()
189
+ t.expect(typeof routes["/api/health"]).toBe("object")
190
+ })
191
+
192
+ t.test("groups multiple methods under same path", async () => {
193
+ const fetch = await makeFetch(
194
+ makeRouter([
195
+ {
196
+ path: "/resource",
197
+ routes: Route
198
+ .get(Route.text(Effect.succeed("get")))
199
+ .post(Route.text(Effect.succeed("post")))
200
+ .del(Route.text(Effect.succeed("delete"))),
201
+ },
202
+ ]),
203
+ )
204
+
205
+ const getRes = await fetch("/resource")
206
+ const postRes = await fetch("/resource", { method: "POST" })
207
+ const delRes = await fetch("/resource", { method: "DELETE" })
208
+
209
+ t.expect(await getRes.text()).toBe("get")
210
+ t.expect(await postRes.text()).toBe("post")
211
+ t.expect(await delRes.text()).toBe("delete")
212
+ })
213
+ })
214
+
215
+ t.describe("fetch handler Response", () => {
216
+ t.test("returns Response instance", async () => {
217
+ const fetch = await makeFetch(
218
+ makeRouter([{
219
+ path: "/test",
220
+ routes: Route.text(Effect.succeed("test")),
221
+ }]),
222
+ )
223
+
224
+ const response = await fetch("/test")
225
+
226
+ t.expect(response).toBeInstanceOf(Response)
227
+ })
228
+
229
+ t.test("text response has correct content-type", async () => {
230
+ const fetch = await makeFetch(
231
+ makeRouter([{
232
+ path: "/text",
233
+ routes: Route.text(Effect.succeed("hello")),
234
+ }]),
235
+ )
236
+
237
+ const response = await fetch("/text")
238
+
239
+ t.expect(response.headers.get("content-type")).toContain("text/plain")
240
+ })
241
+
242
+ t.test("json response has correct content-type", async () => {
243
+ const fetch = await makeFetch(
244
+ makeRouter([{
245
+ path: "/json",
246
+ routes: Route.json(Effect.succeed({ data: 1 })),
247
+ }]),
248
+ )
249
+
250
+ const response = await fetch("/json")
251
+
252
+ t.expect(response.headers.get("content-type")).toContain("application/json")
253
+ })
254
+
255
+ t.test("html response has correct content-type", async () => {
256
+ const fetch = await makeFetch(
257
+ makeRouter([{
258
+ path: "/html",
259
+ routes: Route.html(Effect.succeed("<p>hi</p>")),
260
+ }]),
261
+ )
262
+
263
+ const response = await fetch("/html")
264
+
265
+ t.expect(response.headers.get("content-type")).toContain("text/html")
266
+ })
267
+
268
+ t.test("response body is readable", async () => {
269
+ const fetch = await makeFetch(
270
+ makeRouter([{
271
+ path: "/body",
272
+ routes: Route.text(Effect.succeed("readable body")),
273
+ }]),
274
+ )
275
+
276
+ const response = await fetch("/body")
277
+
278
+ t.expect(response.bodyUsed).toBe(false)
279
+ const text = await response.text()
280
+ t.expect(text).toBe("readable body")
281
+ t.expect(response.bodyUsed).toBe(true)
282
+ })
283
+
284
+ t.test("response ok is true for 200 status", async () => {
285
+ const fetch = await makeFetch(
286
+ makeRouter([{ path: "/ok", routes: Route.text(Effect.succeed("ok")) }]),
287
+ )
288
+
289
+ const response = await fetch("/ok")
290
+
291
+ t.expect(response.ok).toBe(true)
292
+ t.expect(response.status).toBe(200)
293
+ })
294
+ })
295
+
296
+ const makeRouter = (
297
+ routesList: Array<{
298
+ path: `/${string}`
299
+ routes: Route.RouteSet.Default
300
+ }>,
301
+ ): Router.RouterContext => ({
302
+ routes: routesList.map((m) => ({
303
+ path: m.path,
304
+ load: () => Promise.resolve({ default: m.routes }),
305
+ })),
306
+ })
307
+
308
+ type FetchFn = (path: string, init?: { method?: string }) => Promise<Response>
309
+
310
+ type HandlerFn = (
311
+ req: Request,
312
+ server: unknown,
313
+ ) => Response | Promise<Response>
314
+
315
+ async function makeFetch(router: Router.RouterContext): Promise<FetchFn> {
316
+ const routes = await Effect.runPromise(BunRoute.routesFromRouter(router))
317
+ const mockServer = {} as import("bun").Server<unknown>
318
+
319
+ return async (path, init) => {
320
+ const method = init?.method ?? "GET"
321
+ const handler = routes[path]
322
+
323
+ if (!handler) {
324
+ throw new Error(`No handler for path: ${path}`)
325
+ }
326
+
327
+ if (typeof handler === "function") {
328
+ return handler(new Request(`http://localhost${path}`, init), mockServer)
329
+ }
330
+
331
+ const methodHandler = (handler as Record<string, HandlerFn>)[method]
332
+ if (!methodHandler) {
333
+ throw new Error(`No handler for ${method} ${path}`)
334
+ }
335
+
336
+ return methodHandler(
337
+ new Request(`http://localhost${path}`, init),
338
+ mockServer,
339
+ )
340
+ }
341
+ }
@@ -0,0 +1,326 @@
1
+ import * as HttpApp from "@effect/platform/HttpApp"
2
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
3
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
4
+ import type * as Bun from "bun"
5
+ import * as Effect from "effect/Effect"
6
+
7
+ import * as Function from "effect/Function"
8
+ import * as Predicate from "effect/Predicate"
9
+ import type * as Runtime from "effect/Runtime"
10
+ import * as HttpUtils from "../HttpUtils.ts"
11
+ import * as Random from "../Random.ts"
12
+ import * as Route from "../Route.ts"
13
+ import * as Router from "../Router.ts"
14
+ import * as RouteRender from "../RouteRender.ts"
15
+ import * as RouterPattern from "../RouterPattern.ts"
16
+
17
+ const TypeId: unique symbol = Symbol.for("effect-start/BunRoute")
18
+
19
+ export type BunRoute =
20
+ & Route.Route
21
+ & {
22
+ [TypeId]: typeof TypeId
23
+ load: () => Promise<Bun.HTMLBundle>
24
+ }
25
+
26
+ export function loadBundle(
27
+ load: () => Promise<Bun.HTMLBundle | { default: Bun.HTMLBundle }>,
28
+ ): BunRoute {
29
+ const route = Route.make({
30
+ method: "GET",
31
+ media: "text/html",
32
+ handler: () => HttpServerResponse.text("Empty BunRoute"),
33
+ schemas: {},
34
+ })
35
+
36
+ const bunRoute: BunRoute = Object.assign(
37
+ Object.create(route),
38
+ {
39
+ [TypeId]: TypeId,
40
+ load: () => load().then(mod => "default" in mod ? mod.default : mod),
41
+ },
42
+ )
43
+
44
+ bunRoute.set = [bunRoute]
45
+
46
+ return bunRoute
47
+ }
48
+
49
+ export function isBunRoute(input: unknown): input is BunRoute {
50
+ return Predicate.hasProperty(input, TypeId)
51
+ }
52
+
53
+ function findMatchingLayerRoutes(
54
+ route: Route.Route.Default,
55
+ layers: Route.RouteLayer[],
56
+ ): Route.Route.Default[] {
57
+ const matchingRoutes: Route.Route.Default[] = []
58
+ for (const layer of layers) {
59
+ for (const layerRoute of layer.set) {
60
+ if (Route.matches(layerRoute, route)) {
61
+ matchingRoutes.push(layerRoute)
62
+ }
63
+ }
64
+ }
65
+ return matchingRoutes
66
+ }
67
+
68
+ function wrapWithLayerRoute(
69
+ innerRoute: Route.Route.Default,
70
+ layerRoute: Route.Route.Default,
71
+ ): Route.Route.Default {
72
+ const handler: Route.RouteHandler = (context) => {
73
+ const innerNext = () => innerRoute.handler(context)
74
+
75
+ const contextWithNext: Route.RouteContext = {
76
+ ...context,
77
+ next: innerNext,
78
+ }
79
+
80
+ return layerRoute.handler(contextWithNext)
81
+ }
82
+
83
+ return Route.make({
84
+ method: layerRoute.method,
85
+ media: layerRoute.media,
86
+ handler,
87
+ schemas: {},
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Finds BunRoutes in the Router and returns
93
+ * a mapping of paths to their bundles that can be passed
94
+ * to Bun's `serve` function.
95
+ */
96
+ export function bundlesFromRouter(
97
+ router: Router.RouterContext,
98
+ ): Effect.Effect<Record<string, Bun.HTMLBundle>> {
99
+ return Function.pipe(
100
+ Effect.forEach(
101
+ router.routes,
102
+ (mod) =>
103
+ Effect.promise(() =>
104
+ mod.load().then((m) => ({ path: mod.path, exported: m.default }))
105
+ ),
106
+ ),
107
+ Effect.map((modules) =>
108
+ modules.flatMap(({ path, exported }) => {
109
+ if (Route.isRouteSet(exported)) {
110
+ return [...exported.set]
111
+ .filter(isBunRoute)
112
+ .map((route) =>
113
+ [
114
+ path,
115
+ route,
116
+ ] as const
117
+ )
118
+ }
119
+
120
+ return []
121
+ })
122
+ ),
123
+ Effect.flatMap((bunRoutes) =>
124
+ Effect.forEach(
125
+ bunRoutes,
126
+ ([path, route]) =>
127
+ Effect.promise(() =>
128
+ route.load().then((bundle) => {
129
+ const httpPath = RouterPattern.toHttpPath(path)
130
+
131
+ return [httpPath, bundle] as const
132
+ })
133
+ ),
134
+ { concurrency: "unbounded" },
135
+ )
136
+ ),
137
+ Effect.map((entries) =>
138
+ Object.fromEntries(entries) as Record<string, Bun.HTMLBundle>
139
+ ),
140
+ )
141
+ }
142
+
143
+ type BunServerFetchHandler = (
144
+ request: Request,
145
+ server: Bun.Server<unknown>,
146
+ ) => Response | Promise<Response>
147
+
148
+ type BunServerRouteHandler =
149
+ | Bun.HTMLBundle
150
+ | BunServerFetchHandler
151
+ | Partial<Record<Bun.Serve.HTTPMethod, BunServerFetchHandler>>
152
+
153
+ export type BunRoutes = Record<string, BunServerRouteHandler>
154
+
155
+ type MethodHandlers = Partial<
156
+ Record<Bun.Serve.HTTPMethod, BunServerFetchHandler>
157
+ >
158
+
159
+ function isMethodHandlers(value: unknown): value is MethodHandlers {
160
+ return typeof value === "object" && value !== null && !("index" in value)
161
+ }
162
+
163
+ /**
164
+ * Converts a Router into Bun-compatible routes passed to {@link Bun.serve}.
165
+ *
166
+ * For BunRoutes (HtmlBundle), creates two routes:
167
+ * - An internal route at `${path}~BunRoute-${nonce}:${path}` holding the actual HtmlBundle
168
+ * - A proxy route at the original path that forwards requests to the internal route
169
+ *
170
+ * This allows middleware to be attached to the proxy route while Bun handles
171
+ * the HtmlBundle natively on the internal route.
172
+ */
173
+ export function routesFromRouter(
174
+ router: Router.RouterContext,
175
+ runtime?: Runtime.Runtime<never>,
176
+ ): Effect.Effect<BunRoutes> {
177
+ return Effect.gen(function*() {
178
+ const rt = runtime ?? (yield* Effect.runtime<never>())
179
+ const nonce = Random.token(6)
180
+
181
+ const loadedRoutes = yield* Effect.forEach(
182
+ router.routes,
183
+ (mod) =>
184
+ Effect.gen(function*() {
185
+ const routeModule = yield* Effect.promise(() => mod.load())
186
+
187
+ const layerModules = mod.layers
188
+ ? yield* Effect.forEach(
189
+ mod.layers,
190
+ (layerLoad) => Effect.promise(() => layerLoad()),
191
+ )
192
+ : []
193
+
194
+ const layers = layerModules
195
+ .map((m: any) => m.default)
196
+ .filter(Route.isRouteLayer)
197
+
198
+ return {
199
+ path: mod.path,
200
+ exported: routeModule.default,
201
+ layers,
202
+ }
203
+ }),
204
+ )
205
+
206
+ const result: BunRoutes = {}
207
+
208
+ for (const { path, exported, layers } of loadedRoutes) {
209
+ const httpPaths = RouterPattern.toBun(path)
210
+
211
+ const byMethod = new Map<Route.RouteMethod, Route.Route.Default[]>()
212
+ for (const route of exported.set) {
213
+ if (isBunRoute(route)) {
214
+ const bundle = yield* Effect.promise(() => route.load())
215
+ const internalPath = `${path}~BunRoute-${nonce}`
216
+
217
+ result[internalPath] = bundle
218
+
219
+ const proxyHandler: BunServerFetchHandler = (request) => {
220
+ const url = new URL(internalPath, request.url)
221
+ return fetch(new Request(url, request))
222
+ }
223
+
224
+ for (const httpPath of httpPaths) {
225
+ if (!(httpPath in result)) {
226
+ result[httpPath] = proxyHandler
227
+ }
228
+ }
229
+ } else {
230
+ const existing = byMethod.get(route.method) ?? []
231
+ existing.push(route)
232
+ byMethod.set(route.method, existing)
233
+ }
234
+ }
235
+
236
+ for (const [method, routes] of byMethod) {
237
+ const httpApp = Effect.gen(function*() {
238
+ const request = yield* HttpServerRequest.HttpServerRequest
239
+ const accept = request.headers.accept ?? ""
240
+
241
+ let selectedRoute: Route.Route.Default | undefined
242
+
243
+ if (accept.includes("application/json")) {
244
+ selectedRoute = routes.find((r) => r.media === "application/json")
245
+ }
246
+ if (!selectedRoute && accept.includes("text/plain")) {
247
+ selectedRoute = routes.find((r) => r.media === "text/plain")
248
+ }
249
+ if (
250
+ !selectedRoute
251
+ && (accept.includes("text/html")
252
+ || accept.includes("*/*")
253
+ || !accept)
254
+ ) {
255
+ selectedRoute = routes.find((r) => r.media === "text/html")
256
+ }
257
+ if (!selectedRoute) {
258
+ selectedRoute = routes[0]
259
+ }
260
+
261
+ if (!selectedRoute) {
262
+ return HttpServerResponse.empty({ status: 406 })
263
+ }
264
+
265
+ const matchingLayerRoutes = findMatchingLayerRoutes(
266
+ selectedRoute,
267
+ layers,
268
+ )
269
+ let wrappedRoute = selectedRoute
270
+ for (const layerRoute of matchingLayerRoutes.reverse()) {
271
+ wrappedRoute = wrapWithLayerRoute(wrappedRoute, layerRoute)
272
+ }
273
+
274
+ const context: Route.RouteContext = {
275
+ request,
276
+ get url() {
277
+ return HttpUtils.makeUrlFromRequest(request)
278
+ },
279
+ slots: {},
280
+ next: () => Effect.void,
281
+ }
282
+
283
+ return yield* RouteRender.render(wrappedRoute, context)
284
+ })
285
+
286
+ const allMiddleware = layers
287
+ .map((layer) => layer.httpMiddleware)
288
+ .filter((m): m is Route.HttpMiddlewareFunction => m !== undefined)
289
+
290
+ let finalHandler = httpApp
291
+ for (const middleware of allMiddleware) {
292
+ finalHandler = middleware(finalHandler)
293
+ }
294
+
295
+ const webHandler = HttpApp.toWebHandlerRuntime(rt)(finalHandler)
296
+ const handler: BunServerFetchHandler = (request) => webHandler(request)
297
+
298
+ for (const httpPath of httpPaths) {
299
+ if (method === "*") {
300
+ if (!(httpPath in result)) {
301
+ result[httpPath] = handler
302
+ }
303
+ } else {
304
+ const existing = result[httpPath]
305
+ if (isMethodHandlers(existing)) {
306
+ existing[method] = handler
307
+ } else if (!(httpPath in result)) {
308
+ result[httpPath] = { [method]: handler }
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ return result
316
+ })
317
+ }
318
+
319
+ export const isHTMLBundle = (handle: any) => {
320
+ return (
321
+ typeof handle === "object"
322
+ && handle !== null
323
+ && (handle.toString() === "[object HTMLBundle]"
324
+ || typeof handle.index === "string")
325
+ )
326
+ }