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
|
@@ -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(
|
|
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
|
|
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"
|