effect-start 0.14.0 → 0.15.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 (81) hide show
  1. package/package.json +8 -9
  2. package/src/Commander.test.ts +507 -245
  3. package/src/ContentNegotiation.test.ts +500 -0
  4. package/src/ContentNegotiation.ts +535 -0
  5. package/src/FileRouter.ts +16 -12
  6. package/src/{FileRouterCodegen.test.ts → FileRouterCodegen.todo.ts} +384 -219
  7. package/src/FileRouterCodegen.ts +6 -6
  8. package/src/FileRouterPattern.test.ts +93 -62
  9. package/src/FileRouter_files.test.ts +5 -5
  10. package/src/FileRouter_path.test.ts +121 -69
  11. package/src/FileRouter_tree.test.ts +62 -56
  12. package/src/FileSystemExtra.test.ts +46 -30
  13. package/src/Http.test.ts +24 -0
  14. package/src/Http.ts +25 -0
  15. package/src/HttpAppExtra.test.ts +39 -20
  16. package/src/HttpAppExtra.ts +0 -1
  17. package/src/HttpUtils.test.ts +35 -18
  18. package/src/HttpUtils.ts +2 -0
  19. package/src/PathPattern.test.ts +648 -0
  20. package/src/PathPattern.ts +483 -0
  21. package/src/Route.ts +258 -1073
  22. package/src/RouteBody.test.ts +182 -0
  23. package/src/RouteBody.ts +106 -0
  24. package/src/RouteHook.test.ts +40 -0
  25. package/src/RouteHook.ts +105 -0
  26. package/src/RouteHttp.test.ts +443 -0
  27. package/src/RouteHttp.ts +219 -0
  28. package/src/RouteMount.test.ts +468 -0
  29. package/src/RouteMount.ts +313 -0
  30. package/src/RouteSchema.test.ts +81 -0
  31. package/src/RouteSchema.ts +44 -0
  32. package/src/RouteTree.test.ts +346 -0
  33. package/src/RouteTree.ts +165 -0
  34. package/src/RouteTrie.test.ts +322 -0
  35. package/src/RouteTrie.ts +224 -0
  36. package/src/RouterPattern.test.ts +569 -548
  37. package/src/RouterPattern.ts +7 -7
  38. package/src/Start.ts +3 -3
  39. package/src/TuplePathPattern.ts +64 -0
  40. package/src/Values.ts +16 -0
  41. package/src/bun/BunBundle.test.ts +36 -42
  42. package/src/bun/BunBundle.ts +2 -2
  43. package/src/bun/BunBundle_imports.test.ts +4 -6
  44. package/src/bun/BunHttpServer.test.ts +183 -6
  45. package/src/bun/BunHttpServer.ts +56 -32
  46. package/src/bun/BunHttpServer_web.ts +18 -6
  47. package/src/bun/BunImportTrackerPlugin.test.ts +3 -3
  48. package/src/bun/BunRoute.ts +29 -210
  49. package/src/{BundleHttp.test.ts → bundler/BundleHttp.test.ts} +34 -60
  50. package/src/{BundleHttp.ts → bundler/BundleHttp.ts} +1 -2
  51. package/src/client/index.ts +1 -1
  52. package/src/{Effect_HttpRouter.test.ts → effect/HttpRouter.test.ts} +69 -90
  53. package/src/experimental/EncryptedCookies.test.ts +125 -64
  54. package/src/experimental/SseHttpResponse.ts +0 -1
  55. package/src/hyper/Hyper.ts +89 -0
  56. package/src/{HyperHtml.test.ts → hyper/HyperHtml.test.ts} +13 -13
  57. package/src/{HyperHtml.ts → hyper/HyperHtml.ts} +2 -2
  58. package/src/{jsx.d.ts → hyper/jsx.d.ts} +1 -1
  59. package/src/index.ts +2 -4
  60. package/src/middlewares/BasicAuthMiddleware.test.ts +29 -19
  61. package/src/{NodeFileSystem.ts → node/FileSystem.ts} +6 -2
  62. package/src/testing/TestHttpClient.test.ts +26 -26
  63. package/src/testing/TestLogger.test.ts +27 -11
  64. package/src/x/datastar/Datastar.test.ts +47 -48
  65. package/src/x/datastar/Datastar.ts +1 -1
  66. package/src/x/tailwind/TailwindPlugin.test.ts +56 -58
  67. package/src/x/tailwind/plugin.ts +1 -1
  68. package/src/FileHttpRouter.test.ts +0 -239
  69. package/src/FileHttpRouter.ts +0 -194
  70. package/src/Hyper.ts +0 -194
  71. package/src/Route.test.ts +0 -1370
  72. package/src/RouteRender.ts +0 -40
  73. package/src/Router.test.ts +0 -375
  74. package/src/Router.ts +0 -255
  75. package/src/bun/BunRoute.test.ts +0 -480
  76. package/src/bun/BunRoute_bundles.test.ts +0 -219
  77. /package/src/{Bundle.ts → bundler/Bundle.ts} +0 -0
  78. /package/src/{BundleFiles.ts → bundler/BundleFiles.ts} +0 -0
  79. /package/src/{HyperNode.ts → hyper/HyperNode.ts} +0 -0
  80. /package/src/{jsx-runtime.ts → hyper/jsx-runtime.ts} +0 -0
  81. /package/src/{NodeUtils.ts → node/Utils.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import * as FileSystem from "@effect/platform/FileSystem"
2
- import * as t from "bun:test"
2
+ import * as test from "bun:test"
3
3
  import { MemoryFileSystem } from "effect-memfs"
4
4
  import * as Chunk from "effect/Chunk"
5
5
  import * as Effect from "effect/Effect"
@@ -8,8 +8,8 @@ import * as Function from "effect/Function"
8
8
  import * as Stream from "effect/Stream"
9
9
  import * as FileSystemExtra from "./FileSystemExtra.ts"
10
10
 
11
- t.describe(`${FileSystemExtra.watchSource.name}`, () => {
12
- t.it("emits events for file creation", () =>
11
+ test.describe(`${FileSystemExtra.watchSource.name}`, () => {
12
+ test.it("emits events for file creation", () =>
13
13
  Effect
14
14
  .gen(function*() {
15
15
  const fs = yield* FileSystem.FileSystem
@@ -28,11 +28,19 @@ t.describe(`${FileSystemExtra.watchSource.name}`, () => {
28
28
 
29
29
  const events = yield* Fiber.join(fiber)
30
30
 
31
- t.expect(Chunk.size(events)).toBeGreaterThan(0)
31
+ test
32
+ .expect(Chunk.size(events))
33
+ .toBeGreaterThan(0)
32
34
  const first = Chunk.unsafeGet(events, 0)
33
- t.expect(first.path).toContain("test.ts")
34
- t.expect(["rename", "change"]).toContain(first.eventType)
35
- t.expect(first.filename).toBe("test.ts")
35
+ test
36
+ .expect(first.path)
37
+ .toContain("test.ts")
38
+ test
39
+ .expect(["rename", "change"])
40
+ .toContain(first.eventType)
41
+ test
42
+ .expect(first.filename)
43
+ .toBe("test.ts")
36
44
  })
37
45
  .pipe(
38
46
  Effect.scoped,
@@ -42,7 +50,7 @@ t.describe(`${FileSystemExtra.watchSource.name}`, () => {
42
50
  Effect.runPromise,
43
51
  ))
44
52
 
45
- t.it(
53
+ test.it(
46
54
  "emits change event for file modification",
47
55
  () =>
48
56
  Effect
@@ -63,8 +71,12 @@ t.describe(`${FileSystemExtra.watchSource.name}`, () => {
63
71
 
64
72
  const events = yield* Fiber.join(fiber)
65
73
 
66
- t.expect(Chunk.size(events)).toBeGreaterThan(0)
67
- t.expect(Chunk.unsafeGet(events, 0).eventType).toBe("change")
74
+ test
75
+ .expect(Chunk.size(events))
76
+ .toBeGreaterThan(0)
77
+ test
78
+ .expect(Chunk.unsafeGet(events, 0).eventType)
79
+ .toBe("change")
68
80
  })
69
81
  .pipe(
70
82
  Effect.scoped,
@@ -75,7 +87,7 @@ t.describe(`${FileSystemExtra.watchSource.name}`, () => {
75
87
  ),
76
88
  )
77
89
 
78
- t.it("applies custom filter", () =>
90
+ test.it("applies custom filter", () =>
79
91
  Effect
80
92
  .gen(function*() {
81
93
  const fs = yield* FileSystem.FileSystem
@@ -98,8 +110,12 @@ t.describe(`${FileSystemExtra.watchSource.name}`, () => {
98
110
 
99
111
  const events = yield* Fiber.join(fiber)
100
112
 
101
- t.expect(Chunk.size(events)).toBe(1)
102
- t.expect(Chunk.unsafeGet(events, 0).path).toContain("included.ts")
113
+ test
114
+ .expect(Chunk.size(events))
115
+ .toBe(1)
116
+ test
117
+ .expect(Chunk.unsafeGet(events, 0).path)
118
+ .toContain("included.ts")
103
119
  })
104
120
  .pipe(
105
121
  Effect.scoped,
@@ -110,9 +126,9 @@ t.describe(`${FileSystemExtra.watchSource.name}`, () => {
110
126
  ))
111
127
  })
112
128
 
113
- t.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
114
- t.it("matches source file extensions", () => {
115
- t
129
+ test.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
130
+ test.it("matches source file extensions", () => {
131
+ test
116
132
  .expect(
117
133
  FileSystemExtra.filterSourceFiles({
118
134
  eventType: "change",
@@ -121,7 +137,7 @@ t.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
121
137
  }),
122
138
  )
123
139
  .toBe(true)
124
- t
140
+ test
125
141
  .expect(
126
142
  FileSystemExtra.filterSourceFiles({
127
143
  eventType: "change",
@@ -130,7 +146,7 @@ t.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
130
146
  }),
131
147
  )
132
148
  .toBe(true)
133
- t
149
+ test
134
150
  .expect(
135
151
  FileSystemExtra.filterSourceFiles({
136
152
  eventType: "change",
@@ -139,7 +155,7 @@ t.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
139
155
  }),
140
156
  )
141
157
  .toBe(true)
142
- t
158
+ test
143
159
  .expect(
144
160
  FileSystemExtra.filterSourceFiles({
145
161
  eventType: "change",
@@ -148,7 +164,7 @@ t.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
148
164
  }),
149
165
  )
150
166
  .toBe(true)
151
- t
167
+ test
152
168
  .expect(
153
169
  FileSystemExtra.filterSourceFiles({
154
170
  eventType: "change",
@@ -157,7 +173,7 @@ t.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
157
173
  }),
158
174
  )
159
175
  .toBe(true)
160
- t
176
+ test
161
177
  .expect(
162
178
  FileSystemExtra.filterSourceFiles({
163
179
  eventType: "change",
@@ -166,7 +182,7 @@ t.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
166
182
  }),
167
183
  )
168
184
  .toBe(true)
169
- t
185
+ test
170
186
  .expect(
171
187
  FileSystemExtra.filterSourceFiles({
172
188
  eventType: "change",
@@ -177,8 +193,8 @@ t.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
177
193
  .toBe(true)
178
194
  })
179
195
 
180
- t.it("rejects non-source files", () => {
181
- t
196
+ test.it("rejects non-source files", () => {
197
+ test
182
198
  .expect(
183
199
  FileSystemExtra.filterSourceFiles({
184
200
  eventType: "change",
@@ -187,7 +203,7 @@ t.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
187
203
  }),
188
204
  )
189
205
  .toBe(false)
190
- t
206
+ test
191
207
  .expect(
192
208
  FileSystemExtra.filterSourceFiles({
193
209
  eventType: "change",
@@ -199,9 +215,9 @@ t.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
199
215
  })
200
216
  })
201
217
 
202
- t.describe(`${FileSystemExtra.filterDirectory.name}`, () => {
203
- t.it("matches directories", () => {
204
- t
218
+ test.describe(`${FileSystemExtra.filterDirectory.name}`, () => {
219
+ test.it("matches directories", () => {
220
+ test
205
221
  .expect(
206
222
  FileSystemExtra.filterDirectory({
207
223
  eventType: "change",
@@ -212,8 +228,8 @@ t.describe(`${FileSystemExtra.filterDirectory.name}`, () => {
212
228
  .toBe(true)
213
229
  })
214
230
 
215
- t.it("rejects files", () => {
216
- t
231
+ test.it("rejects files", () => {
232
+ test
217
233
  .expect(
218
234
  FileSystemExtra.filterDirectory({
219
235
  eventType: "change",
@@ -0,0 +1,24 @@
1
+ import * as test from "bun:test"
2
+ import * as Http from "./Http.ts"
3
+
4
+ test.it("cloneRequest copies request and adds props", () => {
5
+ const request = new Request("http://localhost/test", {
6
+ method: "POST",
7
+ headers: { "Content-Type": "application/json" },
8
+ })
9
+
10
+ const cloned = Http.cloneRequest(request, { params: { id: "123" } })
11
+
12
+ test
13
+ .expect(cloned.url)
14
+ .toBe("http://localhost/test")
15
+ test
16
+ .expect(cloned.method)
17
+ .toBe("POST")
18
+ test
19
+ .expect(cloned.headers.get("Content-Type"))
20
+ .toBe("application/json")
21
+ test
22
+ .expect(cloned.params)
23
+ .toEqual({ id: "123" })
24
+ })
package/src/Http.ts ADDED
@@ -0,0 +1,25 @@
1
+ export type Method =
2
+ | "GET"
3
+ | "POST"
4
+ | "PUT"
5
+ | "DELETE"
6
+ | "PATCH"
7
+ | "HEAD"
8
+ | "OPTIONS"
9
+
10
+ export type WebHandler = (request: Request) => Response | Promise<Response>
11
+
12
+ export function cloneRequest<T extends object>(
13
+ request: Request,
14
+ props: T,
15
+ ): Request & T {
16
+ const cloned = new Request(request.url, {
17
+ method: request.method,
18
+ headers: request.headers,
19
+ body: request.body,
20
+ })
21
+ for (const [key, value] of Object.entries(props)) {
22
+ ;(cloned as any)[key] = value
23
+ }
24
+ return cloned as Request & T
25
+ }
@@ -1,11 +1,12 @@
1
1
  import { HttpServerRequest } from "@effect/platform"
2
2
  import { RouteNotFound } from "@effect/platform/HttpServerError"
3
- import * as t from "bun:test"
3
+ import * as test from "bun:test"
4
4
  import {
5
- Effect,
6
5
  Layer,
6
+ Logger,
7
7
  } from "effect"
8
8
  import * as Cause from "effect/Cause"
9
+ import * as LogLevel from "effect/LogLevel"
9
10
  import * as HttpAppExtra from "./HttpAppExtra.ts"
10
11
  import { effectFn } from "./testing"
11
12
 
@@ -18,60 +19,76 @@ const mockRequest = HttpServerRequest.HttpServerRequest.of({
18
19
  },
19
20
  } as any)
20
21
 
21
- const mockRequestLayer = Layer.succeed(
22
- HttpServerRequest.HttpServerRequest,
23
- mockRequest,
22
+ const mockRequestLayer = Layer.mergeAll(
23
+ Layer.succeed(HttpServerRequest.HttpServerRequest, mockRequest),
24
+ Logger.minimumLogLevel(LogLevel.None),
24
25
  )
25
26
 
26
27
  const effect = effectFn(mockRequestLayer)
27
28
 
28
- t.describe("renderError", () => {
29
+ test.describe("renderError", () => {
29
30
  const routeNotFoundCause = Cause.fail(
30
31
  new RouteNotFound({ request: {} as any }),
31
32
  )
32
33
 
33
- t.it("returns JSON for Accept: application/json", () =>
34
+ test.it("returns JSON for Accept: application/json", () =>
34
35
  effect(function*() {
35
36
  const response = yield* HttpAppExtra.renderError(
36
37
  routeNotFoundCause,
37
38
  "application/json",
38
39
  )
39
40
 
40
- t.expect(response.status).toEqual(404)
41
- t.expect(response.headers["content-type"]).toContain("application/json")
41
+ test
42
+ .expect(response.status)
43
+ .toEqual(404)
44
+ test
45
+ .expect(response.headers["content-type"])
46
+ .toContain("application/json")
42
47
  }))
43
48
 
44
- t.it("returns HTML for Accept: text/html", () =>
49
+ test.it("returns HTML for Accept: text/html", () =>
45
50
  effect(function*() {
46
51
  const response = yield* HttpAppExtra.renderError(
47
52
  routeNotFoundCause,
48
53
  "text/html",
49
54
  )
50
55
 
51
- t.expect(response.status).toEqual(404)
52
- t.expect(response.headers["content-type"]).toContain("text/html")
56
+ test
57
+ .expect(response.status)
58
+ .toEqual(404)
59
+ test
60
+ .expect(response.headers["content-type"])
61
+ .toContain("text/html")
53
62
  }))
54
63
 
55
- t.it("returns plain text for Accept: text/plain", () =>
64
+ test.it("returns plain text for Accept: text/plain", () =>
56
65
  effect(function*() {
57
66
  const response = yield* HttpAppExtra.renderError(
58
67
  routeNotFoundCause,
59
68
  "text/plain",
60
69
  )
61
70
 
62
- t.expect(response.status).toEqual(404)
63
- t.expect(response.headers["content-type"]).toContain("text/plain")
71
+ test
72
+ .expect(response.status)
73
+ .toEqual(404)
74
+ test
75
+ .expect(response.headers["content-type"])
76
+ .toContain("text/plain")
64
77
  }))
65
78
 
66
- t.it("returns JSON by default (no Accept header)", () =>
79
+ test.it("returns JSON by default (no Accept header)", () =>
67
80
  effect(function*() {
68
81
  const response = yield* HttpAppExtra.renderError(routeNotFoundCause, "")
69
82
 
70
- t.expect(response.status).toEqual(404)
71
- t.expect(response.headers["content-type"]).toContain("application/json")
83
+ test
84
+ .expect(response.status)
85
+ .toEqual(404)
86
+ test
87
+ .expect(response.headers["content-type"])
88
+ .toContain("application/json")
72
89
  }))
73
90
 
74
- t.it("returns 500 for unexpected errors", () =>
91
+ test.it("returns 500 for unexpected errors", () =>
75
92
  effect(function*() {
76
93
  const unexpectedCause = Cause.fail({ message: "Something went wrong" })
77
94
  const response = yield* HttpAppExtra.renderError(
@@ -79,6 +96,8 @@ t.describe("renderError", () => {
79
96
  "application/json",
80
97
  )
81
98
 
82
- t.expect(response.status).toEqual(500)
99
+ test
100
+ .expect(response.status)
101
+ .toEqual(500)
83
102
  }))
84
103
  })
@@ -5,7 +5,6 @@ import {
5
5
  } from "@effect/platform"
6
6
  import * as HttpApp from "@effect/platform/HttpApp"
7
7
  import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
8
-
9
8
  import {
10
9
  RequestError,
11
10
  RouteNotFound,
@@ -1,6 +1,5 @@
1
1
  import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
2
- import * as t from "bun:test"
3
-
2
+ import * as test from "bun:test"
4
3
  import * as HttpUtils from "./HttpUtils.ts"
5
4
 
6
5
  const makeRequest = (url: string, headers: Record<string, string> = {}) =>
@@ -8,61 +7,79 @@ const makeRequest = (url: string, headers: Record<string, string> = {}) =>
8
7
  new Request(`http://test${url}`, { headers }),
9
8
  )
10
9
 
11
- t.describe("makeUrlFromRequest", () => {
12
- t.it("uses Host header for relative URL", () => {
10
+ test.describe("makeUrlFromRequest", () => {
11
+ test.it("uses Host header for relative URL", () => {
13
12
  const request = makeRequest("/api/users", {
14
13
  host: "example.com",
15
14
  })
16
15
  const url = HttpUtils.makeUrlFromRequest(request)
17
16
 
18
- t.expect(url.href).toBe("http://example.com/api/users")
17
+ test
18
+ .expect(url.href)
19
+ .toBe("http://example.com/api/users")
19
20
  })
20
21
 
21
- t.it("uses Origin header when present (takes precedence over Host)", () => {
22
+ test.it("uses Origin header when present (takes precedence over Host)", () => {
22
23
  const request = makeRequest("/api/users", {
23
24
  origin: "https://app.example.com",
24
25
  host: "example.com",
25
26
  })
26
27
  const url = HttpUtils.makeUrlFromRequest(request)
27
28
 
28
- t.expect(url.href).toBe("https://app.example.com/api/users")
29
+ test
30
+ .expect(url.href)
31
+ .toBe("https://app.example.com/api/users")
29
32
  })
30
33
 
31
- t.it("uses X-Forwarded-Proto for protocol behind reverse proxy", () => {
34
+ test.it("uses X-Forwarded-Proto for protocol behind reverse proxy", () => {
32
35
  const request = makeRequest("/api/users", {
33
36
  host: "example.com",
34
37
  "x-forwarded-proto": "https",
35
38
  })
36
39
  const url = HttpUtils.makeUrlFromRequest(request)
37
40
 
38
- t.expect(url.href).toBe("https://example.com/api/users")
41
+ test
42
+ .expect(url.href)
43
+ .toBe("https://example.com/api/users")
39
44
  })
40
45
 
41
- t.it("falls back to http://localhost when no headers", () => {
46
+ test.it("falls back to http://localhost when no headers", () => {
42
47
  const request = makeRequest("/api/users", {})
43
48
  const url = HttpUtils.makeUrlFromRequest(request)
44
49
 
45
- t.expect(url.href).toBe("http://localhost/api/users")
50
+ test
51
+ .expect(url.href)
52
+ .toBe("http://localhost/api/users")
46
53
  })
47
54
 
48
- t.it("handles URL with query parameters", () => {
55
+ test.it("handles URL with query parameters", () => {
49
56
  const request = makeRequest("/search?q=test&page=1", {
50
57
  host: "example.com",
51
58
  })
52
59
  const url = HttpUtils.makeUrlFromRequest(request)
53
60
 
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")
61
+ test
62
+ .expect(url.href)
63
+ .toBe("http://example.com/search?q=test&page=1")
64
+ test
65
+ .expect(url.searchParams.get("q"))
66
+ .toBe("test")
67
+ test
68
+ .expect(url.searchParams.get("page"))
69
+ .toBe("1")
57
70
  })
58
71
 
59
- t.it("handles root path", () => {
72
+ test.it("handles root path", () => {
60
73
  const request = makeRequest("/", {
61
74
  host: "example.com",
62
75
  })
63
76
  const url = HttpUtils.makeUrlFromRequest(request)
64
77
 
65
- t.expect(url.href).toBe("http://example.com/")
66
- t.expect(url.pathname).toBe("/")
78
+ test
79
+ .expect(url.href)
80
+ .toBe("http://example.com/")
81
+ test
82
+ .expect(url.pathname)
83
+ .toBe("/")
67
84
  })
68
85
  })
package/src/HttpUtils.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
2
2
 
3
+ export type FetchHandler = (request: Request) => Promise<Response>
4
+
3
5
  export function makeUrlFromRequest(
4
6
  request: HttpServerRequest.HttpServerRequest,
5
7
  ): URL {