effect-start 0.15.0 → 0.17.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 (46) hide show
  1. package/package.json +2 -1
  2. package/src/ContentNegotiation.test.ts +103 -0
  3. package/src/ContentNegotiation.ts +10 -3
  4. package/src/Development.test.ts +119 -0
  5. package/src/Development.ts +137 -0
  6. package/src/Entity.test.ts +592 -0
  7. package/src/Entity.ts +359 -0
  8. package/src/FileRouter.ts +2 -2
  9. package/src/Http.test.ts +315 -20
  10. package/src/Http.ts +153 -11
  11. package/src/PathPattern.ts +3 -1
  12. package/src/Route.ts +26 -10
  13. package/src/RouteBody.test.ts +98 -66
  14. package/src/RouteBody.ts +125 -35
  15. package/src/RouteHook.ts +15 -14
  16. package/src/RouteHttp.test.ts +2549 -83
  17. package/src/RouteHttp.ts +337 -113
  18. package/src/RouteHttpTracer.ts +92 -0
  19. package/src/RouteMount.test.ts +23 -10
  20. package/src/RouteMount.ts +161 -4
  21. package/src/RouteSchema.test.ts +346 -0
  22. package/src/RouteSchema.ts +386 -7
  23. package/src/RouteSse.test.ts +249 -0
  24. package/src/RouteSse.ts +195 -0
  25. package/src/RouteTree.test.ts +233 -85
  26. package/src/RouteTree.ts +98 -44
  27. package/src/StreamExtra.ts +21 -1
  28. package/src/Values.test.ts +263 -0
  29. package/src/Values.ts +68 -6
  30. package/src/bun/BunBundle.ts +0 -73
  31. package/src/bun/BunHttpServer.ts +23 -7
  32. package/src/bun/BunRoute.test.ts +162 -0
  33. package/src/bun/BunRoute.ts +144 -105
  34. package/src/hyper/HyperHtml.test.ts +119 -0
  35. package/src/hyper/HyperHtml.ts +10 -2
  36. package/src/hyper/HyperNode.ts +2 -0
  37. package/src/hyper/HyperRoute.test.tsx +197 -0
  38. package/src/hyper/HyperRoute.ts +61 -0
  39. package/src/hyper/index.ts +4 -0
  40. package/src/hyper/jsx.d.ts +15 -0
  41. package/src/index.ts +2 -0
  42. package/src/node/FileSystem.ts +8 -0
  43. package/src/testing/TestLogger.test.ts +0 -3
  44. package/src/testing/TestLogger.ts +15 -9
  45. package/src/FileSystemExtra.test.ts +0 -242
  46. package/src/FileSystemExtra.ts +0 -66
package/src/RouteTree.ts CHANGED
@@ -6,21 +6,31 @@ import * as RouteMount from "./RouteMount.ts"
6
6
  const TypeId: unique symbol = Symbol.for("effect-start/RouteTree")
7
7
  const RouteTreeRoutes: unique symbol = Symbol()
8
8
 
9
+ type MethodRoute = Route.Route.With<{ method: string }>
10
+
11
+ export type RouteTuple = Iterable<MethodRoute>
12
+
13
+ export type LayerRoute = Iterable<Route.Route.With<{ method: "*" }>>
14
+
15
+ type LayerKey = "*"
16
+ const LayerKey: LayerKey = "*"
17
+
18
+ export type InputRouteMap = {
19
+ [LayerKey]?: LayerRoute
20
+ } & {
21
+ [path: PathPattern.PathPattern]: RouteTuple | RouteTree
22
+ }
23
+
9
24
  export type RouteMap = {
10
- [path: PathPattern.PathPattern]: Iterable<
11
- Route.Route.With<{
12
- method: RouteMount.RouteMount.Method
13
- format?: string
14
- }>
15
- >
25
+ [path: PathPattern.PathPattern]: Route.Route.Tuple
16
26
  }
17
27
 
18
28
  export type Routes<
