effect-start 0.9.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/package.json +57 -0
  4. package/src/Bundle.ts +167 -0
  5. package/src/BundleFiles.ts +174 -0
  6. package/src/BundleHttp.test.ts +160 -0
  7. package/src/BundleHttp.ts +259 -0
  8. package/src/Commander.test.ts +1378 -0
  9. package/src/Commander.ts +672 -0
  10. package/src/Datastar.test.ts +267 -0
  11. package/src/Datastar.ts +68 -0
  12. package/src/Effect_HttpRouter.test.ts +570 -0
  13. package/src/EncryptedCookies.test.ts +427 -0
  14. package/src/EncryptedCookies.ts +451 -0
  15. package/src/FileHttpRouter.test.ts +207 -0
  16. package/src/FileHttpRouter.ts +122 -0
  17. package/src/FileRouter.ts +405 -0
  18. package/src/FileRouterCodegen.test.ts +598 -0
  19. package/src/FileRouterCodegen.ts +251 -0
  20. package/src/FileRouter_files.test.ts +64 -0
  21. package/src/FileRouter_path.test.ts +132 -0
  22. package/src/FileRouter_tree.test.ts +126 -0
  23. package/src/FileSystemExtra.ts +102 -0
  24. package/src/HttpAppExtra.ts +127 -0
  25. package/src/Hyper.ts +194 -0
  26. package/src/HyperHtml.test.ts +90 -0
  27. package/src/HyperHtml.ts +139 -0
  28. package/src/HyperNode.ts +37 -0
  29. package/src/JsModule.test.ts +14 -0
  30. package/src/JsModule.ts +116 -0
  31. package/src/PublicDirectory.test.ts +280 -0
  32. package/src/PublicDirectory.ts +108 -0
  33. package/src/Route.test.ts +873 -0
  34. package/src/Route.ts +992 -0
  35. package/src/Router.ts +80 -0
  36. package/src/SseHttpResponse.ts +55 -0
  37. package/src/Start.ts +133 -0
  38. package/src/StartApp.ts +43 -0
  39. package/src/StartHttp.ts +42 -0
  40. package/src/StreamExtra.ts +146 -0
  41. package/src/TestHttpClient.test.ts +54 -0
  42. package/src/TestHttpClient.ts +100 -0
  43. package/src/bun/BunBundle.test.ts +277 -0
  44. package/src/bun/BunBundle.ts +309 -0
  45. package/src/bun/BunBundle_imports.test.ts +50 -0
  46. package/src/bun/BunFullstackServer.ts +45 -0
  47. package/src/bun/BunFullstackServer_httpServer.ts +541 -0
  48. package/src/bun/BunImportTrackerPlugin.test.ts +77 -0
  49. package/src/bun/BunImportTrackerPlugin.ts +97 -0
  50. package/src/bun/BunTailwindPlugin.test.ts +335 -0
  51. package/src/bun/BunTailwindPlugin.ts +322 -0
  52. package/src/bun/BunVirtualFilesPlugin.ts +59 -0
  53. package/src/bun/index.ts +4 -0
  54. package/src/client/Overlay.ts +34 -0
  55. package/src/client/ScrollState.ts +120 -0
  56. package/src/client/index.ts +101 -0
  57. package/src/index.ts +24 -0
  58. package/src/jsx-datastar.d.ts +63 -0
  59. package/src/jsx-runtime.ts +23 -0
  60. package/src/jsx.d.ts +4402 -0
  61. package/src/testing.ts +55 -0
  62. package/src/x/cloudflare/CloudflareTunnel.ts +110 -0
  63. package/src/x/cloudflare/index.ts +1 -0
  64. package/src/x/datastar/Datastar.test.ts +267 -0
  65. package/src/x/datastar/Datastar.ts +68 -0
  66. package/src/x/datastar/index.ts +4 -0
  67. package/src/x/datastar/jsx-datastar.d.ts +63 -0
