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,384 @@
1
+ import * as Cookies from "@effect/platform/Cookies"
2
+ import type * as FileSystem from "@effect/platform/FileSystem"
3
+ import * as Headers from "@effect/platform/Headers"
4
+ import * as HttpApp from "@effect/platform/HttpApp"
5
+ import * as HttpIncomingMessage from "@effect/platform/HttpIncomingMessage"
6
+ import type { HttpMethod } from "@effect/platform/HttpMethod"
7
+ import * as HttpServerError from "@effect/platform/HttpServerError"
8
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
9
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
10
+ import type * as Multipart from "@effect/platform/Multipart"
11
+ import type * as Path from "@effect/platform/Path"
12
+ import * as Socket from "@effect/platform/Socket"
13
+ import * as UrlParams from "@effect/platform/UrlParams"
14
+ import type {
15
+ Server as BunServerInstance,
16
+ ServerWebSocket,
17
+ } from "bun"
18
+ import * as Deferred from "effect/Deferred"
19
+ import * as Effect from "effect/Effect"
20
+ import * as FiberSet from "effect/FiberSet"
21
+ import * as Inspectable from "effect/Inspectable"
22
+ import * as Option from "effect/Option"
23
+ import type { ReadonlyRecord } from "effect/Record"
24
+ import * as Runtime from "effect/Runtime"
25
+ import type * as Scope from "effect/Scope"
26
+ import * as Stream from "effect/Stream"
27
+
28
+ export interface WebSocketContext {
29
+ readonly deferred: Deferred.Deferred<ServerWebSocket<WebSocketContext>>
30
+ readonly closeDeferred: Deferred.Deferred<void, Socket.SocketError>
31
+ readonly buffer: Array<Uint8Array | string>
32
+ run: (_: Uint8Array | string) => void
33
+ }
34
+
35
+ export class ServerRequestImpl extends Inspectable.Class
36
+ implements HttpServerRequest.HttpServerRequest
37
+ {
38
+ readonly [HttpServerRequest.TypeId]: HttpServerRequest.TypeId
39
+ readonly [HttpIncomingMessage.TypeId]: HttpIncomingMessage.TypeId
40
+
41
+ constructor(
42
+ readonly source: Request,
43
+ public resolve: (response: Response) => void,
44
+ readonly url: string,
45
+ private bunServer: BunServerInstance<WebSocketContext>,
46
+ public headersOverride?: Headers.Headers,
47
+ private remoteAddressOverride?: string,
48
+ ) {
49
+ super()
50
+ this[HttpServerRequest.TypeId] = HttpServerRequest.TypeId
51
+ this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId
52
+ }
53
+
54
+ toJSON(): unknown {
55
+ return HttpIncomingMessage.inspect(this, {
56
+ _id: "@effect/platform/HttpServerRequest",
57
+ method: this.method,
58
+ url: this.originalUrl,
59
+ })
60
+ }
61
+
62
+ modify(
63
+ options: {
64
+ readonly url?: string | undefined
65
+ readonly headers?: Headers.Headers | undefined
66
+ readonly remoteAddress?: string | undefined
67
+ },
68
+ ) {
69
+ return new ServerRequestImpl(
70
+ this.source,
71
+ this.resolve,
72
+ options.url ?? this.url,
73
+ this.bunServer,
74
+ options.headers ?? this.headersOverride,
75
+ options.remoteAddress ?? this.remoteAddressOverride,
76
+ )
77
+ }
78
+
79
+ get method(): HttpMethod {
80
+ return this.source.method.toUpperCase() as HttpMethod
81
+ }
82
+
83
+ get originalUrl() {
84
+ return this.source.url
85
+ }
86
+
87
+ get remoteAddress(): Option.Option<string> {
88
+ return this.remoteAddressOverride
89
+ ? Option.some(this.remoteAddressOverride)
90
+ : Option.fromNullable(this.bunServer.requestIP(this.source)?.address)
91
+ }
92
+
93
+ get headers(): Headers.Headers {
94
+ this.headersOverride ??= Headers.fromInput(this.source.headers)
95
+ return this.headersOverride
96
+ }
97
+
98
+ private cachedCookies: ReadonlyRecord<string, string> | undefined
99
+ get cookies() {
100
+ if (this.cachedCookies) {
101
+ return this.cachedCookies
102
+ }
103
+ return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "")
104
+ }
105
+
106
+ get stream(): Stream.Stream<Uint8Array, HttpServerError.RequestError> {
107
+ return this.source.body
108
+ ? Stream.fromReadableStream(
109
+ () => this.source.body as ReadableStream<Uint8Array>,
110
+ (cause) =>
111
+ new HttpServerError.RequestError({
112
+ request: this,
113
+ reason: "Decode",
114
+ cause,
115
+ }),
116
+ )
117
+ : Stream.fail(
118
+ new HttpServerError.RequestError({
119
+ request: this,
120
+ reason: "Decode",
121
+ description: "can not create stream from empty body",
122
+ }),
123
+ )
124
+ }
125
+
126
+ private textEffect:
127
+ | Effect.Effect<string, HttpServerError.RequestError>
128
+ | undefined
129
+
130
+ get text(): Effect.Effect<string, HttpServerError.RequestError> {
131
+ if (this.textEffect) {
132
+ return this.textEffect
133
+ }
134
+ this.textEffect = Effect.runSync(Effect.cached(
135
+ Effect.tryPromise({
136
+ try: () => this.source.text(),
137
+ catch: (cause) =>
138
+ new HttpServerError.RequestError({
139
+ request: this,
140
+ reason: "Decode",
141
+ cause,
142
+ }),
143
+ }),
144
+ ))
145
+ return this.textEffect
146
+ }
147
+
148
+ get json(): Effect.Effect<unknown, HttpServerError.RequestError> {
149
+ return Effect.tryMap(this.text, {
150
+ try: (_) => JSON.parse(_) as unknown,
151
+ catch: (cause) =>
152
+ new HttpServerError.RequestError({
153
+ request: this,
154
+ reason: "Decode",
155
+ cause,
156
+ }),
157
+ })
158
+ }
159
+
160
+ get urlParamsBody(): Effect.Effect<
161
+ UrlParams.UrlParams,
162
+ HttpServerError.RequestError
163
+ > {
164
+ return Effect.flatMap(this.text, (_) =>
165
+ Effect.try({
166
+ try: () => UrlParams.fromInput(new URLSearchParams(_)),
167
+ catch: (cause) =>
168
+ new HttpServerError.RequestError({
169
+ request: this,
170
+ reason: "Decode",
171
+ cause,
172
+ }),
173
+ }))
174
+ }
175
+
176
+ private multipartEffect:
177
+ | Effect.Effect<
178
+ Multipart.Persisted,
179
+ Multipart.MultipartError,
180
+ Scope.Scope | FileSystem.FileSystem | Path.Path
181
+ >
182
+ | undefined
183
+
184
+ get multipart(): Effect.Effect<
185
+ Multipart.Persisted,
186
+ Multipart.MultipartError,
187
+ Scope.Scope | FileSystem.FileSystem | Path.Path
188
+ > {
189
+ if (this.multipartEffect) {
190
+ return this.multipartEffect
191
+ }
192
+ this.multipartEffect = Effect.runSync(Effect.cached(
193
+ Effect.die("Multipart not implemented"),
194
+ ))
195
+ return this.multipartEffect
196
+ }
197
+
198
+ get multipartStream(): Stream.Stream<
199
+ Multipart.Part,
200
+ Multipart.MultipartError
201
+ > {
202
+ return Stream.die("Multipart stream not implemented")
203
+ }
204
+
205
+ private arrayBufferEffect:
206
+ | Effect.Effect<ArrayBuffer, HttpServerError.RequestError>
207
+ | undefined
208
+ get arrayBuffer(): Effect.Effect<ArrayBuffer, HttpServerError.RequestError> {
209
+ if (this.arrayBufferEffect) {
210
+ return this.arrayBufferEffect
211
+ }
212
+ this.arrayBufferEffect = Effect.runSync(Effect.cached(
213
+ Effect.tryPromise({
214
+ try: () => this.source.arrayBuffer(),
215
+ catch: (cause) =>
216
+ new HttpServerError.RequestError({
217
+ request: this,
218
+ reason: "Decode",
219
+ cause,
220
+ }),
221
+ }),
222
+ ))
223
+ return this.arrayBufferEffect
224
+ }
225
+
226
+ get upgrade(): Effect.Effect<Socket.Socket, HttpServerError.RequestError> {
227
+ return Effect.flatMap(
228
+ Effect.all([
229
+ Deferred.make<ServerWebSocket<WebSocketContext>>(),
230
+ Deferred.make<void, Socket.SocketError>(),
231
+ Effect.makeSemaphore(1),
232
+ ]),
233
+ ([deferred, closeDeferred, semaphore]) =>
234
+ Effect.async<Socket.Socket, HttpServerError.RequestError>((resume) => {
235
+ const success = this.bunServer.upgrade(
236
+ this.source,
237
+ {
238
+ data: {
239
+ deferred,
240
+ closeDeferred,
241
+ buffer: [],
242
+ run: wsDefaultRun,
243
+ },
244
+ },
245
+ )
246
+ if (!success) {
247
+ resume(Effect.fail(
248
+ new HttpServerError.RequestError({
249
+ request: this,
250
+ reason: "Decode",
251
+ description: "Not an upgradeable ServerRequest",
252
+ }),
253
+ ))
254
+ return
255
+ }
256
+ resume(Effect.map(Deferred.await(deferred), (ws) => {
257
+ const write = (chunk: Uint8Array | string | Socket.CloseEvent) =>
258
+ Effect.sync(() => {
259
+ if (typeof chunk === "string") {
260
+ ws.sendText(chunk)
261
+ } else if (Socket.isCloseEvent(chunk)) {
262
+ ws.close(chunk.code, chunk.reason)
263
+ } else {
264
+ ws.sendBinary(chunk)
265
+ }
266
+
267
+ return true
268
+ })
269
+ const writer = Effect.succeed(write)
270
+ const runRaw = Effect.fnUntraced(
271
+ function*<RR, EE, _>(
272
+ handler: (
273
+ _: Uint8Array | string,
274
+ ) => Effect.Effect<_, EE, RR> | void,
275
+ opts?: { readonly onOpen?: Effect.Effect<void> | undefined },
276
+ ) {
277
+ const set = yield* FiberSet.make<unknown, EE>()
278
+ const run = yield* FiberSet.runtime(set)<RR>()
279
+ function runRawInner(data: Uint8Array | string) {
280
+ const result = handler(data)
281
+ if (Effect.isEffect(result)) {
282
+ run(result)
283
+ }
284
+ }
285
+ ws.data.run = runRawInner
286
+ ws.data.buffer.forEach(runRawInner)
287
+ ws.data.buffer.length = 0
288
+ if (opts?.onOpen) yield* opts.onOpen
289
+ return yield* FiberSet.join(set)
290
+ },
291
+ Effect.scoped,
292
+ Effect.onExit((exit) => {
293
+ ws.close(exit._tag === "Success" ? 1000 : 1011)
294
+ return Effect.void
295
+ }),
296
+ Effect.raceFirst(Deferred.await(closeDeferred)),
297
+ semaphore.withPermits(1),
298
+ )
299
+
300
+ const encoder = new TextEncoder()
301
+ const run = <RR, EE, _>(
302
+ handler: (_: Uint8Array) => Effect.Effect<_, EE, RR> | void,
303
+ opts?: {
304
+ readonly onOpen?: Effect.Effect<void> | undefined
305
+ },
306
+ ) =>
307
+ runRaw(
308
+ (data) =>
309
+ typeof data === "string"
310
+ ? handler(encoder.encode(data))
311
+ : handler(data),
312
+ opts,
313
+ )
314
+
315
+ return Socket.Socket.of({
316
+ [Socket.TypeId]: Socket.TypeId,
317
+ run,
318
+ runRaw,
319
+ writer,
320
+ })
321
+ }))
322
+ }),
323
+ )
324
+ }
325
+ }
326
+
327
+ function wsDefaultRun(this: WebSocketContext, _: Uint8Array | string) {
328
+ this.buffer.push(_)
329
+ }
330
+
331
+ export function makeResponse(
332
+ request: HttpServerRequest.HttpServerRequest,
333
+ response: HttpServerResponse.HttpServerResponse,
334
+ runtime: Runtime.Runtime<never>,
335
+ ): Response {
336
+ const fields: {
337
+ headers: globalThis.Headers
338
+ status?: number
339
+ statusText?: string
340
+ } = {
341
+ headers: new globalThis.Headers(response.headers),
342
+ status: response.status,
343
+ }
344
+
345
+ if (!Cookies.isEmpty(response.cookies)) {
346
+ for (const header of Cookies.toSetCookieHeaders(response.cookies)) {
347
+ fields.headers.append("set-cookie", header)
348
+ }
349
+ }
350
+
351
+ if (response.statusText !== undefined) {
352
+ fields.statusText = response.statusText
353
+ }
354
+
355
+ if (request.method === "HEAD") {
356
+ return new Response(undefined, fields)
357
+ }
358
+ const ejectedResponse = HttpApp.unsafeEjectStreamScope(response)
359
+ const body = ejectedResponse.body
360
+ switch (body._tag) {
361
+ case "Empty": {
362
+ return new Response(undefined, fields)
363
+ }
364
+ case "Uint8Array":
365
+ case "Raw": {
366
+ if (body.body instanceof Response) {
367
+ for (const [key, value] of fields.headers.entries()) {
368
+ body.body.headers.set(key, value)
369
+ }
370
+ return body.body
371
+ }
372
+ return new Response(body.body as BodyInit, fields)
373
+ }
374
+ case "FormData": {
375
+ return new Response(body.formData as FormData, fields)
376
+ }
377
+ case "Stream": {
378
+ return new Response(
379
+ Stream.toReadableStreamRuntime(body.stream, runtime),
380
+ fields,
381
+ )
382
+ }
383
+ }
384
+ }