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
package/src/HttpAppExtra.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HttpRouter,
|
|
3
|
+
HttpServerRequest,
|
|
3
4
|
HttpServerResponse,
|
|
4
5
|
} from "@effect/platform"
|
|
5
6
|
import * as HttpApp from "@effect/platform/HttpApp"
|
|
6
7
|
import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
|
|
8
|
+
|
|
7
9
|
import {
|
|
8
10
|
RequestError,
|
|
9
11
|
RouteNotFound,
|
|
@@ -27,10 +29,242 @@ type GraciousError =
|
|
|
27
29
|
| RequestError
|
|
28
30
|
| ParseResult.ParseError
|
|
29
31
|
|
|
32
|
+
type StackFrame = {
|
|
33
|
+
function: string
|
|
34
|
+
file: string
|
|
35
|
+
type: "application" | "framework" | "node_modules"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ERROR_PAGE_CSS = `
|
|
39
|
+
:root {
|
|
40
|
+
--error-red: #c00;
|
|
41
|
+
--error-red-dark: #a00;
|
|
42
|
+
--bg-error: #fee;
|
|
43
|
+
--bg-light: #f5f5f5;
|
|
44
|
+
--bg-white: #fff;
|
|
45
|
+
--border-color: #ddd;
|
|
46
|
+
--text-dark: #333;
|
|
47
|
+
--text-gray: #666;
|
|
48
|
+
--text-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
* { box-sizing: border-box; }
|
|
52
|
+
|
|
53
|
+
body {
|
|
54
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
55
|
+
margin: 0;
|
|
56
|
+
padding: 0;
|
|
57
|
+
color: var(--text-dark);
|
|
58
|
+
line-height: 1.6;
|
|
59
|
+
min-height: 100dvh;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.error-page { width: 100%; margin: 0; }
|
|
63
|
+
|
|
64
|
+
.error-header {
|
|
65
|
+
background: var(--error-red);
|
|
66
|
+
color: white;
|
|
67
|
+
padding: 2rem 2.5rem;
|
|
68
|
+
margin: 0;
|
|
69
|
+
font-family: var(--text-mono);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.error-header h1 {
|
|
73
|
+
margin: 0 0 0.5rem 0;
|
|
74
|
+
font-size: 2rem;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
font-family: var(--text-mono);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.error-message {
|
|
80
|
+
margin: 0;
|
|
81
|
+
font-size: 1.1rem;
|
|
82
|
+
opacity: 0.95;
|
|
83
|
+
font-family: var(--text-mono);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.error-content {
|
|
87
|
+
background: var(--bg-white);
|
|
88
|
+
padding: 2rem 2.5rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.stack-trace {
|
|
92
|
+
margin: 1.5rem 0;
|
|
93
|
+
border: 1px solid var(--border-color);
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.stack-trace-header {
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
padding: 0.75rem 1rem;
|
|
101
|
+
background: var(--bg-light);
|
|
102
|
+
border-bottom: 1px solid var(--border-color);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.stack-list {
|
|
106
|
+
list-style: none;
|
|
107
|
+
padding: 0;
|
|
108
|
+
margin: 0;
|
|
109
|
+
max-height: 400px;
|
|
110
|
+
overflow-y: auto;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.stack-list li {
|
|
114
|
+
padding: 0.5rem 1rem;
|
|
115
|
+
font-family: var(--text-mono);
|
|
116
|
+
font-size: 0.875rem;
|
|
117
|
+
border-bottom: 1px solid var(--border-color);
|
|
118
|
+
background: var(--bg-white);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.stack-list li:last-child { border-bottom: none; }
|
|
122
|
+
|
|
123
|
+
.stack-list li:hover { background: #fafafa; }
|
|
124
|
+
|
|
125
|
+
.stack-list code {
|
|
126
|
+
background: transparent;
|
|
127
|
+
padding: 0;
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
color: var(--error-red-dark);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.stack-list .path { color: var(--text-gray); margin-left: 0.5rem; }
|
|
133
|
+
|
|
134
|
+
.request-info {
|
|
135
|
+
margin: 1.5rem 0;
|
|
136
|
+
border: 1px solid var(--border-color);
|
|
137
|
+
border-radius: 4px;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.request-info-header {
|
|
142
|
+
font-weight: 700;
|
|
143
|
+
padding: 0.75rem 1rem;
|
|
144
|
+
background: var(--bg-light);
|
|
145
|
+
border-bottom: 1px solid var(--border-color);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.request-info-content {
|
|
149
|
+
padding: 1rem;
|
|
150
|
+
font-family: var(--text-mono);
|
|
151
|
+
font-size: 0.875rem;
|
|
152
|
+
white-space: pre-wrap;
|
|
153
|
+
word-break: break-all;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@media (max-width: 768px) {
|
|
157
|
+
.error-header, .error-content { padding: 1.5rem 1rem; }
|
|
158
|
+
.error-header h1 { font-size: 1.5rem; }
|
|
159
|
+
}
|
|
160
|
+
`
|
|
161
|
+
|
|
162
|
+
type ErrorHtmlData = {
|
|
163
|
+
status: number
|
|
164
|
+
tag: string
|
|
165
|
+
message?: string
|
|
166
|
+
details?: object
|
|
167
|
+
requestContext?: RequestContext
|
|
168
|
+
errorName?: string
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function errorHtml(data: ErrorHtmlData): HttpServerResponse.HttpServerResponse {
|
|
172
|
+
let detailsHtml = ""
|
|
173
|
+
|
|
174
|
+
if (data.details) {
|
|
175
|
+
const detailsObj = data.details as Record<string, unknown>
|
|
176
|
+
|
|
177
|
+
if ("stack" in detailsObj && Array.isArray(detailsObj.stack)) {
|
|
178
|
+
const stackFrames = detailsObj.stack as StackFrame[]
|
|
179
|
+
detailsHtml = renderStackTrace(stackFrames)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const requestHtml = data.requestContext
|
|
184
|
+
? renderRequestContext(data.requestContext)
|
|
185
|
+
: ""
|
|
186
|
+
|
|
187
|
+
const messageHtml = data.message
|
|
188
|
+
? `<p class="error-message">${escapeHtml(data.message)}</p>`
|
|
189
|
+
: ""
|
|
190
|
+
|
|
191
|
+
const headerTitle = data.errorName ?? "UnexpectedError"
|
|
192
|
+
|
|
193
|
+
const html = `<!DOCTYPE html>
|
|
194
|
+
<html lang="en">
|
|
195
|
+
<head>
|
|
196
|
+
<meta charset="UTF-8">
|
|
197
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
198
|
+
<title>${headerTitle} - Error ${data.status}</title>
|
|
199
|
+
<style>${ERROR_PAGE_CSS}</style>
|
|
200
|
+
</head>
|
|
201
|
+
<body>
|
|
202
|
+
<div class="error-header">
|
|
203
|
+
<h1>${escapeHtml(headerTitle)}</h1>
|
|
204
|
+
${messageHtml}
|
|
205
|
+
</div>
|
|
206
|
+
<div class="error-content">
|
|
207
|
+
${detailsHtml}
|
|
208
|
+
${requestHtml}
|
|
209
|
+
</div>
|
|
210
|
+
</body>
|
|
211
|
+
</html>`
|
|
212
|
+
return HttpServerResponse.text(html, {
|
|
213
|
+
status: data.status,
|
|
214
|
+
contentType: "text/html",
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function errorText(
|
|
219
|
+
status: number,
|
|
220
|
+
tag: string,
|
|
221
|
+
details?: object,
|
|
222
|
+
): HttpServerResponse.HttpServerResponse {
|
|
223
|
+
const text = details ? `${tag}\n${JSON.stringify(details, null, 2)}` : tag
|
|
224
|
+
return HttpServerResponse.text(text, { status })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function respondWithError(
|
|
228
|
+
accept: string,
|
|
229
|
+
status: number,
|
|
230
|
+
tag: string,
|
|
231
|
+
message?: string,
|
|
232
|
+
details?: object,
|
|
233
|
+
requestContext?: RequestContext,
|
|
234
|
+
errorName?: string,
|
|
235
|
+
): HttpServerResponse.HttpServerResponse {
|
|
236
|
+
if (accept.includes("text/html")) {
|
|
237
|
+
return errorHtml({
|
|
238
|
+
status,
|
|
239
|
+
tag,
|
|
240
|
+
message,
|
|
241
|
+
details,
|
|
242
|
+
requestContext,
|
|
243
|
+
errorName,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
if (accept.includes("text/plain")) {
|
|
247
|
+
return errorText(status, tag, details)
|
|
248
|
+
}
|
|
249
|
+
return HttpServerResponse.unsafeJson(
|
|
250
|
+
{ error: { _tag: tag, ...details } },
|
|
251
|
+
{ status },
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
30
255
|
export const renderError = (
|
|
31
256
|
error: unknown,
|
|
257
|
+
accept: string = "",
|
|
32
258
|
) =>
|
|
33
259
|
Effect.gen(function*() {
|
|
260
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
261
|
+
|
|
262
|
+
const requestContext: RequestContext = {
|
|
263
|
+
url: request.url,
|
|
264
|
+
method: request.method,
|
|
265
|
+
headers: filterSensitiveHeaders(request.headers),
|
|
266
|
+
}
|
|
267
|
+
|
|
34
268
|
let unwrappedError: GraciousError | undefined
|
|
35
269
|
|
|
36
270
|
if (Cause.isCause(error)) {
|
|
@@ -45,68 +279,175 @@ export const renderError = (
|
|
|
45
279
|
|
|
46
280
|
switch (unwrappedError?._tag) {
|
|
47
281
|
case "RouteNotFound":
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
282
|
+
return respondWithError(
|
|
283
|
+
accept,
|
|
284
|
+
404,
|
|
285
|
+
"RouteNotFound",
|
|
286
|
+
"The page you were looking for doesn't exist",
|
|
287
|
+
undefined,
|
|
288
|
+
requestContext,
|
|
289
|
+
)
|
|
55
290
|
case "RequestError": {
|
|
56
291
|
const message = unwrappedError.reason === "Decode"
|
|
57
292
|
? "Request body is invalid"
|
|
58
|
-
:
|
|
293
|
+
: "Request could not be processed"
|
|
59
294
|
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
295
|
+
return respondWithError(
|
|
296
|
+
accept,
|
|
297
|
+
400,
|
|
298
|
+
"RequestError",
|
|
299
|
+
message,
|
|
300
|
+
{
|
|
63
301
|
reason: unwrappedError.reason,
|
|
64
|
-
message,
|
|
65
302
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
})
|
|
303
|
+
requestContext,
|
|
304
|
+
)
|
|
69
305
|
}
|
|
70
306
|
case "ParseError": {
|
|
71
307
|
const issues = yield* ParseResult.ArrayFormatter.formatIssue(
|
|
72
308
|
unwrappedError.issue,
|
|
73
309
|
)
|
|
74
|
-
const cleanIssues = issues.map(v => Record.remove(v, "_tag"))
|
|
310
|
+
const cleanIssues = issues.map((v) => Record.remove(v, "_tag"))
|
|
75
311
|
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
312
|
+
return respondWithError(
|
|
313
|
+
accept,
|
|
314
|
+
400,
|
|
315
|
+
"ParseError",
|
|
316
|
+
"Validation failed",
|
|
317
|
+
{
|
|
79
318
|
issues: cleanIssues,
|
|
80
319
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
})
|
|
320
|
+
requestContext,
|
|
321
|
+
)
|
|
84
322
|
}
|
|
85
323
|
}
|
|
86
324
|
|
|
87
|
-
|
|
88
|
-
error
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
325
|
+
if (Cause.isCause(error)) {
|
|
326
|
+
const defects = [...Cause.defects(error)]
|
|
327
|
+
const defect = defects[0]
|
|
328
|
+
if (defect instanceof Error) {
|
|
329
|
+
const stackFrames = extractPrettyStack(defect.stack ?? "")
|
|
330
|
+
return respondWithError(
|
|
331
|
+
accept,
|
|
332
|
+
500,
|
|
333
|
+
"UnexpectedError",
|
|
334
|
+
defect.message,
|
|
335
|
+
{
|
|
336
|
+
name: defect.name,
|
|
337
|
+
stack: stackFrames,
|
|
338
|
+
},
|
|
339
|
+
requestContext,
|
|
340
|
+
defect.name,
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return respondWithError(
|
|
346
|
+
accept,
|
|
347
|
+
500,
|
|
348
|
+
"UnexpectedError",
|
|
349
|
+
"An unexpected error occurred",
|
|
350
|
+
undefined,
|
|
351
|
+
requestContext,
|
|
352
|
+
"UnexpectedError",
|
|
353
|
+
)
|
|
94
354
|
})
|
|
95
355
|
|
|
96
|
-
function
|
|
356
|
+
function parseStackFrame(line: string): StackFrame | null {
|
|
357
|
+
const match = line.trim().match(StackLinePattern)
|
|
358
|
+
if (!match) return null
|
|
359
|
+
|
|
360
|
+
const [_, fn, fullPath] = match
|
|
361
|
+
const relativePath = fullPath.replace(process.cwd(), ".")
|
|
362
|
+
|
|
363
|
+
let type: "application" | "framework" | "node_modules"
|
|
364
|
+
if (relativePath.includes("node_modules")) {
|
|
365
|
+
type = "node_modules"
|
|
366
|
+
} else if (
|
|
367
|
+
relativePath.startsWith("./src")
|
|
368
|
+
|| relativePath.startsWith("./examples")
|
|
369
|
+
) {
|
|
370
|
+
type = "application"
|
|
371
|
+
} else {
|
|
372
|
+
type = "framework"
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
function: fn,
|
|
377
|
+
file: relativePath,
|
|
378
|
+
type,
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function extractPrettyStack(stack: string): StackFrame[] {
|
|
97
383
|
return stack
|
|
98
384
|
.split("\n")
|
|
99
385
|
.slice(1)
|
|
100
|
-
.map(
|
|
101
|
-
|
|
386
|
+
.map(parseStackFrame)
|
|
387
|
+
.filter((frame): frame is StackFrame => frame !== null)
|
|
388
|
+
}
|
|
102
389
|
|
|
103
|
-
|
|
390
|
+
function renderStackFrames(frames: StackFrame[]): string {
|
|
391
|
+
if (frames.length === 0) {
|
|
392
|
+
return "<li>No stack frames</li>"
|
|
393
|
+
}
|
|
394
|
+
return frames
|
|
395
|
+
.map(
|
|
396
|
+
(f) =>
|
|
397
|
+
`<li><code>${f.function}</code> at <span class="path">${f.file}</span></li>`,
|
|
398
|
+
)
|
|
399
|
+
.join("")
|
|
400
|
+
}
|
|
104
401
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
402
|
+
function renderStackTrace(frames: StackFrame[]): string {
|
|
403
|
+
return `
|
|
404
|
+
<div class="stack-trace">
|
|
405
|
+
<div class="stack-trace-header">Stack Trace (${frames.length})</div>
|
|
406
|
+
<ul class="stack-list">${renderStackFrames(frames)}</ul>
|
|
407
|
+
</div>
|
|
408
|
+
`
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function escapeHtml(unsafe: string): string {
|
|
412
|
+
return unsafe
|
|
413
|
+
.replace(/&/g, "&")
|
|
414
|
+
.replace(/</g, "<")
|
|
415
|
+
.replace(/>/g, ">")
|
|
416
|
+
.replace(/"/g, """)
|
|
417
|
+
.replace(/'/g, "'")
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function filterSensitiveHeaders(
|
|
421
|
+
headers: Record<string, string>,
|
|
422
|
+
): Record<string, string> {
|
|
423
|
+
const sensitive = ["authorization", "cookie", "x-api-key"]
|
|
424
|
+
return Object.fromEntries(
|
|
425
|
+
Object.entries(headers).filter(
|
|
426
|
+
([key]) => !sensitive.includes(key.toLowerCase()),
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
type RequestContext = {
|
|
432
|
+
url: string
|
|
433
|
+
method: string
|
|
434
|
+
headers: Record<string, string>
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function renderRequestContext(context: RequestContext): string {
|
|
438
|
+
const headersText = Object
|
|
439
|
+
.entries(context.headers)
|
|
440
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
441
|
+
.join("\n")
|
|
442
|
+
|
|
443
|
+
const requestText = `${context.method} ${context.url}\n${headersText}`
|
|
444
|
+
|
|
445
|
+
return `
|
|
446
|
+
<div class="request-info">
|
|
447
|
+
<div class="request-info-header">Request</div>
|
|
448
|
+
<div class="request-info-content">${escapeHtml(requestText)}</div>
|
|
449
|
+
</div>
|
|
450
|
+
`
|
|
110
451
|
}
|
|
111
452
|
|
|
112
453
|
export function handleErrors<
|
|
@@ -114,14 +455,25 @@ export function handleErrors<
|
|
|
114
455
|
R,
|
|
115
456
|
>(
|
|
116
457
|
app: HttpApp.Default<E, R>,
|
|
117
|
-
): HttpApp.Default<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
458
|
+
): HttpApp.Default<
|
|
459
|
+
Exclude<E, RouteNotFound>,
|
|
460
|
+
R | HttpServerRequest.HttpServerRequest
|
|
461
|
+
> {
|
|
462
|
+
return Effect.gen(function*() {
|
|
463
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
464
|
+
const accept = request.headers.accept ?? ""
|
|
465
|
+
return yield* app.pipe(
|
|
466
|
+
Effect.catchAllCause((cause) => renderError(cause, accept)),
|
|
467
|
+
)
|
|
468
|
+
})
|
|
121
469
|
}
|
|
122
470
|
|
|
123
|
-
export const withErrorHandled = HttpMiddleware.make(app =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
471
|
+
export const withErrorHandled = HttpMiddleware.make(app =>
|
|
472
|
+
Effect.gen(function*() {
|
|
473
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
474
|
+
const accept = request.headers.accept ?? ""
|
|
475
|
+
return yield* app.pipe(
|
|
476
|
+
Effect.catchAllCause((cause) => renderError(cause, accept)),
|
|
477
|
+
)
|
|
478
|
+
})
|
|
479
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
2
|
+
import * as t from "bun:test"
|
|
3
|
+
|
|
4
|
+
import * as HttpUtils from "./HttpUtils.ts"
|
|
5
|
+
|
|
6
|
+
const makeRequest = (url: string, headers: Record<string, string> = {}) =>
|
|
7
|
+
HttpServerRequest.fromWeb(
|
|
8
|
+
new Request(`http://test${url}`, { headers }),
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
t.describe("makeUrlFromRequest", () => {
|
|
12
|
+
t.it("uses Host header for relative URL", () => {
|
|
13
|
+
const request = makeRequest("/api/users", {
|
|
14
|
+
host: "example.com",
|
|
15
|
+
})
|
|
16
|
+
const url = HttpUtils.makeUrlFromRequest(request)
|
|
17
|
+
|
|
18
|
+
t.expect(url.href).toBe("http://example.com/api/users")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
t.it("uses Origin header when present (takes precedence over Host)", () => {
|
|
22
|
+
const request = makeRequest("/api/users", {
|
|
23
|
+
origin: "https://app.example.com",
|
|
24
|
+
host: "example.com",
|
|
25
|
+
})
|
|
26
|
+
const url = HttpUtils.makeUrlFromRequest(request)
|
|
27
|
+
|
|
28
|
+
t.expect(url.href).toBe("https://app.example.com/api/users")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
t.it("uses X-Forwarded-Proto for protocol behind reverse proxy", () => {
|
|
32
|
+
const request = makeRequest("/api/users", {
|
|
33
|
+
host: "example.com",
|
|
34
|
+
"x-forwarded-proto": "https",
|
|
35
|
+
})
|
|
36
|
+
const url = HttpUtils.makeUrlFromRequest(request)
|
|
37
|
+
|
|
38
|
+
t.expect(url.href).toBe("https://example.com/api/users")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
t.it("falls back to http://localhost when no headers", () => {
|
|
42
|
+
const request = makeRequest("/api/users", {})
|
|
43
|
+
const url = HttpUtils.makeUrlFromRequest(request)
|
|
44
|
+
|
|
45
|
+
t.expect(url.href).toBe("http://localhost/api/users")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
t.it("handles URL with query parameters", () => {
|
|
49
|
+
const request = makeRequest("/search?q=test&page=1", {
|
|
50
|
+
host: "example.com",
|
|
51
|
+
})
|
|
52
|
+
const url = HttpUtils.makeUrlFromRequest(request)
|
|
53
|
+
|
|
54
|
+
t.expect(url.href).toBe("http://example.com/search?q=test&page=1")
|
|
55
|
+
t.expect(url.searchParams.get("q")).toBe("test")
|
|
56
|
+
t.expect(url.searchParams.get("page")).toBe("1")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
t.it("handles root path", () => {
|
|
60
|
+
const request = makeRequest("/", {
|
|
61
|
+
host: "example.com",
|
|
62
|
+
})
|
|
63
|
+
const url = HttpUtils.makeUrlFromRequest(request)
|
|
64
|
+
|
|
65
|
+
t.expect(url.href).toBe("http://example.com/")
|
|
66
|
+
t.expect(url.pathname).toBe("/")
|
|
67
|
+
})
|
|
68
|
+
})
|
package/src/HttpUtils.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
2
|
+
|
|
3
|
+
export function makeUrlFromRequest(
|
|
4
|
+
request: HttpServerRequest.HttpServerRequest,
|
|
5
|
+
): URL {
|
|
6
|
+
const origin = request.headers.origin
|
|
7
|
+
?? request.headers.host
|
|
8
|
+
?? "http://localhost"
|
|
9
|
+
const protocol = request.headers["x-forwarded-proto"] ?? "http"
|
|
10
|
+
const host = request.headers.host ?? "localhost"
|
|
11
|
+
const base = origin.startsWith("http")
|
|
12
|
+
? origin
|
|
13
|
+
: `${protocol}://${host}`
|
|
14
|
+
return new URL(request.url, base)
|
|
15
|
+
}
|
package/src/HyperHtml.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders Hyper JSX nodes to HTML.
|
|
3
|
+
*
|
|
4
|
+
* Effect Start comes with {@link Hyper} and {@link JsxRuntime} to enable
|
|
5
|
+
* JSX support. The advantage of using JSX over HTML strings or templates
|
|
6
|
+
* is type safety and better editor support.
|
|
7
|
+
*
|
|
8
|
+
* JSX nodes are compatible with React's and Solid's.
|
|
9
|
+
|
|
10
|
+
* You can enable JSX support by updating `tsconfig.json`:
|
|
11
|
+
*
|
|
12
|
+
* {
|
|
13
|
+
* compilerOptions: {
|
|
14
|
+
* jsx: "react-jsx",
|
|
15
|
+
* jsxImportSource: "effect-start" | "react" | "praect" // etc.
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type * as Hyper from "./Hyper.tsx"
|
|
1
21
|
import * as HyperNode from "./HyperNode.ts"
|
|
2
22
|
import { JSX } from "./jsx"
|
|
23
|
+
import type * as JsxRuntime from "./jsx-runtime.ts"
|
|
3
24
|
|
|
4
|
-
/**
|
|
5
|
-
* From: https://github.com/developit/vhtml
|
|
6
|
-
*/
|
|
7
25
|
const EMPTY_TAGS = [
|
|
8
26
|
"area",
|
|
9
27
|
"base",
|
|
@@ -93,8 +111,8 @@ export function renderToString(
|
|
|
93
111
|
for (const key in props) {
|
|
94
112
|
if (
|
|
95
113
|
key !== "children"
|
|
96
|
-
&& key !== "innerHTML"
|
|
97
|
-
&& key !== "dangerouslySetInnerHTML"
|
|
114
|
+
&& key !== "innerHTML" // Solid-specific
|
|
115
|
+
&& key !== "dangerouslySetInnerHTML" // React-specific
|
|
98
116
|
&& props[key] !== false
|
|
99
117
|
&& props[key] != null
|
|
100
118
|
) {
|
|
@@ -113,6 +131,7 @@ export function renderToString(
|
|
|
113
131
|
if (!EMPTY_TAGS.includes(type)) {
|
|
114
132
|
stack.push(`</${type}>`)
|
|
115
133
|
|
|
134
|
+
// React-specific
|
|
116
135
|
const html = props.dangerouslySetInnerHTML?.__html
|
|
117
136
|
?? props.innerHTML
|
|
118
137
|
|
package/src/JsModule.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as t from "bun:test"
|
|
2
2
|
import * as JsModule from "./JsModule.ts"
|
|
3
3
|
|
|
4
|
-
t.describe(
|
|
4
|
+
t.describe(`${JsModule.importSource.name}`, () => {
|
|
5
5
|
t.it("imports a string", async () => {
|
|
6
6
|
const mod = await JsModule.importSource<any>(`
|
|
7
7
|
export const b = "B"
|