effect-start 0.20.0 → 0.21.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.
@@ -51,6 +51,8 @@ let map = {
51
51
  "'": "apos",
52
52
  }
53
53
 
54
+ const RAW_TEXT_TAGS = ["script", "style"]
55
+
54
56
  export function renderToString(
55
57
  node: JSX.Children,
56
58
  hooks?: { onNode?: (node: HyperNode.HyperNode) => void },
@@ -147,6 +149,8 @@ export function renderToString(
147
149
 
148
150
  if (type === "script" && typeof children === "function") {
149
151
  result += `(${children.toString()})(window)`
152
+ } else if (RAW_TEXT_TAGS.includes(type) && children != null) {
153
+ result += Array.isArray(children) ? children.join("") : children
150
154
  } else if (Array.isArray(children)) {
151
155
  for (let i = children.length - 1; i >= 0; i--) {
152
156
  stack.push(children[i])
@@ -1,12 +1,12 @@
1
1
  /*
2
2
  * Adapted from @effect/platform
3
3
  */
4
- import * as FileSystem from "@effect/platform/FileSystem"
5
4
  import type * as Context from "effect/Context"
6
5
  import * as Effect from "effect/Effect"
7
6
  import * as Function from "effect/Function"
8
7
  import * as Layer from "effect/Layer"
9
8
  import * as Option from "effect/Option"
9
+ import * as FileSystem from "../FileSystem.ts"
10
10
  import * as Stream from "effect/Stream"
11
11
  import * as NCrypto from "node:crypto"
12
12
  import * as NFS from "node:fs"
@@ -665,23 +665,9 @@ const make = Effect.map(
665
665
  }),
666
666
  )
667
667
 
668
- export const layer = Layer.unwrapEffect(
669
- Effect
670
- .gen(function*() {
671
- const mod = yield* Effect.tryPromise(() =>
672
- import("@effect/platform/FileSystem")
673
- )
674
- return Layer.effect(mod.FileSystem, make)
675
- })
676
- .pipe(
677
- Effect.catchAll(() =>
678
- Effect.die(new globalThis.Error("@effect/platform is not installed"))
679
- ),
680
- ),
681
- )
668
+ export const layer = Layer.effect(FileSystem.FileSystem, make)
682
669
 
683
670
  export {
684
- FileSystem,
685
671
  PlatformError as Error,
686
672
  }
687
673
 
@@ -1,3 +1,2 @@
1
- export * as TestHttpClient from "./TestHttpClient.ts"
2
1
  export * as TestLogger from "./TestLogger.ts"
3
2
  export * from "./utils.ts"
@@ -1,9 +1,6 @@
1
- import {
2
- Command,
3
- HttpServer,
4
- } from "@effect/platform"
5
1
  import {
6
2
  Config,
3
+ Data,
7
4
  Effect,
8
5
  identity,
9
6
  Layer,
@@ -14,9 +11,10 @@ import {
14
11
  String,
15
12
  } from "effect"
16
13
 
17
- /**
18
- * Starts Cloudflare tunnel using cloudflared cli.
19
- */
14
+ export class CloudflareTunnelSpawnError extends Data.TaggedError(
15
+ "CloudflareTunnelSpawnError",
16
+ )<{ cause: unknown }> {}
17
+
20
18
  export const start = (opts: {
21
19
  command?: string
22
20
  tunnelName: string
@@ -42,21 +40,31 @@ export const start = (opts: {
42
40
  ]
43
41
  .flatMap(v => v)
44
42
 
45
- const process = yield* pipe(
46
- Command.make(opts.command ?? "cloudflared", ...args),
47
- Command.start,
43
+ const proc = yield* Effect.try({
44
+ try: () =>
45
+ Bun.spawn(
46
+ [opts.command ?? "cloudflared", ...args],
47
+ { stderr: "pipe", stdout: "pipe" },
48
+ ),
49
+ catch: (err) => new CloudflareTunnelSpawnError({ cause: err }),
50
+ })
51
+
52
+ yield* Effect.addFinalizer(() =>
53
+ Effect.sync(() => {
54
+ proc.kill()
55
+ })
48
56
  )
49
57
 
50
58
  yield* Effect.logInfo(
51
- `Cloudflare tunnel started name=${opts.tunnelName} pid=${process.pid} tunnelUrl=${
59
+ `Cloudflare tunnel started name=${opts.tunnelName} pid=${proc.pid} tunnelUrl=${
52
60
  opts.tunnelUrl ?? "<empty>"
53
61
  }`,
54
62
  )
55
63
 
56
64
  yield* pipe(
57
65
  Stream.merge(
58
- process.stdout,
59
- process.stderr,
66
+ Stream.fromReadableStream(() => proc.stdout, identity),
67
+ Stream.fromReadableStream(() => proc.stderr, identity),
60
68
  ),
61
69
  Stream.decodeText("utf-8"),
62
70
  Stream.splitLines,
@@ -95,16 +103,13 @@ export const layer = () =>
95
103
 
96
104
  yield* Effect
97
105
  .forkScoped(
98
- pipe(
99
- start({
100
- tunnelName,
101
- tunnelUrl,
102
- }),
103
- ),
104
- )
105
- .pipe(
106
- Effect.catchAll(err =>
107
- Effect.logError("Cloudflare tunnel failed", err)
106
+ start({
107
+ tunnelName,
108
+ tunnelUrl,
109
+ }).pipe(
110
+ Effect.catchAll(err =>
111
+ Effect.logError("Cloudflare tunnel failed", err)
112
+ ),
108
113
  ),
109
114
  )
110
115
  }))
@@ -1,478 +0,0 @@
1
- import {
2
- HttpRouter,
3
- HttpServerRequest,
4
- HttpServerResponse,
5
- } from "@effect/platform"
6
- import * as HttpApp from "@effect/platform/HttpApp"
7
- import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
8
- import {
9
- RequestError,
10
- RouteNotFound,
11
- } from "@effect/platform/HttpServerError"
12
- import {
13
- Cause,
14
- Effect,
15
- Option,
16
- ParseResult,
17
- Record,
18
- } from "effect"
19
-
20
- /**
21
- * Groups: function, path
22
- */
23
- const StackLinePattern = /^at (.*?) \((.*?)\)/
24
-
25
- type GraciousError =
26
- | RouteNotFound
27
- | ParseResult.ParseError
28
- | RequestError
29
- | ParseResult.ParseError
30
-
31
- type StackFrame = {
32
- function: string
33
- file: string
34
- type: "application" | "framework" | "node_modules"
35
- }
36
-
37
- const ERROR_PAGE_CSS = `
38
- :root {
39
- --error-red: #c00;
40
- --error-red-dark: #a00;
41
- --bg-error: #fee;
42
- --bg-light: #f5f5f5;
43
- --bg-white: #fff;
44
- --border-color: #ddd;
45
- --text-dark: #333;
46
- --text-gray: #666;
47
- --text-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
48
- }
49
-
50
- * { box-sizing: border-box; }
51
-
52
- body {
53
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
54
- margin: 0;
55
- padding: 0;
56
- color: var(--text-dark);
57
- line-height: 1.6;
58
- min-height: 100dvh;
59
- }
60
-
61
- .error-page { width: 100%; margin: 0; }
62
-
63
- .error-header {
64
- background: var(--error-red);
65
- color: white;
66
- padding: 2rem 2.5rem;
67
- margin: 0;
68
- font-family: var(--text-mono);
69
- }
70
-
71
- .error-header h1 {
72
- margin: 0 0 0.5rem 0;
73
- font-size: 2rem;
74
- font-weight: 600;
75
- font-family: var(--text-mono);
76
- }
77
-
78
- .error-message {
79
- margin: 0;
80
- font-size: 1.1rem;
81
- opacity: 0.95;
82
- font-family: var(--text-mono);
83
- }
84
-
85
- .error-content {
86
- background: var(--bg-white);
87
- padding: 2rem 2.5rem;
88
- }
89
-
90
- .stack-trace {
91
- margin: 1.5rem 0;
92
- border: 1px solid var(--border-color);
93
- border-radius: 4px;
94
- overflow: hidden;
95
- }
96
-
97
- .stack-trace-header {
98
- font-weight: 600;
99
- padding: 0.75rem 1rem;
100
- background: var(--bg-light);
101
- border-bottom: 1px solid var(--border-color);
102
- }
103
-
104
- .stack-list {
105
- list-style: none;
106
- padding: 0;
107
- margin: 0;
108
- max-height: 400px;
109
- overflow-y: auto;
110
- }
111
-
112
- .stack-list li {
113
- padding: 0.5rem 1rem;
114
- font-family: var(--text-mono);
115
- font-size: 0.875rem;
116
- border-bottom: 1px solid var(--border-color);
117
- background: var(--bg-white);
118
- }
119
-
120
- .stack-list li:last-child { border-bottom: none; }
121
-
122
- .stack-list li:hover { background: #fafafa; }
123
-
124
- .stack-list code {
125
- background: transparent;
126
- padding: 0;
127
- font-weight: 600;
128
- color: var(--error-red-dark);
129
- }
130
-
131
- .stack-list .path { color: var(--text-gray); margin-left: 0.5rem; }
132
-
133
- .request-info {
134
- margin: 1.5rem 0;
135
- border: 1px solid var(--border-color);
136
- border-radius: 4px;
137
- overflow: hidden;
138
- }
139
-
140
- .request-info-header {
141
- font-weight: 700;
142
- padding: 0.75rem 1rem;
143
- background: var(--bg-light);
144
- border-bottom: 1px solid var(--border-color);
145
- }
146
-
147
- .request-info-content {
148
- padding: 1rem;
149
- font-family: var(--text-mono);
150
- font-size: 0.875rem;
151
- white-space: pre-wrap;
152
- word-break: break-all;
153
- }
154
-
155
- @media (max-width: 768px) {
156
- .error-header, .error-content { padding: 1.5rem 1rem; }
157
- .error-header h1 { font-size: 1.5rem; }
158
- }
159
- `
160
-
161
- type ErrorHtmlData = {
162
- status: number
163
- tag: string
164
- message?: string
165
- details?: object
166
- requestContext?: RequestContext
167
- errorName?: string
168
- }
169
-
170
- function errorHtml(data: ErrorHtmlData): HttpServerResponse.HttpServerResponse {
171
- let detailsHtml = ""
172
-
173
- if (data.details) {
174
- const detailsObj = data.details as Record<string, unknown>
175
-
176
- if ("stack" in detailsObj && Array.isArray(detailsObj.stack)) {
177
- const stackFrames = detailsObj.stack as StackFrame[]
178
- detailsHtml = renderStackTrace(stackFrames)
179
- }
180
- }
181
-
182
- const requestHtml = data.requestContext
183
- ? renderRequestContext(data.requestContext)
184
- : ""
185
-
186
- const messageHtml = data.message
187
- ? `<p class="error-message">${escapeHtml(data.message)}</p>`
188
- : ""
189
-
190
- const headerTitle = data.errorName ?? "UnexpectedError"
191
-
192
- const html = `<!DOCTYPE html>
193
- <html lang="en">
194
- <head>
195
- <meta charset="UTF-8">
196
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
197
- <title>${headerTitle} - Error ${data.status}</title>
198
- <style>${ERROR_PAGE_CSS}</style>
199
- </head>
200
- <body>
201
- <div class="error-header">
202
- <h1>${escapeHtml(headerTitle)}</h1>
203
- ${messageHtml}
204
- </div>
205
- <div class="error-content">
206
- ${detailsHtml}
207
- ${requestHtml}
208
- </div>
209
- </body>
210
- </html>`
211
- return HttpServerResponse.text(html, {
212
- status: data.status,
213
- contentType: "text/html",
214
- })
215
- }
216
-
217
- function errorText(
218
- status: number,
219
- tag: string,
220
- details?: object,
221
- ): HttpServerResponse.HttpServerResponse {
222
- const text = details ? `${tag}\n${JSON.stringify(details, null, 2)}` : tag
223
- return HttpServerResponse.text(text, { status })
224
- }
225
-
226
- function respondWithError(
227
- accept: string,
228
- status: number,
229
- tag: string,
230
- message?: string,
231
- details?: object,
232
- requestContext?: RequestContext,
233
- errorName?: string,
234
- ): HttpServerResponse.HttpServerResponse {
235
- if (accept.includes("text/html")) {
236
- return errorHtml({
237
- status,
238
- tag,
239
- message,
240
- details,
241
- requestContext,
242
- errorName,
243
- })
244
- }
245
- if (accept.includes("text/plain")) {
246
- return errorText(status, tag, details)
247
- }
248
- return HttpServerResponse.unsafeJson(
249
- { error: { _tag: tag, ...details } },
250
- { status },
251
- )
252
- }
253
-
254
- export const renderError = (
255
- error: unknown,
256
- accept: string = "",
257
- ) =>
258
- Effect.gen(function*() {
259
- const request = yield* HttpServerRequest.HttpServerRequest
260
-
261
- const requestContext: RequestContext = {
262
- url: request.url,
263
- method: request.method,
264
- headers: filterSensitiveHeaders(request.headers),
265
- }
266
-
267
- let unwrappedError: GraciousError | undefined
268
-
269
- if (Cause.isCause(error)) {
270
- const failure = Cause.failureOption(error).pipe(Option.getOrUndefined)
271
-
272
- if (failure?.["_tag"]) {
273
- unwrappedError = failure as GraciousError
274
- }
275
-
276
- yield* Effect.logError(error)
277
- }
278
-
279
- switch (unwrappedError?._tag) {
280
- case "RouteNotFound":
281
- return respondWithError(
282
- accept,
283
- 404,
284
- "RouteNotFound",
285
- "The page you were looking for doesn't exist",
286
- undefined,
287
- requestContext,
288
- )
289
- case "RequestError": {
290
- const message = unwrappedError.reason === "Decode"
291
- ? "Request body is invalid"
292
- : "Request could not be processed"
293
-
294
- return respondWithError(
295
- accept,
296
- 400,
297
- "RequestError",
298
- message,
299
- {
300
- reason: unwrappedError.reason,
301
- },
302
- requestContext,
303
- )
304
- }
305
- case "ParseError": {
306
- const issues = yield* ParseResult.ArrayFormatter.formatIssue(
307
- unwrappedError.issue,
308
- )
309
- const cleanIssues = issues.map((v) => Record.remove(v, "_tag"))
310
-
311
- return respondWithError(
312
- accept,
313
- 400,
314
- "ParseError",
315
- "Validation failed",
316
- {
317
- issues: cleanIssues,
318
- },
319
- requestContext,
320
- )
321
- }
322
- }
323
-
324
- if (Cause.isCause(error)) {
325
- const defects = [...Cause.defects(error)]
326
- const defect = defects[0]
327
- if (defect instanceof Error) {
328
- const stackFrames = extractPrettyStack(defect.stack ?? "")
329
- return respondWithError(
330
- accept,
331
- 500,
332
- "UnexpectedError",
333
- defect.message,
334
- {
335
- name: defect.name,
336
- stack: stackFrames,
337
- },
338
- requestContext,
339
- defect.name,
340
- )
341
- }
342
- }
343
-
344
- return respondWithError(
345
- accept,
346
- 500,
347
- "UnexpectedError",
348
- "An unexpected error occurred",
349
- undefined,
350
- requestContext,
351
- "UnexpectedError",
352
- )
353
- })
354
-
355
- function parseStackFrame(line: string): StackFrame | null {
356
- const match = line.trim().match(StackLinePattern)
357
- if (!match) return null
358
-
359
- const [_, fn, fullPath] = match
360
- const relativePath = fullPath.replace(process.cwd(), ".")
361
-
362
- let type: "application" | "framework" | "node_modules"
363
- if (relativePath.includes("node_modules")) {
364
- type = "node_modules"
365
- } else if (
366
- relativePath.startsWith("./src")
367
- || relativePath.startsWith("./examples")
368
- ) {
369
- type = "application"
370
- } else {
371
- type = "framework"
372
- }
373
-
374
- return {
375
- function: fn,
376
- file: relativePath,
377
- type,
378
- }
379
- }
380
-
381
- function extractPrettyStack(stack: string): StackFrame[] {
382
- return stack
383
- .split("\n")
384
- .slice(1)
385
- .map(parseStackFrame)
386
- .filter((frame): frame is StackFrame => frame !== null)
387
- }
388
-
389
- function renderStackFrames(frames: StackFrame[]): string {
390
- if (frames.length === 0) {
391
- return "<li>No stack frames</li>"
392
- }
393
- return frames
394
- .map(
395
- (f) =>
396
- `<li><code>${f.function}</code> at <span class="path">${f.file}</span></li>`,
397
- )
398
- .join("")
399
- }
400
-
401
- function renderStackTrace(frames: StackFrame[]): string {
402
- return `
403
- <div class="stack-trace">
404
- <div class="stack-trace-header">Stack Trace (${frames.length})</div>
405
- <ul class="stack-list">${renderStackFrames(frames)}</ul>
406
- </div>
407
- `
408
- }
409
-
410
- function escapeHtml(unsafe: string): string {
411
- return unsafe
412
- .replace(/&/g, "&amp;")
413
- .replace(/</g, "&lt;")
414
- .replace(/>/g, "&gt;")
415
- .replace(/"/g, "&quot;")
416
- .replace(/'/g, "&#039;")
417
- }
418
-
419
- function filterSensitiveHeaders(
420
- headers: Record<string, string>,
421
- ): Record<string, string> {
422
- const sensitive = ["authorization", "cookie", "x-api-key"]
423
- return Object.fromEntries(
424
- Object.entries(headers).filter(
425
- ([key]) => !sensitive.includes(key.toLowerCase()),
426
- ),
427
- )
428
- }
429
-
430
- type RequestContext = {
431
- url: string
432
- method: string
433
- headers: Record<string, string>
434
- }
435
-
436
- function renderRequestContext(context: RequestContext): string {
437
- const headersText = Object
438
- .entries(context.headers)
439
- .map(([key, value]) => `${key}: ${value}`)
440
- .join("\n")
441
-
442
- const requestText = `${context.method} ${context.url}\n${headersText}`
443
-
444
- return `
445
- <div class="request-info">
446
- <div class="request-info-header">Request</div>
447
- <div class="request-info-content">${escapeHtml(requestText)}</div>
448
- </div>
449
- `
450
- }
451
-
452
- export function handleErrors<
453
- E,
454
- R,
455
- >(
456
- app: HttpApp.Default<E, R>,
457
- ): HttpApp.Default<
458
- Exclude<E, RouteNotFound>,
459
- R | HttpServerRequest.HttpServerRequest
460
- > {
461
- return Effect.gen(function*() {
462
- const request = yield* HttpServerRequest.HttpServerRequest
463
- const accept = request.headers.accept ?? ""
464
- return yield* app.pipe(
465
- Effect.catchAllCause((cause) => renderError(cause, accept)),
466
- )
467
- })
468
- }
469
-
470
- export const withErrorHandled = HttpMiddleware.make(app =>
471
- Effect.gen(function*() {
472
- const request = yield* HttpServerRequest.HttpServerRequest
473
- const accept = request.headers.accept ?? ""
474
- return yield* app.pipe(
475
- Effect.catchAllCause((cause) => renderError(cause, accept)),
476
- )
477
- })
478
- )
package/src/HttpUtils.ts DELETED
@@ -1,17 +0,0 @@
1
- import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
2
-
3
- export type FetchHandler = (request: Request) => Promise<Response>
4
-
5
- export function makeUrlFromRequest(
6
- request: HttpServerRequest.HttpServerRequest,
7
- ): URL {
8
- const origin = request.headers.origin
9
- ?? request.headers.host
10
- ?? "http://localhost"
11
- const protocol = request.headers["x-forwarded-proto"] ?? "http"
12
- const host = request.headers.host ?? "localhost"
13
- const base = origin.startsWith("http")
14
- ? origin
15
- : `${protocol}://${host}`
16
- return new URL(request.url, base)
17
- }