effect-start 0.15.0 → 0.16.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 +1 -1
- package/src/ContentNegotiation.test.ts +103 -0
- package/src/ContentNegotiation.ts +10 -3
- package/src/Entity.test.ts +592 -0
- package/src/Entity.ts +362 -0
- package/src/Http.test.ts +315 -20
- package/src/Http.ts +153 -11
- package/src/PathPattern.ts +3 -1
- package/src/Route.ts +22 -10
- package/src/RouteBody.test.ts +81 -29
- package/src/RouteBody.ts +122 -35
- package/src/RouteHook.ts +15 -14
- package/src/RouteHttp.test.ts +2546 -83
- package/src/RouteHttp.ts +321 -113
- package/src/RouteHttpTracer.ts +92 -0
- package/src/RouteMount.test.ts +23 -10
- package/src/RouteMount.ts +161 -4
- package/src/RouteSchema.test.ts +346 -0
- package/src/RouteSchema.ts +386 -7
- package/src/RouteTree.test.ts +233 -85
- package/src/RouteTree.ts +98 -44
- package/src/StreamExtra.ts +21 -1
- package/src/Values.test.ts +263 -0
- package/src/Values.ts +60 -0
- package/src/bun/BunHttpServer.ts +23 -7
- package/src/bun/BunRoute.test.ts +162 -0
- package/src/bun/BunRoute.ts +146 -105
- package/src/index.ts +1 -0
- package/src/testing/TestLogger.test.ts +0 -3
- package/src/testing/TestLogger.ts +15 -9
package/src/bun/BunRoute.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
3
|
-
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
4
1
|
import type * as Bun from "bun"
|
|
5
2
|
import * as Array from "effect/Array"
|
|
3
|
+
import * as Data from "effect/Data"
|
|
6
4
|
import * as Effect from "effect/Effect"
|
|
7
5
|
import * as Option from "effect/Option"
|
|
8
|
-
import * as
|
|
6
|
+
import * as Entity from "../Entity.ts"
|
|
9
7
|
import * as Hyper from "../hyper/Hyper.ts"
|
|
10
8
|
import * as HyperHtml from "../hyper/HyperHtml.ts"
|
|
11
9
|
import * as Random from "../Random.ts"
|
|
@@ -13,108 +11,159 @@ import * as Route from "../Route.ts"
|
|
|
13
11
|
import * as RouterPattern from "../RouterPattern.ts"
|
|
14
12
|
import * as BunHttpServer from "./BunHttpServer.ts"
|
|
15
13
|
|
|
16
|
-
const BunHandlerTypeId: unique symbol = Symbol.for("effect-start/BunHandler")
|
|
17
|
-
|
|
18
14
|
const INTERNAL_FETCH_HEADER = "x-effect-start-internal-fetch"
|
|
19
15
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
load: () => Promise<Bun.HTMLBundle>
|
|
26
|
-
}
|
|
16
|
+
export class BunRouteError extends Data.TaggedError("BunRouteError")<{
|
|
17
|
+
reason: "ProxyError" | "UnsupportedPattern"
|
|
18
|
+
pattern: string
|
|
19
|
+
message: string
|
|
20
|
+
}> {}
|
|
27
21
|
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
export type BunDescriptors = {
|
|
23
|
+
bunPrefix: string
|
|
24
|
+
bunLoad: () => Promise<Bun.HTMLBundle>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function descriptors(
|
|
28
|
+
route: Route.Route.Route,
|
|
29
|
+
): BunDescriptors | undefined {
|
|
30
|
+
const descriptor = Route.descriptor(route) as Partial<BunDescriptors>
|
|
31
|
+
if (
|
|
32
|
+
typeof descriptor.bunPrefix === "string"
|
|
33
|
+
&& typeof descriptor.bunLoad === "function"
|
|
34
|
+
) {
|
|
35
|
+
return descriptor as BunDescriptors
|
|
36
|
+
}
|
|
37
|
+
return undefined
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
export function
|
|
40
|
+
export function htmlBundle(
|
|
34
41
|
load: () => Promise<Bun.HTMLBundle | { default: Bun.HTMLBundle }>,
|
|
35
|
-
)
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
42
|
+
) {
|
|
43
|
+
const bunPrefix = `/.BunRoute-${Random.token(6)}`
|
|
44
|
+
const bunLoad = () => load().then(mod => "default" in mod ? mod.default : mod)
|
|
45
|
+
const descriptors = { bunPrefix, bunLoad, format: "html" as const }
|
|
46
|
+
|
|
47
|
+
return function<
|
|
48
|
+
D extends Route.RouteDescriptor.Any,
|
|
49
|
+
B extends {},
|
|
50
|
+
I extends Route.Route.Tuple,
|
|
51
|
+
>(
|
|
52
|
+
self: Route.RouteSet.RouteSet<D, B, I>,
|
|
53
|
+
): Route.RouteSet.RouteSet<
|
|
54
|
+
D,
|
|
55
|
+
B,
|
|
56
|
+
[
|
|
57
|
+
...I,
|
|
58
|
+
Route.Route.Route<
|
|
59
|
+
BunDescriptors & { format: "html" },
|
|
60
|
+
{ request: Request },
|
|
61
|
+
string,
|
|
62
|
+
BunRouteError,
|
|
63
|
+
BunHttpServer.BunHttpServer
|
|
64
|
+
>,
|
|
65
|
+
]
|
|
66
|
+
> {
|
|
67
|
+
const handler: Route.Route.Handler<
|
|
68
|
+
BunDescriptors & { format: "html" } & { request: Request },
|
|
69
|
+
string,
|
|
70
|
+
BunRouteError,
|
|
71
|
+
BunHttpServer.BunHttpServer
|
|
72
|
+
> = (context, next) =>
|
|
73
|
+
Effect.gen(function*() {
|
|
74
|
+
const originalRequest = context.request
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
originalRequest.headers.get(INTERNAL_FETCH_HEADER) === "true"
|
|
78
|
+
) {
|
|
79
|
+
const url = new URL(originalRequest.url)
|
|
80
|
+
return yield* Effect.fail(
|
|
81
|
+
new BunRouteError({
|
|
82
|
+
reason: "ProxyError",
|
|
83
|
+
pattern: url.pathname,
|
|
84
|
+
message:
|
|
85
|
+
"Request to internal Bun server was caught by BunRoute handler. This should not happen. Please report a bug.",
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
const bunServer = yield* BunHttpServer.BunHttpServer
|
|
91
|
+
const url = new URL(originalRequest.url)
|
|
92
|
+
|
|
93
|
+
const internalPath = `${bunPrefix}${url.pathname}`
|
|
94
|
+
const internalUrl = new URL(internalPath, bunServer.server.url)
|
|
95
|
+
|
|
96
|
+
const headers = new Headers(originalRequest.headers)
|
|
97
|
+
headers.set(INTERNAL_FETCH_HEADER, "true")
|
|
98
|
+
|
|
99
|
+
const proxyRequest = new Request(internalUrl, {
|
|
100
|
+
method: originalRequest.method,
|
|
101
|
+
headers,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const response = yield* Effect.tryPromise({
|
|
105
|
+
try: () => fetch(proxyRequest),
|
|
106
|
+
catch: (error) =>
|
|
107
|
+
new BunRouteError({
|
|
108
|
+
reason: "ProxyError",
|
|
109
|
+
pattern: internalPath,
|
|
110
|
+
message: `Failed to fetch internal HTML bundle: ${String(error)}`,
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
let html = yield* Effect.tryPromise({
|
|
115
|
+
try: () => response.text(),
|
|
116
|
+
catch: (error) =>
|
|
117
|
+
new BunRouteError({
|
|
118
|
+
reason: "ProxyError",
|
|
119
|
+
pattern: internalPath,
|
|
120
|
+
message: String(error),
|
|
121
|
+
}),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const childEntity = yield* Entity.resolve(next(context))
|
|
125
|
+
const children = childEntity?.body ?? childEntity
|
|
126
|
+
|
|
127
|
+
let childrenHtml = ""
|
|
128
|
+
if (children != null) {
|
|
129
|
+
if ((children as unknown) instanceof Response) {
|
|
130
|
+
childrenHtml = yield* Effect.promise(() =>
|
|
131
|
+
(children as unknown as Response).text()
|
|
132
|
+
)
|
|
133
|
+
} else if (Hyper.isGenericJsxObject(children)) {
|
|
134
|
+
childrenHtml = HyperHtml.renderToString(children)
|
|
135
|
+
} else {
|
|
136
|
+
childrenHtml = String(children)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
78
139
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
140
|
+
html = html.replace(/%children%/g, childrenHtml)
|
|
141
|
+
|
|
142
|
+
return Entity.make(html, {
|
|
143
|
+
status: response.status,
|
|
144
|
+
headers: {
|
|
145
|
+
"content-type": response.headers.get("content-type"),
|
|
146
|
+
},
|
|
147
|
+
})
|
|
87
148
|
})
|
|
88
149
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
html = html.replace(/%yield%/g, childrenHtml)
|
|
103
|
-
html = html.replace(
|
|
104
|
-
/%slots\.(\w+)%/g,
|
|
105
|
-
(_, name) => context.slots[name] ?? "",
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
return html
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
return Object.assign(handler, {
|
|
112
|
-
[BunHandlerTypeId]: BunHandlerTypeId,
|
|
113
|
-
internalPathPrefix,
|
|
114
|
-
load: () => load().then(mod => "default" in mod ? mod.default : mod),
|
|
115
|
-
}) as BunHandler
|
|
150
|
+
const route = Route.make<
|
|
151
|
+
BunDescriptors & { format: "html" },
|
|
152
|
+
{ request: Request },
|
|
153
|
+
string,
|
|
154
|
+
BunRouteError,
|
|
155
|
+
BunHttpServer.BunHttpServer
|
|
156
|
+
>(handler, descriptors)
|
|
157
|
+
|
|
158
|
+
return Route.set(
|
|
159
|
+
[...Route.items(self), route] as any,
|
|
160
|
+
Route.descriptor(self),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
116
163
|
}
|
|
117
164
|
|
|
165
|
+
|
|
166
|
+
|
|
118
167
|
type BunServerFetchHandler = (
|
|
119
168
|
request: Request,
|
|
120
169
|
server: Bun.Server<unknown>,
|
|
@@ -127,14 +176,6 @@ type BunServerRouteHandler =
|
|
|
127
176
|
|
|
128
177
|
export type BunRoutes = Record<string, BunServerRouteHandler>
|
|
129
178
|
|
|
130
|
-
type MethodHandlers = Partial<
|
|
131
|
-
Record<Bun.Serve.HTTPMethod, BunServerFetchHandler>
|
|
132
|
-
>
|
|
133
|
-
|
|
134
|
-
function isMethodHandlers(value: unknown): value is MethodHandlers {
|
|
135
|
-
return typeof value === "object" && value !== null && !("index" in value)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
179
|
/**
|
|
139
180
|
* Validates that a route pattern can be implemented with Bun.serve routes.
|
|
140
181
|
*
|
|
@@ -156,7 +197,7 @@ function isMethodHandlers(value: unknown): value is MethodHandlers {
|
|
|
156
197
|
|
|
157
198
|
export function validateBunPattern(
|
|
158
199
|
pattern: string,
|
|
159
|
-
): Option.Option<
|
|
200
|
+
): Option.Option<BunRouteError> {
|
|
160
201
|
const segments = RouterPattern.parse(pattern)
|
|
161
202
|
|
|
162
203
|
const unsupported = Array.findFirst(segments, (seg) => {
|
|
@@ -169,7 +210,7 @@ export function validateBunPattern(
|
|
|
169
210
|
|
|
170
211
|
if (Option.isSome(unsupported)) {
|
|
171
212
|
return Option.some(
|
|
172
|
-
new
|
|
213
|
+
new BunRouteError({
|
|
173
214
|
reason: "UnsupportedPattern",
|
|
174
215
|
pattern,
|
|
175
216
|
message:
|
|
@@ -182,7 +223,7 @@ export function validateBunPattern(
|
|
|
182
223
|
return Option.none()
|
|
183
224
|
}
|
|
184
225
|
|
|
185
|
-
export const
|
|
226
|
+
export const isHtmlBundle = (handle: any) => {
|
|
186
227
|
return (
|
|
187
228
|
typeof handle === "object"
|
|
188
229
|
&& handle !== null
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as test from "bun:test"
|
|
2
2
|
import * as Effect from "effect/Effect"
|
|
3
|
-
import * as Logger from "effect/Logger"
|
|
4
3
|
import * as Ref from "effect/Ref"
|
|
5
4
|
import * as TestLogger from "./TestLogger.ts"
|
|
6
5
|
|
|
@@ -9,12 +8,10 @@ test.it("TestLogger captures log messages", () =>
|
|
|
9
8
|
.gen(function*() {
|
|
10
9
|
const logger = yield* TestLogger.TestLogger
|
|
11
10
|
|
|
12
|
-
// Log some messages
|
|
13
11
|
yield* Effect.logError("This is an error")
|
|
14
12
|
yield* Effect.logWarning("This is a warning")
|
|
15
13
|
yield* Effect.logInfo("This is info")
|
|
16
14
|
|
|
17
|
-
// Read captured messages
|
|
18
15
|
const messages = yield* Ref.get(logger.messages)
|
|
19
16
|
|
|
20
17
|
test
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import * as Cause from "effect/Cause"
|
|
1
2
|
import * as Context from "effect/Context"
|
|
2
3
|
import * as Effect from "effect/Effect"
|
|
3
4
|
import * as FiberRef from "effect/FiberRef"
|
|
4
5
|
import * as HashSet from "effect/HashSet"
|
|
5
6
|
import * as Layer from "effect/Layer"
|
|
6
7
|
import * as Logger from "effect/Logger"
|
|
8
|
+
import * as MutableRef from "effect/MutableRef"
|
|
7
9
|
import * as Ref from "effect/Ref"
|
|
8
10
|
|
|
9
11
|
export type TestLoggerContext = {
|
|
@@ -20,13 +22,19 @@ export function layer(): Layer.Layer<TestLogger> {
|
|
|
20
22
|
TestLogger,
|
|
21
23
|
Effect.gen(function*() {
|
|
22
24
|
const messages = yield* Ref.make<Array<string>>([])
|
|
25
|
+
const mutableRef = (messages as any).ref as MutableRef.MutableRef<
|
|
26
|
+
Array<string>
|
|
27
|
+
>
|
|
23
28
|
|
|
24
|
-
const customLogger = Logger.make(({ message, logLevel }) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const customLogger = Logger.make(({ message, logLevel, cause }) => {
|
|
30
|
+
const causeStr = !Cause.isEmpty(cause)
|
|
31
|
+
? ` ${Cause.pretty(cause)}`
|
|
32
|
+
: ""
|
|
33
|
+
MutableRef.update(
|
|
34
|
+
mutableRef,
|
|
35
|
+
(
|
|
36
|
+
msgs,
|
|
37
|
+
) => [...msgs, `[${logLevel._tag}] ${String(message)}${causeStr}`],
|
|
30
38
|
)
|
|
31
39
|
})
|
|
32
40
|
|
|
@@ -39,9 +47,7 @@ export function layer(): Layer.Layer<TestLogger> {
|
|
|
39
47
|
),
|
|
40
48
|
)
|
|
41
49
|
|
|
42
|
-
return {
|
|
43
|
-
messages,
|
|
44
|
-
}
|
|
50
|
+
return { messages }
|
|
45
51
|
}),
|
|
46
52
|
)
|
|
47
53
|
}
|