package/src/Router.ts ADDED
@@ -0,0 +1,80 @@
1
+ import * as HttpApp from "@effect/platform/HttpApp"
2
+ import * as HttpRouter from "@effect/platform/HttpRouter"
3
+ import * as Context from "effect/Context"
4
+ import * as Effect from "effect/Effect"
5
+ import * as Function from "effect/Function"
6
+ import * as Layer from "effect/Layer"
7
+ import * as FileHttpRouter from "./FileHttpRouter.ts"
8
+ import * as FileRouter from "./FileRouter.ts"
9
+ import * as Route from "./Route"
10
+
11
+ export const ServerMethods = [
12
+ "GET",
13
+ "POST",
14
+ "PUT",
15
+ "PATCH",
16
+ "DELETE",
17
+ "OPTIONS",
18
+ "HEAD",
19
+ ] as const
20
+
21
+ export type ServerMethod = (typeof ServerMethods)[number]
22
+
23
+ export type ServerModule = {
24
+ default: Route.Route | Route.RouteSet.Default
25
+ }
26
+
27
+ export type ServerRoute = {
28
+ path: `/${string}`
29
+ segments: readonly FileRouter.Segment[]
30
+ load: () => Promise<ServerModule>
31
+ }
32
+
33
+ export type RouteManifest = {
34
+ modules: readonly FileRouter.RouteModule[]
35
+ }
36
+
37
+ export type RouterContext =
38
+ & RouteManifest
39
+ & {
40
+ httpRouter: HttpRouter.HttpRouter
41
+ }
42
+
43
+ export class Router extends Context.Tag("effect-start/Router")<
44
+ Router,
45
+ RouterContext
46
+ >() {}
47
+
48
+ export function layer(
49
+ manifest: RouteManifest,
50
+ ): Layer.Layer<Router, never, never> {
51
+ return Layer.effect(
52
+ Router,
53
+ Effect.gen(function*() {
54
+ const serverRoutes = manifest.modules.map((mod) => ({
55
+ path: mod.path,
56
+ load: mod.load,
57
+ }))
58
+ const httpRouter = yield* FileHttpRouter.make(serverRoutes)
59
+ return {
60
+ ...manifest,
61
+ httpRouter,
62
+ }
63
+ }),
64
+ )
65
+ }
66
+
67
+ export function layerPromise(
68
+ load: () => Promise<RouteManifest>,
69
+ ): Layer.Layer<Router, never, never> {
70
+ return Layer.unwrapEffect(
71
+ Effect.gen(function*() {
72
+ const importedModule = yield* Function.pipe(
73
+ Effect.promise(() => load()),
74
+ Effect.orDie,
75
+ )
76
+
77
+ return layer(importedModule)
78
+ }),
79
+ )
80
+ }
@@ -0,0 +1,55 @@
1
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
2
+ import * as Duration from "effect/Duration"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Function from "effect/Function"
5
+ import * as Schedule from "effect/Schedule"
6
+ import * as Stream from "effect/Stream"
7
+ import * as StreamExtra from "./StreamExtra.ts"
8
+
9
+ const DefaultHeartbeatInterval = Duration.seconds(5)
10
+
11
+ export const make = <T = any>(stream: Stream.Stream<T, any>, options?: {
12
+ heartbeatInterval?: Duration.DurationInput
13
+ }) =>
14
+ Effect.gen(function*() {
15
+ const heartbeat = Stream.repeat(
16
+ Stream.succeed(null),
17
+ Schedule.spaced(options?.heartbeatInterval ?? DefaultHeartbeatInterval),
18
+ )
19
+
20
+ const encoder = new TextEncoder()
21
+
22
+ const events = Function.pipe(
23
+ Stream.merge(
24
+ heartbeat.pipe(
25
+ Stream.map(() => ":\n\n"),
26
+ ),
27
+ stream.pipe(
28
+ Stream.map(event => `data: ${JSON.stringify(event)}\n\n`),
29
+ ),
30
+ ),
31
+ // without Stream.tap, only two events are sent
32
+ // Effect.fork(Stream.runDrain) doesn't seem to work.
33
+ // Asked for help here: [2025-04-09]
34
+ // https://discord.com/channels/795981131316985866/1359523331400929341
35
+ Stream.tap(v => Effect.gen(function*() {})),
36
+ Stream.map(str => encoder.encode(str)),
37
+ )
38
+
39
+ const toStream = StreamExtra.toReadableStreamRuntimePatched(
40
+ yield* Effect.runtime(),
41
+ )
42
+
43
+ // see toStream to understand why we're not using
44
+ // HttpServerResponse.stream here.
45
+ return HttpServerResponse.raw(
46
+ toStream(events),
47
+ {
48
+ headers: {
49
+ "Content-Type": "text/event-stream",
50
+ "Cache-Control": "no-cache",
51
+ "Connection": "keep-alive",
52
+ },
53
+ },
54
+ )
55
+ })
package/src/Start.ts ADDED
@@ -0,0 +1,133 @@
1
+ import * as BunContext from "@effect/platform-bun/BunContext"
2
+ import * as BunHttpServer from "@effect/platform-bun/BunHttpServer"
3
+ import * as BunRuntime from "@effect/platform-bun/BunRuntime"
4
+ import * as FetchHttpClient from "@effect/platform/FetchHttpClient"
5
+ import * as HttpClient from "@effect/platform/HttpClient"
6
+ import * as HttpRouter from "@effect/platform/HttpRouter"
7
+ import * as HttpServer from "@effect/platform/HttpServer"
8
+ import * as Effect from "effect/Effect"
9
+ import * as Function from "effect/Function"
10
+ import * as Layer from "effect/Layer"
11
+ import * as BunBundle from "./bun/BunBundle.ts"
12
+ import * as Bundle from "./Bundle.ts"
13
+ import * as BundleHttp from "./BundleHttp.ts"
14
+ import * as FileRouter from "./FileRouter.ts"
15
+ import * as HttpAppExtra from "./HttpAppExtra.ts"
16
+ import * as Router from "./Router.ts"
17
+ import * as StartApp from "./StartApp.ts"
18
+
19
+ // TODO: we probably want to remove this API to avoid
20
+ // multiple entrypoints for routers and bundles.
21
+ // We could handle endpoints routing in {@link layer}
22
+ // or {@link serve}.
23
+ // Serve probably makes more sense because it's an entrypoint
24
+ // for serving an HTTP server
25
+ export function router(options: {
26
+ load: () => Promise<Router.RouteManifest>
27
+ path: string
28
+ }) {
29
+ return Layer.provideMerge(
30
+ // add it to BundleHttp
31
+ Layer.effectDiscard(
32
+ Effect.gen(function*() {
33
+ const httpRouter = yield* HttpRouter.Default
34
+ const startRouter = yield* Router.Router
35
+
36
+ yield* httpRouter.concat(startRouter.httpRouter)
37
+ }),
38
+ ),
39
+ Layer.merge(
40
+ Router.layerPromise(options.load),
41
+ FileRouter.layer(options),
42
+ ),
43
+ )
44
+ }
45
+
46
+ export function bundleClient(config: BunBundle.BuildOptions | string) {
47
+ const clientLayer = Layer.effect(
48
+ Bundle.ClientBundle,
49
+ Function.pipe(
50
+ BunBundle.buildClient(config),
51
+ Bundle.handleBundleErrorSilently,
52
+ ),
53
+ )
54
+ const assetsLayer = Layer.effectDiscard(Effect.gen(function*() {
55
+ const router = yield* HttpRouter.Default
56
+ const app = BundleHttp.toHttpApp(Bundle.ClientBundle)
57
+
58
+ yield* router.mountApp(
59
+ "/_bundle",
60
+ // we need to use as any here because HttpRouter.Default
61
+ // only accepts default services.
62
+ app as any,
63
+ )
64
+ }))
65
+
66
+ return Layer.mergeAll(
67
+ clientLayer,
68
+ assetsLayer,
69
+ )
70
+ }
71
+
72
+ export function layer<
73
+ Layers extends [
74
+ Layer.Layer<never, any, any>,
75
+ ...Array<Layer.Layer<never, any, any>>,
76
+ ],
77
+ >(...layers: Layers): Layer.Layer<
78
+ { [k in keyof Layers]: Layer.Layer.Success<Layers[k]> }[number],
79
+ { [k in keyof Layers]: Layer.Layer.Error<Layers[k]> }[number],
80
+ { [k in keyof Layers]: Layer.Layer.Context<Layers[k]> }[number]
81
+ > {
82
+ return Layer.mergeAll(...layers)
83
+ }
84
+
85
+ export function serve<ROut, E>(
86
+ load: () => Promise<{
87
+ default: Layer.Layer<
88
+ ROut,
89
+ E,
90
+ | HttpServer.HttpServer
91
+ | HttpRouter.Default
92
+ | HttpClient.HttpClient
93
+ | BunContext.BunContext
94
+ >
95
+ }>,
96
+ ) {
97
+ const appLayer = Function.pipe(
98
+ Effect.tryPromise(load),
99
+ Effect.map(v => v.default),
100
+ Effect.orDie,
101
+ Layer.unwrapEffect,
102
+ )
103
+
104
+ return Function.pipe(
105
+ Layer.unwrapEffect(Effect.gen(function*() {
106
+ const middlewareService = yield* StartApp.StartApp
107
+ const middleware = yield* middlewareService.middleware
108
+
109
+ const finalMiddleware = Function.flow(
110
+ HttpAppExtra.handleErrors,
111
+ middleware,
112
+ )
113
+
114
+ return Function.pipe(
115
+ HttpRouter
116
+ .Default
117
+ .serve(finalMiddleware),
118
+ HttpServer.withLogAddress,
119
+ )
120
+ })),
121
+ Layer.provide(appLayer),
122
+ Layer.provide([
123
+ FetchHttpClient.layer,
124
+ HttpRouter.Default.Live,
125
+ BunHttpServer.layer({
126
+ port: 3000,
127
+ }),
128
+ StartApp.layer(),
129
+ ]),
130
+ Layer.launch,
131
+ BunRuntime.runMain,
132
+ )
133
+ }
@@ -0,0 +1,43 @@
1
+ import * as HttpApp from "@effect/platform/HttpApp"
2
+ import * as Context from "effect/Context"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Function from "effect/Function"
5
+ import * as Layer from "effect/Layer"
6
+ import * as Ref from "effect/Ref"
7
+
8
+ type NewType = HttpApp.Default<never, never>
9
+
10
+ type StartMiddleware = <E, R>(
11
+ self: HttpApp.Default<E, R>,
12
+ ) => NewType
13
+
14
+ export class StartApp extends Context.Tag("effect-start/StartApp")<
15
+ StartApp,
16
+ {
17
+ readonly env: "development" | "production" | string
18
+ readonly relativeUrlRoot?: string
19
+ readonly addMiddleware: (
20
+ middleware: StartMiddleware,
21
+ ) => Effect.Effect<void>
22
+ readonly middleware: Ref.Ref<StartMiddleware>
23
+ }
24
+ >() {
25
+ }
26
+
27
+ export function layer(options?: {
28
+ env?: string
29
+ }) {
30
+ return Layer.sync(StartApp, () => {
31
+ const env = options?.env ?? process.env.NODE_ENV ?? "development"
32
+ const middleware = Ref.unsafeMake(
33
+ Function.identity as StartMiddleware,
34
+ )
35
+
36
+ return StartApp.of({
37
+ env,
38
+ middleware,
39
+ addMiddleware: (f) =>
40
+ Ref.update(middleware, (prev) => (app) => f(prev(app))),
41
+ })
42
+ })
43
+ }
@@ -0,0 +1,42 @@
1
+ import * as HttpApp from "@effect/platform/HttpApp"
2
+ import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
3
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
4
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
5
+ import * as Effect from "effect/Effect"
6
+ import {
7
+ Bundle,
8
+ BundleHttp,
9
+ } from "."
10
+
11
+ type SsrRenderer = (req: Request) => PromiseLike<Response>
12
+
13
+ /**
14
+ * Attempts to render SSR page. If the renderer returns 404,
15
+ * we fall back to app.
16
+ */
17
+ export function ssr(renderer: SsrRenderer) {
18
+ return Effect.gen(function*() {
19
+ const request = yield* HttpServerRequest.HttpServerRequest
20
+ const webRequest = request.source as Request
21
+ const ssrRes = yield* Effect.tryPromise(() => renderer(webRequest))
22
+
23
+ return HttpServerResponse.raw(ssrRes.body, {
24
+ status: ssrRes.status,
25
+ headers: ssrRes.headers,
26
+ })
27
+ })
28
+ }
29
+
30
+ export function withBundleAssets(opts?: {
31
+ path?: string
32
+ }) {
33
+ return HttpMiddleware.make(app =>
34
+ Effect.gen(function*() {
35
+ const request = yield* HttpServerRequest.HttpServerRequest
36
+ const bundleResponse = yield* BundleHttp.httpApp()
37
+
38
+ // Fallback to original app
39
+ return yield* app
40
+ })
41
+ )
42
+ }
@@ -0,0 +1,146 @@
1
+ import * as Cause from "effect/Cause"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Fiber from "effect/Fiber"
4
+ import { dual } from "effect/Function"
5
+ import * as Predicate from "effect/Predicate"
6
+ import * as Runtime from "effect/Runtime"
7
+ import * as Stream from "effect/Stream"
8
+ import {
9
+ runForEachChunk,
10
+ StreamTypeId,
11
+ } from "effect/Stream"
12
+
13
+ /**
14
+ * Patched version of original Stream.toReadableStreamRuntime (v3.14.4) to
15
+ * fix an issue in Bun when native stream controller stops working when request
16
+ * is terminated by the client:
17
+ *
18
+ * TypeError: Value of "this" must be of type ReadableStreamDefaultController
19
+ *
20
+ * See related issues:
21
+ * https://github.com/Effect-TS/effect/issues/4538
22
+ * https://github.com/oven-sh/bun/issues/17837
23
+ */
24
+ export const toReadableStreamRuntimePatched = dual<
25
+ <A, XR>(
26
+ runtime: Runtime.Runtime<XR>,
27
+ options?: { readonly strategy?: QueuingStrategy<A> | undefined },
28
+ ) => <E, R extends XR>(self: Stream.Stream<A, E, R>) => ReadableStream<A>,
29
+ <A, E, XR, R extends XR>(
30
+ self: Stream.Stream<A, E, R>,
31
+ runtime: Runtime.Runtime<XR>,
32
+ options?: { readonly strategy?: QueuingStrategy<A> | undefined },
33
+ ) => ReadableStream<A>
34
+ >(
35
+ (args) =>
36
+ Predicate.hasProperty(args[0], StreamTypeId) || Effect.isEffect(args[0]),
37
+ <A, E, XR, R extends XR>(
38
+ self: Stream.Stream<A, E, R>,
39
+ runtime: Runtime.Runtime<XR>,
40
+ options?: { readonly strategy?: QueuingStrategy<A> | undefined },
41
+ ): ReadableStream<A> => {
42
+ const runFork = Runtime.runFork(runtime)
43
+ let currentResolve: (() => void) | undefined = undefined
44
+ let fiber: Fiber.RuntimeFiber<void, E> | undefined = undefined
45
+ const latch = Effect.unsafeMakeLatch(false)
46
+
47
+ return new ReadableStream<A>({
48
+ start(controller) {
49
+ fiber = runFork(
50
+ runForEachChunk(self, (chunk) =>
51
+ latch.whenOpen(Effect.sync(() => {
52
+ latch.unsafeClose()
53
+ try {
54
+ for (const item of chunk) {
55
+ controller.enqueue(item)
56
+ }
57
+ } catch (e) {
58
+ if (
59
+ (e as Error).message
60
+ === `Value of "this" must be of type ReadableStreamDefaultController`
61
+ ) {
62
+ // Do nothing when this happens in Bun.
63
+ } else {
64
+ throw e
65
+ }
66
+ }
67
+ currentResolve!()
68
+ currentResolve = undefined
69
+ }))),
70
+ )
71
+ // --- CHANGES HERE ---
72
+ // In original code, we had fiber.addObserver here that called
73
+ // error() or close() on controller. This patched version removes it.
74
+ },
75
+ pull() {
76
+ return new Promise<void>((resolve) => {
77
+ currentResolve = resolve
78
+ Effect.runSync(latch.open)
79
+ })
80
+ },
81
+ cancel() {
82
+ if (!fiber) return
83
+ return Effect.runPromise(Effect.asVoid(Fiber.interrupt(fiber)))
84
+ },
85
+ }, options?.strategy)
86
+ },
87
+ )
88
+
89
+ export const toReadableStreamRuntimePatched2 = dual<
90
+ <A, XR>(
91
+ runtime: Runtime.Runtime<XR>,
92
+ options?: { readonly strategy?: QueuingStrategy<A> | undefined },
93
+ ) => <E, R extends XR>(self: Stream.Stream<A, E, R>) => ReadableStream<A>,
94
+ <A, E, XR, R extends XR>(
95
+ self: Stream.Stream<A, E, R>,
96
+ runtime: Runtime.Runtime<XR>,
97
+ options?: { readonly strategy?: QueuingStrategy<A> | undefined },
98
+ ) => ReadableStream<A>
99
+ >(
100
+ (args) =>
101
+ Predicate.hasProperty(args[0], StreamTypeId) || Effect.isEffect(args[0]),
102
+ <A, E, XR, R extends XR>(
103
+ self: Stream.Stream<A, E, R>,
104
+ runtime: Runtime.Runtime<XR>,
105
+ options?: { readonly strategy?: QueuingStrategy<A> | undefined },
106
+ ): ReadableStream<A> => {
107
+ const runSync = Runtime.runSync(runtime)
108
+ const runFork = Runtime.runFork(runtime)
109
+ let currentResolve: (() => void) | undefined = undefined
110
+ let fiber: Fiber.RuntimeFiber<void, E> | undefined = undefined
111
+ const latch = Effect.unsafeMakeLatch(false)
112
+
113
+ return new ReadableStream<A>({
114
+ start(controller) {
115
+ fiber = runFork(
116
+ runForEachChunk(self, (chunk) =>
117
+ latch.whenOpen(Effect.sync(() => {
118
+ latch.unsafeClose()
119
+ for (const item of chunk) {
120
+ controller.enqueue(item)
121
+ }
122
+ currentResolve!()
123
+ currentResolve = undefined
124
+ }))),
125
+ )
126
+ fiber.addObserver((exit) => {
127
+ if (exit._tag === "Failure") {
128
+ controller.error(Cause.squash(exit.cause))
129
+ } else {
130
+ controller.close()
131
+ }
132
+ })
133
+ },
134
+ pull() {
135
+ return new Promise<void>((resolve) => {
136
+ currentResolve = resolve
137
+ Effect.runSync(latch.open)
138
+ })
139
+ },
140
+ cancel() {
141
+ if (!fiber) return
142
+ return Effect.runPromise(Effect.asVoid(Fiber.interrupt(fiber)))
143
+ },
144
+ }, options?.strategy)
145
+ },
146
+ )
@@ -0,0 +1,54 @@
1
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
2
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
3
+ import * as t from "bun:test"
4
+ import * as Effect from "effect/Effect"
5
+ import * as TestHttpClient from "./TestHttpClient.ts"
6
+ import { effectFn } from "./testing.ts"
7
+
8
+ const App = Effect.gen(function*() {
9
+ const req = yield* HttpServerRequest.HttpServerRequest
10
+
11
+ if (req.url == "/") {
12
+ return HttpServerResponse.text("Hello, World!")
13
+ }
14
+
15
+ return HttpServerResponse.text("Not Found", {
16
+ status: 404,
17
+ })
18
+ })
19
+
20
+ const AppClient = TestHttpClient.make(App)
21
+
22
+ const effect = effectFn()
23
+
24
+ t.it("ok", () =>
25
+ effect(function*() {
26
+ const res = yield* AppClient.get("/")
27
+
28
+ t
29
+ .expect(
30
+ res.status,
31
+ )
32
+ .toEqual(200)
33
+ t
34
+ .expect(
35
+ yield* res.text,
36
+ )
37
+ .toEqual("Hello, World!")
38
+ }))
39
+
40
+ t.it("not found", () =>
41
+ effect(function*() {
42
+ const res = yield* AppClient.get("/nope")
43
+
44
+ t
45
+ .expect(
46
+ res.status,
47
+ )
48
+ .toEqual(404)
49
+ t
50
+ .expect(
51
+ yield* res.text,
52
+ )
53
+ .toEqual("Not Found")
54
+ }))
@@ -0,0 +1,100 @@
1
+ // @ts-nocheck
2
+ import * as HttpApp from "@effect/platform/HttpApp"
3
+ import * as HttpClient from "@effect/platform/HttpClient"
4
+ import * as HttpClientError from "@effect/platform/HttpClientError"
5
+ import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
6
+ import * as HttpClientResponse from "@effect/platform/HttpClientResponse"
7
+ import { RouteNotFound } from "@effect/platform/HttpServerError"
8
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
9
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
10
+ import * as Effect from "effect/Effect"
11
+ import * as Function from "effect/Function"
12
+ import * as Scope from "effect/Scope"
13
+ import * as Stream from "effect/Stream"
14
+
15
+ const WebHeaders = globalThis.Headers
16
+
17
+ export const make = <E = any, R = any>(
18
+ httpApp: HttpApp.Default<E, R>,
19
+ opts?: {
20
+ baseUrl?: string | null
21
+ handleRouteNotFound?: (
22
+ e: RouteNotFound,
23
+ ) => Effect.Effect<HttpClientResponse.HttpClientResponse> | null
24
+ },
25
+ ): HttpClient.HttpClient.With<
26
+ HttpClientError.HttpClientError | E,
27
+ Exclude<
28
+ Scope.Scope | R,
29
+ HttpServerRequest.HttpServerRequest
30
+ >
31
+ > =>
32
+ HttpClient
33
+ .make(
34
+ (request, url, signal) => {
35
+ const send = (
36
+ body: BodyInit | undefined,
37
+ ) => {
38
+ const app = httpApp
39
+ const serverRequest = HttpServerRequest.fromWeb(
40
+ new Request(url.toString(), {
41
+ method: request.method,
42
+ headers: new WebHeaders(request.headers),
43
+ body,
44
+ duplex: request.body._tag === "Stream" ? "half" : undefined,
45
+ signal,
46
+ } as any),
47
+ )
48
+
49
+ return Function.pipe(
50
+ app,
51
+ Effect.provideService(
52
+ HttpServerRequest.HttpServerRequest,
53
+ serverRequest,
54
+ ),
55
+ Effect.andThen(HttpServerResponse.toWeb),
56
+ Effect.andThen(res => HttpClientResponse.fromWeb(request, res)),
57
+ opts?.handleRouteNotFound === null
58
+ ? Function.identity
59
+ : Effect.catchTag("RouteNotFound", e =>
60
+ Effect
61
+ .succeed(HttpClientResponse.fromWeb(
62
+ e.request,
63
+ new Response("Failed with RouteNotFound", {
64
+ status: 404,
65
+ }),
66
+ ))),
67
+ )
68
+ }
69
+
70
+ switch (
71
+ request
72
+ .body
73
+ ._tag
74
+ ) {
75
+ case "Raw":
76
+ case "Uint8Array":
77
+ return send(
78
+ request
79
+ .body
80
+ .body as any,
81
+ )
82
+ case "FormData":
83
+ return send(request.body.formData)
84
+ case "Stream":
85
+ return Effect.flatMap(
86
+ Stream.toReadableStreamEffect(request.body.stream),
87
+ send,
88
+ )
89
+ }
90
+
91
+ return send(undefined)
92
+ },
93
+ )
94
+ .pipe(
95
+ opts?.baseUrl === null
96
+ ? Function.identity
97
+ : HttpClient.mapRequest(
98
+ HttpClientRequest.prependUrl(opts?.baseUrl ?? "http://localhost"),
99
+ ),
100
+ )