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.
Files changed (54) hide show
  1. package/package.json +15 -14
  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 +85 -16
  7. package/src/FileHttpRouter.ts +119 -32
  8. package/src/FileRouter.ts +62 -166
  9. package/src/FileRouterCodegen.test.ts +252 -66
  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/HttpAppExtra.test.ts +84 -0
  17. package/src/HttpAppExtra.ts +399 -47
  18. package/src/HttpUtils.test.ts +68 -0
  19. package/src/HttpUtils.ts +15 -0
  20. package/src/HyperHtml.ts +24 -5
  21. package/src/JsModule.test.ts +1 -1
  22. package/src/NodeFileSystem.ts +764 -0
  23. package/src/Random.ts +59 -0
  24. package/src/Route.test.ts +515 -18
  25. package/src/Route.ts +321 -166
  26. package/src/RouteRender.ts +40 -0
  27. package/src/Router.test.ts +416 -0
  28. package/src/Router.ts +288 -31
  29. package/src/RouterPattern.test.ts +655 -0
  30. package/src/RouterPattern.ts +416 -0
  31. package/src/Start.ts +14 -52
  32. package/src/TestHttpClient.test.ts +29 -0
  33. package/src/TestHttpClient.ts +122 -73
  34. package/src/assets.d.ts +39 -0
  35. package/src/bun/BunBundle.test.ts +0 -3
  36. package/src/bun/BunHttpServer.test.ts +74 -0
  37. package/src/bun/BunHttpServer.ts +259 -0
  38. package/src/bun/BunHttpServer_web.ts +384 -0
  39. package/src/bun/BunRoute.test.ts +514 -0
  40. package/src/bun/BunRoute.ts +427 -0
  41. package/src/bun/BunRoute_bundles.test.ts +218 -0
  42. package/src/bun/BunRuntime.ts +33 -0
  43. package/src/bun/BunTailwindPlugin.test.ts +1 -1
  44. package/src/bun/_empty.html +1 -0
  45. package/src/bun/index.ts +2 -1
  46. package/src/index.ts +14 -14
  47. package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
  48. package/src/middlewares/BasicAuthMiddleware.ts +36 -0
  49. package/src/testing.ts +12 -3
  50. package/src/Datastar.test.ts +0 -267
  51. package/src/Datastar.ts +0 -68
  52. package/src/bun/BunFullstackServer.ts +0 -45
  53. package/src/bun/BunFullstackServer_httpServer.ts +0 -541
  54. package/src/jsx-datastar.d.ts +0 -63