19
- T extends RouteTree<any>,
29
+ T extends RouteTree,
20
30
  > = T[typeof RouteTreeRoutes]
21
31
 
22
32
  export interface RouteTree<
23
- Routes extends RouteMap = {},
33
+ Routes extends RouteMap = RouteMap,
24
34
  > {
25
35
  [TypeId]: typeof TypeId
26
36
  [RouteTreeRoutes]: Routes
@@ -74,19 +84,78 @@ function sortRoutes(input: RouteMap): RouteMap {
74
84
  return sorted
75
85
  }
76
86
 
87
+ type PrefixKeys<T, Prefix extends string> = {
88
+ [K in keyof T as K extends string ? `${Prefix}${K}` : never]: T[K]
89
+ }
90
+
91
+ type InferItems<T> = T extends Route.RouteSet.Data<any, any, infer M> ? M
92
+ : []
93
+
94
+ type LayerItems<T extends InputRouteMap> = "*" extends keyof T
95
+ ? InferItems<T["*"]>
96
+ : []
97
+
98
+ type FlattenRouteMap<T extends InputRouteMap> =
99
+ & {
100
+ [K in Exclude<keyof T, "*"> as T[K] extends RouteTree ? never : K]: [
101
+ ...LayerItems<T>,
102
+ ...InferItems<T[K]>,
103
+ ]
104
+ }
105
+ & UnionToIntersection<FlattenNested<T, Exclude<keyof T, "*">, LayerItems<T>>>
106
+
107
+ type FlattenNested<
108
+ T,
109
+ K,
110
+ L extends Route.Route.Tuple,
111
+ > = K extends keyof T
112
+ ? T[K] extends RouteTree<infer R>
113
+ ? PrefixKeys<PrependLayers<R, L>, K & string>
114
+ : {}
115
+ : {}
116
+
117
+ type PrependLayers<T extends RouteMap, L extends Route.Route.Tuple> = {
118
+ [K in keyof T]: T[K] extends Route.Route.Tuple ? [...L, ...T[K]] : never
119
+ }
120
+
121
+ type UnionToIntersection<U> = (
122
+ U extends any ? (x: U) => void : never
123
+ ) extends (x: infer I) => void ? I
124
+ : never
125
+
77
126
  export function make<
78
- const Routes extends RouteMap,
127
+ const Routes extends InputRouteMap,
79
128
  >(
80
- routes: Routes,
81
- ): RouteTree<
82
- {
83
- [K in keyof Routes]: Route.RouteSet.Infer<Routes[K]>
129
+ input: Routes,
130
+ ): RouteTree<FlattenRouteMap<Routes>> {
131
+ const layerRoutes = [...(input[LayerKey] ?? [])]
132
+ const merged: RouteMap = {}
133
+
134
+ function flatten(
135
+ map: InputRouteMap,
136
+ prefix: string,
137
+ layers: MethodRoute[],
138
+ ): void {
139
+ for (const key of Object.keys(map)) {
140
+ if (key === LayerKey) continue
141
+ const path = key as PathPattern.PathPattern
142
+ const entry = map[path]
143
+ const fullPath = `${prefix}${path}` as PathPattern.PathPattern
144
+
145
+ if (isRouteTree(entry)) {
146
+ flatten(routes(entry), fullPath, layers)
147
+ } else {
148
+ merged[fullPath] = [...layers, ...(entry as RouteTuple)]
149
+ }
150
+ }
84
151
  }
85
- > {
152
+
153
+ flatten(input, "", layerRoutes)
154
+
86
155
  return {
87
156
  [TypeId]: TypeId,
88
- [RouteTreeRoutes]: sortRoutes(routes),
89
- } as RouteTree<{ [K in keyof Routes]: Route.RouteSet.Infer<Routes[K]> }>
157
+ [RouteTreeRoutes]: sortRoutes(merged),
158
+ } as RouteTree<FlattenRouteMap<Routes>>
90
159
  }
91
160
 
92
161
  export type WalkDescriptor = {
@@ -94,44 +163,29 @@ export type WalkDescriptor = {
94
163
  method: string
95
164
  } & Route.RouteDescriptor.Any
96
165
 
97
- function* flattenItems(
166
+ function* flattenRoutes(
98
167
  path: PathPattern.PathPattern,
99
- items: Route.Route.Tuple,
100
- parentDescriptor: { method: string } & Route.RouteDescriptor.Any,
168
+ routes: Iterable<MethodRoute>,
101
169
  ): Generator<RouteMount.MountedRoute> {
102
- for (const item of items) {
103
- if (Route.isRoute(item)) {
104
- const mergedDescriptor = {
105
- ...parentDescriptor,
106
- ...Route.descriptor(item),
107
- path,
108
- }
109
- yield Route.make(
110
- // handler receives mergedDescriptor (which includes path) at runtime
111
- item.handler as any,
112
- mergedDescriptor,
113
- ) as RouteMount.MountedRoute
114
- } else if (Route.isRouteSet(item)) {
115
- const mergedDescriptor = {
116
- ...parentDescriptor,
117
- ...Route.descriptor(item),
118
- }
119
- yield* flattenItems(path, Route.items(item), mergedDescriptor)
170
+ for (const route of routes) {
171
+ const descriptor = {
172
+ ...route[Route.RouteDescriptor],
173
+ path,
120
174
  }
175
+ yield Route.make(
176
+ route.handler as any,
177
+ descriptor,
178
+ ) as RouteMount.MountedRoute
121
179
  }
122
180
  }
123
181
 
124
182
  export function* walk(
125
183
  tree: RouteTree,
126
184
  ): Generator<RouteMount.MountedRoute> {
127
- const _routes = routes(tree)
185
+ const _routes = routes(tree) as RouteMap
186
+
128
187
  for (const path of Object.keys(_routes) as PathPattern.PathPattern[]) {
129
- const routeSet = _routes[path]
130
- yield* flattenItems(
131
- path,
132
- Route.items(routeSet),
133
- Route.descriptor(routeSet),
134
- )
188
+ yield* flattenRoutes(path, _routes[path])
135
189
  }
136
190
  }
137
191
 
@@ -4,11 +4,31 @@ import * as Fiber from "effect/Fiber"
4
4
  import { dual } from "effect/Function"
5
5
  import * as Predicate from "effect/Predicate"
6
6
  import * as Runtime from "effect/Runtime"
7
- import * as Stream from "effect/Stream"
8
7
  import {
9
8
  runForEachChunk,
10
9
  StreamTypeId,
11
10
  } from "effect/Stream"
11
+ import type * as Stream from "effect/Stream"
12
+
13
+ export const isStream = (
14
+ u: unknown,
15
+ ): u is Stream.Stream<unknown, unknown, unknown> =>
16
+ Predicate.hasProperty(u, StreamTypeId)
17
+
18
+ export type IsStream<T> = T extends Stream.Stream<infer _A, infer _E, infer _R>
19
+ ? true
20
+ : false
21
+
22
+ export type Chunk<T> = T extends Stream.Stream<infer A, infer _E, infer _R> ? A
23
+ : never
24
+
25
+ export type StreamError<T> = T extends
26
+ Stream.Stream<infer _A, infer E, infer _R> ? E
27
+ : never
28
+
29
+ export type Context<T> = T extends Stream.Stream<infer _A, infer _E, infer R>
30
+ ? R
31
+ : never
12
32
 
13
33
  /**
14
34
  * Patched version of original Stream.toReadableStreamRuntime (v3.14.4) to
@@ -0,0 +1,263 @@
1
+ import * as test from "bun:test"
2
+ import * as Values from "./Values.ts"
3
+
4
+ test.describe("isPlainObject", () => {
5
+ test.it("returns true for plain objects", () => {
6
+ test
7
+ .expect(Values.isPlainObject({}))
8
+ .toBe(true)
9
+
10
+ test
11
+ .expect(Values.isPlainObject({ a: 1, b: 2 }))
12
+ .toBe(true)
13
+
14
+ test
15
+ .expect(Values.isPlainObject({ nested: { value: true } }))
16
+ .toBe(true)
17
+ })
18
+
19
+ test.it("returns false for null", () => {
20
+ test
21
+ .expect(Values.isPlainObject(null))
22
+ .toBe(false)
23
+ })
24
+
25
+ test.it("returns false for primitives", () => {
26
+ test
27
+ .expect(Values.isPlainObject("string"))
28
+ .toBe(false)
29
+
30
+ test
31
+ .expect(Values.isPlainObject(42))
32
+ .toBe(false)
33
+
34
+ test
35
+ .expect(Values.isPlainObject(true))
36
+ .toBe(false)
37
+
38
+ test
39
+ .expect(Values.isPlainObject(undefined))
40
+ .toBe(false)
41
+ })
42
+
43
+ test.it("returns false for ArrayBuffer", () => {
44
+ test
45
+ .expect(Values.isPlainObject(new ArrayBuffer(8)))
46
+ .toBe(false)
47
+ })
48
+
49
+ test.it("returns false for ArrayBufferView (Uint8Array)", () => {
50
+ test
51
+ .expect(Values.isPlainObject(new Uint8Array([1, 2, 3])))
52
+ .toBe(false)
53
+ })
54
+
55
+ test.it("returns false for Blob", () => {
56
+ test
57
+ .expect(Values.isPlainObject(new Blob(["test"])))
58
+ .toBe(false)
59
+ })
60
+
61
+ test.it("returns false for FormData", () => {
62
+ test
63
+ .expect(Values.isPlainObject(new FormData()))
64
+ .toBe(false)
65
+ })
66
+
67
+ test.it("returns false for URLSearchParams", () => {
68
+ test
69
+ .expect(Values.isPlainObject(new URLSearchParams()))
70
+ .toBe(false)
71
+ })
72
+
73
+ test.it("returns false for ReadableStream", () => {
74
+ test
75
+ .expect(Values.isPlainObject(new ReadableStream()))
76
+ .toBe(false)
77
+ })
78
+
79
+ test.it("returns false for arrays", () => {
80
+ test
81
+ .expect(Values.isPlainObject([]))
82
+ .toBe(false)
83
+
84
+ test
85
+ .expect(Values.isPlainObject([1, 2, 3]))
86
+ .toBe(false)
87
+ })
88
+
89
+ test.it("returns false for functions", () => {
90
+ test
91
+ .expect(Values.isPlainObject(() => {}))
92
+ .toBe(false)
93
+
94
+ test
95
+ .expect(Values.isPlainObject(function() {}))
96
+ .toBe(false)
97
+ })
98
+
99
+ test.it("returns false for class instances", () => {
100
+ class MyClass {
101
+ value = 42
102
+ }
103
+
104
+ test
105
+ .expect(Values.isPlainObject(new MyClass()))
106
+ .toBe(false)
107
+ })
108
+
109
+ test.it("returns false for Date", () => {
110
+ test
111
+ .expect(Values.isPlainObject(new Date()))
112
+ .toBe(false)
113
+ })
114
+
115
+ test.it("returns false for Map", () => {
116
+ test
117
+ .expect(Values.isPlainObject(new Map()))
118
+ .toBe(false)
119
+ })
120
+
121
+ test.it("returns false for Set", () => {
122
+ test
123
+ .expect(Values.isPlainObject(new Set()))
124
+ .toBe(false)
125
+ })
126
+
127
+ test.it("returns false for RegExp", () => {
128
+ test
129
+ .expect(Values.isPlainObject(/test/))
130
+ .toBe(false)
131
+ })
132
+
133
+ test.it("returns false for Error", () => {
134
+ test
135
+ .expect(Values.isPlainObject(new Error("test")))
136
+ .toBe(false)
137
+ })
138
+
139
+ test.it("returns false for Promise", () => {
140
+ test
141
+ .expect(Values.isPlainObject(Promise.resolve()))
142
+ .toBe(false)
143
+ })
144
+
145
+ test.it("returns true for Object.create(null)", () => {
146
+ test
147
+ .expect(Values.isPlainObject(Object.create(null)))
148
+ .toBe(true)
149
+ })
150
+ })
151
+
152
+ test.describe("IsPlainObject", () => {
153
+ test.it("returns true for plain objects", () => {
154
+ test
155
+ .expectTypeOf<Values.IsPlainObject<{ a: number }>>()
156
+ .toEqualTypeOf<true>()
157
+
158
+ test
159
+ .expectTypeOf<Values.IsPlainObject<{ a: number; b: string }>>()
160
+ .toEqualTypeOf<true>()
161
+
162
+ test
163
+ .expectTypeOf<Values.IsPlainObject<{}>>()
164
+ .toEqualTypeOf<true>()
165
+ })
166
+
167
+ test.it("returns false for functions", () => {
168
+ test
169
+ .expectTypeOf<Values.IsPlainObject<() => void>>()
170
+ .toEqualTypeOf<false>()
171
+
172
+ test
173
+ .expectTypeOf<Values.IsPlainObject<(a: number) => string>>()
174
+ .toEqualTypeOf<false>()
175
+ })
176
+
177
+ test.it("returns false for built-in classes", () => {
178
+ test
179
+ .expectTypeOf<Values.IsPlainObject<Request>>()
180
+ .toEqualTypeOf<false>()
181
+
182
+ test
183
+ .expectTypeOf<Values.IsPlainObject<Response>>()
184
+ .toEqualTypeOf<false>()
185
+
186
+ test
187
+ .expectTypeOf<Values.IsPlainObject<Date>>()
188
+ .toEqualTypeOf<false>()
189
+
190
+ test
191
+ .expectTypeOf<Values.IsPlainObject<Map<string, number>>>()
192
+ .toEqualTypeOf<false>()
193
+ })
194
+
195
+ test.it("returns false for primitives", () => {
196
+ test
197
+ .expectTypeOf<Values.IsPlainObject<string>>()
198
+ .toEqualTypeOf<false>()
199
+
200
+ test
201
+ .expectTypeOf<Values.IsPlainObject<number>>()
202
+ .toEqualTypeOf<false>()
203
+
204
+ test
205
+ .expectTypeOf<Values.IsPlainObject<boolean>>()
206
+ .toEqualTypeOf<false>()
207
+ })
208
+ })
209
+
210
+ test.describe("Simplify", () => {
211
+ test.it("expands nested plain objects", () => {
212
+ type Input = {
213
+ readonly a: { readonly x: number }
214
+ readonly b: string
215
+ }
216
+
217
+ test
218
+ .expectTypeOf<Values.Simplify<Input>>()
219
+ .toEqualTypeOf<{
220
+ a: { x: number }
221
+ b: string
222
+ }>()
223
+ })
224
+
225
+ test.it("preserves Request type", () => {
226
+ type Input = {
227
+ request: Request
228
+ data: { value: number }
229
+ }
230
+
231
+ type Result = Values.Simplify<Input>
232
+
233
+ test
234
+ .expectTypeOf<Result["request"]>()
235
+ .toEqualTypeOf<Request>()
236
+
237
+ test
238
+ .expectTypeOf<Result["data"]>()
239
+ .toEqualTypeOf<{ value: number }>()
240
+ })
241
+
242
+ test.it("preserves other built-in types", () => {
243
+ type Input = {
244
+ date: Date
245
+ map: Map<string, number>
246
+ response: Response
247
+ }
248
+
249
+ type Result = Values.Simplify<Input>
250
+
251
+ test
252
+ .expectTypeOf<Result["date"]>()
253
+ .toEqualTypeOf<Date>()
254
+
255
+ test
256
+ .expectTypeOf<Result["map"]>()
257
+ .toEqualTypeOf<Map<string, number>>()
258
+
259
+ test
260
+ .expectTypeOf<Result["response"]>()
261
+ .toEqualTypeOf<Response>()
262
+ })
263
+ })
package/src/Values.ts CHANGED
@@ -4,13 +4,75 @@ type JsonPrimitives =
4
4
  | boolean
5
5
  | null
6
6
 
7
+ export type JsonObject = {
8
+ [key: string]:
9
+ | Json
10
+ // undefined won't be included in JSON objects but this will allow
11
+ // to use Json type in functions that return object of multiple shapes
12
+ | undefined
13
+ }
14
+
7
15
  export type Json =
8
16
  | JsonPrimitives
9
17
  | Json[]
10
- | {
11
- [key: string]:
12
- | Json
13
- // undefined won't be included in JSON objects but this will allow
14
- // to use Json type in functions that return object of multiple shapes
15
- | undefined
18
+ | JsonObject
19
+
20
+ export function isPlainObject(
21
+ value: unknown,
22
+ ): value is Record<string, unknown> {
23
+ if (value === null || typeof value !== "object") {
24
+ return false
16
25
  }
26
+
27
+ // Check for built-in types and web APIs
28
+ if (
29
+ ArrayBuffer.isView(value)
30
+ || value instanceof ArrayBuffer
31
+ || value instanceof Blob
32
+ || value instanceof FormData
33
+ || value instanceof URLSearchParams
34
+ || value instanceof ReadableStream
35
+ || value instanceof Date
36
+ || value instanceof Map
37
+ || value instanceof Set
38
+ || value instanceof RegExp
39
+ || value instanceof Error
40
+ || value instanceof Promise
41
+ || Array.isArray(value)
42
+ ) {
43
+ return false
44
+ }
45
+
46
+ // Check if it's a plain object (Object.prototype or null prototype)
47
+ const proto = Object.getPrototypeOf(value)
48
+ return proto === null || proto === Object.prototype
49
+ }
50
+
51
+ /**
52
+ * Type helper that returns `true` if type T has any method properties,
53
+ * otherwise returns `never`.
54
+ *
55
+ * Used internally by IsPlainObject to distinguish plain objects from
56
+ * class instances or objects with methods.
57
+ */
58
+ type HasMethod<T> = {
59
+ [K in keyof T]: T[K] extends (...args: any[]) => any ? true : never
60
+ }[keyof T]
61
+
62
+ export type IsPlainObject<T> = T extends object ? T extends Function ? false
63
+ : HasMethod<T> extends never ? true
64
+ : false
65
+ : false
66
+
67
+ export type Simplify<T> = {
68
+ -readonly [K in keyof T]: IsPlainObject<T[K]> extends true
69
+ ? { -readonly [P in keyof T[K]]: T[K][P] }
70
+ : T[K]
71
+ } extends infer U ? { [K in keyof U]: U[K] } : never
72
+
73
+ export const concatBytes = (a: Uint8Array, b: Uint8Array): Uint8Array => {
74
+ const result = new Uint8Array(a.byteLength + b.byteLength)
75
+ result.set(a)
76
+ result.set(b, a.byteLength)
77
+ return result
78
+ }
@@ -9,10 +9,7 @@ import {
9
9
  Iterable,
10
10
  Layer,
11
11
  pipe,
12
- PubSub,
13
12
  Record,
14
- Stream,
15
- SynchronizedRef,
16
13
  } from "effect"
17
14
  import * as NPath from "node:path"
18
15
  import type {
@@ -20,7 +17,6 @@ import type {
20
17
  BundleManifest,
21
18
  } from "../bundler/Bundle.ts"
22
19
  import * as Bundle from "../bundler/Bundle.ts"
23
- import * as FileSystemExtra from "../FileSystemExtra.ts"
24
20
  import { BunImportTrackerPlugin } from "./index.ts"
25
21
 
26
22
  export type BuildOptions = Omit<
@@ -125,75 +121,6 @@ export function layer<T>(
125
121
  return Layer.effect(tag, build(config))
126
122
  }
127
123
 
128
- export function layerDev<T>(
129
- tag: Context.Tag<T, BundleContext>,
130
- config: BuildOptions,
131
- ) {
132
- return Layer.scoped(
133
- tag,
134
- Effect.gen(function*() {
135
- const loadRefKey = "_loadRef"
136
- const sharedBundle = yield* build(config)
137
-
138
- const loadRef = yield* SynchronizedRef.make(null)
139
- sharedBundle[loadRefKey] = loadRef
140
- sharedBundle.events = yield* PubSub.unbounded<Bundle.BundleEvent>()
141
-
142
- yield* Effect.fork(
143
- pipe(
144
- FileSystemExtra.watchSource({
145
- filter: FileSystemExtra.filterSourceFiles,
146
- }),
147
- Stream.map(v =>
148
- ({
149
- _tag: "Change",
150
- path: v.path,
151
- }) as Bundle.BundleEvent
152
- ),
153
- Stream.onError(err =>
154
- Effect.logError("Error while watching files", err)
155
- ),
156
- Stream.runForEach((v) =>
157
- pipe(
158
- Effect.gen(function*() {
159
- yield* Effect.logDebug("Updating bundle: " + tag.key)
160
-
161
- const newBundle = yield* build(config)
162
-
163
- Object.assign(sharedBundle, newBundle)
164
-
165
- // Clean old loaded bundle
166
- yield* SynchronizedRef.update(loadRef, () => null)
167
-
168
- // publish event after the built
169
- if (sharedBundle.events) {
170
- yield* PubSub.publish(sharedBundle.events, v)
171
- }
172
- }),
173
- Effect.catchAll(err =>
174
- Effect.gen(function*() {
175
- yield* Effect.logError(
176
- "Error while updating bundle",
177
- err,
178
- )
179
- if (sharedBundle.events) {
180
- yield* PubSub.publish(sharedBundle.events, {
181
- _tag: "BuildError",
182
- error: String(err),
183
- })
184
- }
185
- })
186
- ),
187
- )
188
- ),
189
- ),
190
- )
191
-
192
- return sharedBundle
193
- }),
194
- )
195
- }
196
-
197
124
  /**
198
125
  * Finds common path prefix across provided paths.
199
126
  */
@@ -201,17 +201,33 @@ export function layerRoutes(
201
201
  return Layer.effectDiscard(
202
202
  Effect.gen(function*() {
203
203
  const bunServer = yield* BunHttpServer
204
- const routes: BunRoute.BunRoutes = {}
205
- for (const [path, handler] of RouteHttp.walkHandles(tree)) {
204
+ const runtime = yield* Effect.runtime<BunHttpServer>()
205
+ const toWebHandler = RouteHttp.toWebHandlerRuntime(runtime)
206
+
207
+ const bunRoutes: BunRoute.BunRoutes = {}
208
+ const pathGroups = new Map<string, RouteMount.MountedRoute[]>()
209
+
210
+ for (const route of RouteTree.walk(tree)) {
211
+ const bunDescriptors = BunRoute.descriptors(route)
212
+ if (bunDescriptors) {
213
+ const htmlBundle = yield* Effect.promise(bunDescriptors.bunLoad)
214
+ bunRoutes[`${bunDescriptors.bunPrefix}/*`] = htmlBundle
215
+ }
216
+
217
+ const path = Route.descriptor(route).path
218
+ const group = pathGroups.get(path) ?? []
219
+ group.push(route)
220
+ pathGroups.set(path, group)
221
+ }
222
+
223
+ for (const [path, routes] of pathGroups) {
224
+ const handler = toWebHandler(routes)
206
225
  for (const bunPath of PathPattern.toBun(path)) {
207
- routes[bunPath] = handler
226
+ bunRoutes[bunPath] = handler
208
227
  }
209
228
  }
210
229
 
211
- // TODO: think how can we define routes upfront rather
212
- // than add them after startup?
213
- // now that we have Rooutes.Route thats should be possible
214
- bunServer.addRoutes(routes)
230
+ bunServer.addRoutes(bunRoutes)
215
231
  }),
216
232
  )
217
233
  }