@@ -0,0 +1,427 @@
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 Array from "effect/Array"
6
+ import * as Effect from "effect/Effect"
7
+ import * as Function from "effect/Function"
8
+ import * as Option from "effect/Option"
9
+ import * as Predicate from "effect/Predicate"
10
+ import type * as Runtime from "effect/Runtime"
11
+ import * as HttpAppExtra from "../HttpAppExtra.ts"
12
+ import * as HttpUtils from "../HttpUtils.ts"
13
+ import * as HyperHtml from "../HyperHtml.ts"
14
+ import * as Random from "../Random.ts"
15
+ import * as Route from "../Route.ts"
16
+ import * as Router from "../Router.ts"
17
+ import * as RouteRender from "../RouteRender.ts"
18
+ import * as RouterPattern from "../RouterPattern.ts"
19
+ import * as BunHttpServer from "./BunHttpServer.ts"
20
+
21
+ const TypeId: unique symbol = Symbol.for("effect-start/BunRoute")
22
+
23
+ const INTERNAL_FETCH_HEADER = "x-effect-start-internal-fetch"
24
+
25
+ export type BunRoute =
26
+ & Route.Route
27
+ & {
28
+ [TypeId]: typeof TypeId
29
+ // Prefix because Bun.serve routes ignore everything after `*` in wildcard patterns.
30
+ // A suffix like `/*~internal` would match the same as `/*`, shadowing the internal route.
31
+ internalPathPrefix: string
32
+ load: () => Promise<Bun.HTMLBundle>
33
+ }
34
+
35
+ export function html(
36
+ load: () => Promise<Bun.HTMLBundle | { default: Bun.HTMLBundle }>,
37
+ ): BunRoute {
38
+ const internalPathPrefix = `/.BunRoute-${Random.token(6)}`
39
+
40
+ const handler: Route.RouteHandler<
41
+ HttpServerResponse.HttpServerResponse,
42
+ Router.RouterError,
43
+ BunHttpServer.BunServer
44
+ > = (context) =>
45
+ Effect.gen(function*() {
46
+ const originalRequest = context.request.source as Request
47
+
48
+ if (
49
+ originalRequest.headers.get(INTERNAL_FETCH_HEADER) === "true"
50
+ ) {
51
+ return yield* Effect.fail(
52
+ new Router.RouterError({
53
+ reason: "ProxyError",
54
+ pattern: context.url.pathname,
55
+ message:
56
+ "Request to internal Bun server was caught by BunRoute handler. This should not happen. Please report a bug.",
57
+ }),
58
+ )
59
+ }
60
+
61
+ const bunServer = yield* BunHttpServer.BunServer
62
+ const internalPath = `${internalPathPrefix}${context.url.pathname}`
63
+ const internalUrl = new URL(internalPath, bunServer.server.url)
64
+
65
+ const headers = new Headers(originalRequest.headers)
66
+ headers.set(INTERNAL_FETCH_HEADER, "true")
67
+
68
+ const proxyRequest = new Request(internalUrl, {
69
+ method: originalRequest.method,
70
+ headers,
71
+ })
72
+
73
+ const response = yield* Effect.tryPromise({
74
+ try: () => fetch(proxyRequest),
75
+ catch: (error) =>
76
+ new Router.RouterError({
77
+ reason: "ProxyError",
78
+ pattern: internalPath,
79
+ message: `Failed to fetch internal HTML bundle: ${String(error)}`,
80
+ }),
81
+ })
82
+
83
+ let html = yield* Effect.tryPromise({
84
+ try: () => response.text(),
85
+ catch: (error) =>
86
+ new Router.RouterError({
87
+ reason: "ProxyError",
88
+ pattern: internalPath,
89
+ message: String(error),
90
+ }),
91
+ })
92
+
93
+ const children = yield* context.next<Router.RouterError, never>()
94
+ let childrenHtml = ""
95
+ if (children != null) {
96
+ if (HttpServerResponse.isServerResponse(children)) {
97
+ const webResponse = HttpServerResponse.toWeb(children)
98
+ childrenHtml = yield* Effect.promise(() => webResponse.text())
99
+ } else if (Route.isGenericJsxObject(children)) {
100
+ childrenHtml = HyperHtml.renderToString(children)
101
+ } else {
102
+ childrenHtml = String(children)
103
+ }
104
+ }
105
+
106
+ html = html.replace(/%yield%/g, childrenHtml)
107
+ html = html.replace(/%slots\.(\w+)%/g, (_, name) =>
108
+ context.slots[name] ?? "")
109
+
110
+ return HttpServerResponse
111
+ .html(html)
112
+ })
113
+
114
+ const route = Route.make({
115
+ method: "*",
116
+ media: "text/html",
117
+ handler,
118
+ schemas: {},
119
+ })
120
+
121
+ const bunRoute: BunRoute = Object.assign(
122
+ Object.create(route),
123
+ {
124
+ [TypeId]: TypeId,
125
+ internalPathPrefix,
126
+ load: () => load().then(mod => "default" in mod ? mod.default : mod),
127
+ set: [],
128
+ },
129
+ )
130
+
131
+ bunRoute.set.push(bunRoute)
132
+
133
+ return bunRoute
134
+ }
135
+
136
+ export function isBunRoute(input: unknown): input is BunRoute {
137
+ return Predicate.hasProperty(input, TypeId)
138
+ }
139
+
140
+ function makeHandler(routes: Route.Route.Default[]) {
141
+ return Effect.gen(function*() {
142
+ const request = yield* HttpServerRequest.HttpServerRequest
143
+ const accept = request.headers.accept ?? ""
144
+
145
+ let selectedRoute: Route.Route.Default | undefined
146
+
147
+ if (accept.includes("application/json")) {
148
+ selectedRoute = routes.find((r) => r.media === "application/json")
149
+ }
150
+ if (!selectedRoute && accept.includes("text/plain")) {
151
+ selectedRoute = routes.find((r) => r.media === "text/plain")
152
+ }
153
+ if (
154
+ !selectedRoute
155
+ && (accept.includes("text/html")
156
+ || accept.includes("*/*")
157
+ || !accept)
158
+ ) {
159
+ selectedRoute = routes.find((r) => r.media === "text/html")
160
+ }
161
+ if (!selectedRoute) {
162
+ selectedRoute = routes[0]
163
+ }
164
+
165
+ if (!selectedRoute) {
166
+ return HttpServerResponse.empty({ status: 406 })
167
+ }
168
+
169
+ const context: Route.RouteContext = {
170
+ request,
171
+ get url() {
172
+ return HttpUtils.makeUrlFromRequest(request)
173
+ },
174
+ slots: {},
175
+ next: () => Effect.void,
176
+ }
177
+
178
+ return yield* RouteRender.render(selectedRoute, context).pipe(
179
+ Effect.catchAllCause((cause) => HttpAppExtra.renderError(cause, accept)),
180
+ )
181
+ })
182
+ }
183
+
184
+ /**
185
+ * Finds BunRoutes in the Router and returns
186
+ * a mapping of paths to their bundles that can be passed
187
+ * to Bun's `serve` function.
188
+ */
189
+ export function bundlesFromRouter(
190
+ router: Router.RouterContext,
191
+ ): Effect.Effect<Record<string, Bun.HTMLBundle>> {
192
+ return Function.pipe(
193
+ Effect.forEach(
194
+ router.routes,
195
+ (mod) =>
196
+ Effect.promise(() =>
197
+ mod.load().then((m) => ({ path: mod.path, exported: m.default }))
198
+ ),
199
+ ),
200
+ Effect.map((modules) =>
201
+ modules.flatMap(({ path, exported }) => {
202
+ if (Route.isRouteSet(exported)) {
203
+ return [...exported.set]
204
+ .filter(isBunRoute)
205
+ .map((route) =>
206
+ [
207
+ path,
208
+ route,
209
+ ] as const
210
+ )
211
+ }
212
+
213
+ return []
214
+ })
215
+ ),
216
+ Effect.flatMap((bunRoutes) =>
217
+ Effect.forEach(
218
+ bunRoutes,
219
+ ([path, route]) =>
220
+ Effect.promise(() =>
221
+ route.load().then((bundle) => {
222
+ const httpPath = RouterPattern.toBun(path)
223
+
224
+ return [
225
+ httpPath,
226
+ bundle,
227
+ ] as const
228
+ })
229
+ ),
230
+ { concurrency: "unbounded" },
231
+ )
232
+ ),
233
+ Effect.map((entries) =>
234
+ Object.fromEntries(entries) as Record<string, Bun.HTMLBundle>
235
+ ),
236
+ )
237
+ }
238
+
239
+ type BunServerFetchHandler = (
240
+ request: Request,
241
+ server: Bun.Server<unknown>,
242
+ ) => Response | Promise<Response>
243
+
244
+ type BunServerRouteHandler =
245
+ | Bun.HTMLBundle
246
+ | BunServerFetchHandler
247
+ | Partial<Record<Bun.Serve.HTTPMethod, BunServerFetchHandler>>
248
+
249
+ export type BunRoutes = Record<string, BunServerRouteHandler>
250
+
251
+ type MethodHandlers = Partial<
252
+ Record<Bun.Serve.HTTPMethod, BunServerFetchHandler>
253
+ >
254
+
255
+ function isMethodHandlers(value: unknown): value is MethodHandlers {
256
+ return typeof value === "object" && value !== null && !("index" in value)
257
+ }
258
+
259
+ /**
260
+ * Validates that a route pattern can be implemented with Bun.serve routes.
261
+ *
262
+ * Supported patterns (native or via multiple routes):
263
+ * - /exact - Exact match
264
+ * - /users/:id - Full-segment named param
265
+ * - /path/* - Directory wildcard
266
+ * - /* - Catch-all
267
+ * - /[[id]] - Optional param (implemented via `/` and `/:id`)
268
+ * - /[[...rest]] - Optional rest param (implemented via `/` and `/*`)
269
+ *
270
+ * Unsupported patterns (cannot be implemented in Bun):
271
+ * - /pk_[id] - Prefix before param
272
+ * - /[id]_sfx - Suffix after param
273
+ * - /[id].json - Suffix with dot
274
+ * - /[id]~test - Suffix with tilde
275
+ * - /hello-* - Inline prefix wildcard
276
+ */
277
+
278
+ export function validateBunPattern(
279
+ pattern: string,
280
+ ): Option.Option<Router.RouterError> {
281
+ const segments = RouterPattern.parse(pattern)
282
+
283
+ const unsupported = Array.findFirst(segments, (seg) => {
284
+ if (seg._tag === "ParamSegment") {
285
+ return seg.prefix !== undefined || seg.suffix !== undefined
286
+ }
287
+
288
+ return false
289
+ })
290
+
291
+ if (Option.isSome(unsupported)) {
292
+ return Option.some(
293
+ new Router.RouterError({
294
+ reason: "UnsupportedPattern",
295
+ pattern,
296
+ message:
297
+ `Pattern "${pattern}" uses prefixed/suffixed params (prefix_[param] or [param]_suffix) `
298
+ + `which cannot be implemented in Bun.serve.`,
299
+ }),
300
+ )
301
+ }
302
+
303
+ return Option.none()
304
+ }
305
+
306
+ /**
307
+ * Converts a RouterBuilder into Bun-compatible routes passed to {@link Bun.serve}.
308
+ *
309
+ * For BunRoutes (HtmlBundle), creates two routes:
310
+ * - An internal route at `${path}~BunRoute-${nonce}:${path}` holding the actual HtmlBundle
311
+ * - A proxy route at the original path that forwards requests to the internal route
312
+ *
313
+ * This allows middleware to be attached to the proxy route while Bun handles
314
+ * the HtmlBundle natively on the internal route.
315
+ */
316
+ export function routesFromRouter(
317
+ router: Router.RouterBuilder.Any,
318
+ runtime?: Runtime.Runtime<BunHttpServer.BunServer>,
319
+ ): Effect.Effect<BunRoutes, Router.RouterError, BunHttpServer.BunServer> {
320
+ return Effect.gen(function*() {
321
+ const rt = runtime ?? (yield* Effect.runtime<BunHttpServer.BunServer>())
322
+ const result: BunRoutes = {}
323
+
324
+ for (const entry of router.entries) {
325
+ const { path, route: routeSet, layers } = entry
326
+
327
+ const validationError = validateBunPattern(path)
328
+ if (Option.isSome(validationError)) {
329
+ return yield* Effect.fail(validationError.value)
330
+ }
331
+
332
+ for (const route of routeSet.set) {
333
+ if (isBunRoute(route)) {
334
+ const bundle = yield* Effect.promise(() => route.load())
335
+ const bunPaths = RouterPattern.toBun(path)
336
+ for (const bunPath of bunPaths) {
337
+ const internalPath = `${route.internalPathPrefix}${bunPath}`
338
+ result[internalPath] = bundle
339
+ }
340
+ }
341
+ }
342
+
343
+ for (const layer of layers) {
344
+ for (const route of layer.set) {
345
+ if (isBunRoute(route)) {
346
+ const bundle = yield* Effect.promise(() => route.load())
347
+ const bunPaths = RouterPattern.toBun(path)
348
+ for (const bunPath of bunPaths) {
349
+ const internalPath = `${route.internalPathPrefix}${bunPath}`
350
+ result[internalPath] = bundle
351
+ }
352
+ }
353
+ }
354
+ }
355
+ }
356
+
357
+ for (const path of Object.keys(router.mounts)) {
358
+ const routeSet = router.mounts[path]
359
+
360
+ const validationError = validateBunPattern(path)
361
+ if (Option.isSome(validationError)) {
362
+ continue
363
+ }
364
+
365
+ const httpPaths = RouterPattern.toBun(path as Route.RoutePattern)
366
+
367
+ const byMethod = new Map<Route.RouteMethod, Route.Route.Default[]>()
368
+ for (const route of routeSet.set) {
369
+ const existing = byMethod.get(route.method) ?? []
370
+ existing.push(route)
371
+ byMethod.set(route.method, existing)
372
+ }
373
+
374
+ const entry = router.entries.find((e) => e.path === path)
375
+ const allMiddleware = (entry?.layers ?? [])
376
+ .map((layer) => layer.httpMiddleware)
377
+ .filter((m): m is Route.HttpMiddlewareFunction => m !== undefined)
378
+
379
+ for (const [method, routes] of byMethod) {
380
+ let httpApp: HttpApp.Default<any, any> = makeHandler(routes)
381
+
382
+ for (const middleware of allMiddleware) {
383
+ httpApp = middleware(httpApp)
384
+ }
385
+
386
+ const webHandler = HttpApp.toWebHandlerRuntime(rt)(httpApp)
387
+ const handler: BunServerFetchHandler = (request) => {
388
+ const url = new URL(request.url)
389
+ if (url.pathname.startsWith("/.BunRoute-")) {
390
+ return new Response(
391
+ "Internal routing error: BunRoute internal path was not matched. "
392
+ + "This indicates the HTMLBundle route was not registered. Please report a bug.",
393
+ { status: 500 },
394
+ )
395
+ }
396
+ return webHandler(request)
397
+ }
398
+
399
+ for (const httpPath of httpPaths) {
400
+ if (method === "*") {
401
+ if (!(httpPath in result)) {
402
+ result[httpPath] = handler
403
+ }
404
+ } else {
405
+ const existing = result[httpPath]
406
+ if (isMethodHandlers(existing)) {
407
+ existing[method] = handler
408
+ } else if (!(httpPath in result)) {
409
+ result[httpPath] = { [method]: handler }
410
+ }
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ return result
417
+ })
418
+ }
419
+
420
+ export const isHTMLBundle = (handle: any) => {
421
+ return (
422
+ typeof handle === "object"
423
+ && handle !== null
424
+ && (handle.toString() === "[object HTMLBundle]"
425
+ || typeof handle.index === "string")
426
+ )
427
+ }
@@ -0,0 +1,218 @@
1
+ import * as t from "bun:test"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Route from "../Route.ts"
4
+ import * as Router from "../Router.ts"
5
+ import * as TestHttpClient from "../TestHttpClient.ts"
6
+ import * as BunHttpServer from "./BunHttpServer.ts"
7
+ import * as BunRoute from "./BunRoute.ts"
8
+
9
+ t.describe("BunRoute proxy with Bun.serve", () => {
10
+ t.test("BunRoute proxy returns same content as direct bundle access", async () => {
11
+ const bunRoute = BunRoute.html(() => import("../../static/TestPage.html"))
12
+
13
+ const router = Router.mount("/test", bunRoute)
14
+
15
+ await Effect.runPromise(
16
+ Effect
17
+ .gen(function*() {
18
+ const bunServer = yield* BunHttpServer.BunServer
19
+ const routes = yield* BunRoute.routesFromRouter(router)
20
+ bunServer.addRoutes(routes)
21
+
22
+ const internalPath = Object.keys(routes).find((k) =>
23
+ k.includes(".BunRoute-")
24
+
25
+ )
26
+ t.expect(internalPath).toBeDefined()
27
+
28
+ const proxyHandler = routes["/test"]
29
+ t.expect(typeof proxyHandler).toBe("function")
30
+
31
+ const internalBundle = routes[internalPath!]
32
+ t.expect(internalBundle).toHaveProperty("index")
33
+
34
+ const baseUrl =
35
+ `http://${bunServer.server.hostname}:${bunServer.server.port}`
36
+ const client = TestHttpClient.make<never, never>(
37
+ (req) => fetch(req),
38
+ {
39
+ baseUrl,
40
+ },
41
+ )
42
+
43
+ const directResponse = yield* client.get(internalPath!)
44
+ const proxyResponse = yield* client.get("/test")
45
+
46
+ t.expect(proxyResponse.status).toBe(directResponse.status)
47
+
48
+ const directText = yield* directResponse.text
49
+ const proxyText = yield* proxyResponse.text
50
+
51
+ t.expect(proxyText).toBe(directText)
52
+ t.expect(proxyText).toContain("Test Page Content")
53
+ })
54
+ .pipe(
55
+ Effect.scoped,
56
+ Effect.provide(BunHttpServer.layer({ port: 0 })),
57
+ ),
58
+ )
59
+ })
60
+
61
+ t.test("multiple BunRoutes each get unique internal paths", async () => {
62
+ const bunRoute1 = BunRoute.html(() => import("../../static/TestPage.html"))
63
+ const bunRoute2 = BunRoute.html(() =>
64
+ import("../../static/AnotherPage.html")
65
+ )
66
+
67
+ const router = Router
68
+ .mount("/page1", bunRoute1)
69
+ .mount("/page2", bunRoute2)
70
+
71
+ await Effect.runPromise(
72
+ Effect
73
+ .gen(function*() {
74
+ const bunServer = yield* BunHttpServer.BunServer
75
+ const routes = yield* BunRoute.routesFromRouter(router)
76
+ bunServer.addRoutes(routes)
77
+
78
+ const internalPaths = Object.keys(routes).filter((k) =>
79
+ k.includes(".BunRoute-")
80
+ )
81
+ t.expect(internalPaths).toHaveLength(2)
82
+
83
+ const nonces = internalPaths.map((p) => {
84
+ const match = p.match(/\.BunRoute-([a-z0-9]+)/)
85
+ return match?.[1]
86
+ })
87
+ t.expect(nonces[0]).not.toBe(nonces[1])
88
+
89
+ const baseUrl =
90
+ `http://${bunServer.server.hostname}:${bunServer.server.port}`
91
+ const client = TestHttpClient.make<never, never>(
92
+ (req) => fetch(req),
93
+ {
94
+ baseUrl,
95
+ },
96
+ )
97
+
98
+ const response1 = yield* client.get("/page1")
99
+ const response2 = yield* client.get("/page2")
100
+
101
+ const text1 = yield* response1.text
102
+ const text2 = yield* response2.text
103
+
104
+ t.expect(text1).toContain("Test Page Content")
105
+ t.expect(text2).toContain("Another Page Content")
106
+ })
107
+ .pipe(
108
+ Effect.scoped,
109
+ Effect.provide(BunHttpServer.layer({ port: 0 })),
110
+ ),
111
+ )
112
+ })
113
+
114
+ t.test("proxy preserves request headers", async () => {
115
+ const bunRoute = BunRoute.html(() => import("../../static/TestPage.html"))
116
+
117
+ const router = Router.mount("/headers-test", bunRoute)
118
+
119
+ await Effect.runPromise(
120
+ Effect
121
+ .gen(function*() {
122
+ const bunServer = yield* BunHttpServer.BunServer
123
+ const routes = yield* BunRoute.routesFromRouter(router)
124
+ bunServer.addRoutes(routes)
125
+
126
+ const baseUrl =
127
+ `http://${bunServer.server.hostname}:${bunServer.server.port}`
128
+ const client = TestHttpClient.make<never, never>(
129
+ (req) => fetch(req),
130
+ {
131
+ baseUrl,
132
+ },
133
+ )
134
+
135
+ const response = yield* client.get("/headers-test", {
136
+ headers: {
137
+ "Accept": "text/html",
138
+ "X-Custom-Header": "test-value",
139
+ },
140
+ })
141
+
142
+ t.expect(response.status).toBe(200)
143
+ const text = yield* response.text
144
+ t.expect(text).toContain("Test Page Content")
145
+ })
146
+ .pipe(
147
+ Effect.scoped,
148
+ Effect.provide(BunHttpServer.layer({ port: 0 })),
149
+ ),
150
+ )
151
+ })
152
+
153
+ t.test("mixed BunRoute and regular routes work together", async () => {
154
+ const bunRoute = BunRoute.html(() => import("../../static/TestPage.html"))
155
+
156
+ const router = Router
157
+ .mount("/html", bunRoute)
158
+ .mount("/api", Route.text("Hello from text route"))
159
+
160
+ await Effect.runPromise(
161
+ Effect
162
+ .gen(function*() {
163
+ const bunServer = yield* BunHttpServer.BunServer
164
+ const routes = yield* BunRoute.routesFromRouter(router)
165
+ bunServer.addRoutes(routes)
166
+
167
+ const baseUrl =
168
+ `http://${bunServer.server.hostname}:${bunServer.server.port}`
169
+ const client = TestHttpClient.make<never, never>(
170
+ (req) => fetch(req),
171
+ {
172
+ baseUrl,
173
+ },
174
+ )
175
+
176
+ const htmlResponse = yield* client.get("/html")
177
+ const apiResponse = yield* client.get("/api")
178
+
179
+ const htmlText = yield* htmlResponse.text
180
+ const apiText = yield* apiResponse.text
181
+
182
+ t.expect(htmlText).toContain("Test Page Content")
183
+ t.expect(apiText).toBe("Hello from text route")
184
+ })
185
+ .pipe(
186
+ Effect.scoped,
187
+ Effect.provide(BunHttpServer.layer({ port: 0 })),
188
+ ),
189
+ )
190
+ })
191
+
192
+ t.test("nonce is different across separate BunRoute instances", async () => {
193
+ const bunRoute1 = BunRoute.html(() => import("../../static/TestPage.html"))
194
+ const bunRoute2 = BunRoute.html(() => import("../../static/TestPage.html"))
195
+
196
+ const router = Router
197
+ .mount("/test1", bunRoute1)
198
+ .mount("/test2", bunRoute2)
199
+
200
+ await Effect.runPromise(
201
+ Effect
202
+ .gen(function*() {
203
+ const routes = yield* BunRoute.routesFromRouter(router)
204
+
205
+ const internalPaths = Object.keys(routes).filter((k) =>
206
+ k.includes(".BunRoute-")
207
+ )
208
+
209
+ t.expect(internalPaths).toHaveLength(2)
210
+ t.expect(internalPaths[0]).not.toBe(internalPaths[1])
211
+ })
212
+ .pipe(
213
+ Effect.scoped,
214
+ Effect.provide(BunHttpServer.layer({ port: 0 })),
215
+ ),
216
+ )
217
+ })
218
+ })
@@ -0,0 +1,33 @@
1
+ import { makeRunMain } from "@effect/platform/Runtime"
2
+ import { constVoid } from "effect/Function"
3
+
4
+ export const runMain = makeRunMain(({
5
+ fiber,
6
+ teardown,
7
+ }) => {
8
+ const keepAlive = setInterval(constVoid, 2 ** 31 - 1)
9
+ let receivedSignal = false
10
+
11
+ fiber.addObserver((exit) => {
12
+ if (!receivedSignal) {
13
+ process.removeListener("SIGINT", onSigint)
14
+ process.removeListener("SIGTERM", onSigint)
15
+ }
16
+ clearInterval(keepAlive)
17
+ teardown(exit, (code) => {
18
+ if (receivedSignal || code !== 0) {
19
+ process.exit(code)
20
+ }
21
+ })
22
+ })
23
+
24
+ function onSigint() {
25
+ receivedSignal = true
26
+ process.removeListener("SIGINT", onSigint)
27
+ process.removeListener("SIGTERM", onSigint)
28
+ fiber.unsafeInterruptAsFork(fiber.id())
29
+ }
30
+
31
+ process.on("SIGINT", onSigint)
32
+ process.on("SIGTERM", onSigint)
33
+ })
@@ -17,7 +17,7 @@ function extractClassNamesBroad(source: string): Set<string> {
17
17
  )
18
18
  }
19
19
 
20
- t.describe("extractClassNames", () => {
20
+ t.describe(`${extractClassNames.name}`, () => {
21
21
  t.test("Basic HTML class attributes", () => {
22
22
  const source = `<div class="bg-red-500 text-white">Hello</div>`
23
23
  const result = extractClassNames(source)
@@ -0,0 +1 @@
1
+
package/src/bun/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * as BunBundle from "./BunBundle.ts"
2
- export * as BunFullstackServer from "./BunFullstackServer.ts"
2
+ export * as BunHttpServer from "./BunHttpServer.ts"
3
3
  export * as BunImportTrackerPlugin from "./BunImportTrackerPlugin.ts"
4
+ export * as BunRoute from "./BunRoute.ts"
4
5
  export * as BunTailwindPlugin from "./BunTailwindPlugin.ts"