effect-start 0.14.0 → 0.16.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 (87) hide show
  1. package/package.json +8 -9
  2. package/src/Commander.test.ts +507 -245
  3. package/src/ContentNegotiation.test.ts +603 -0
  4. package/src/ContentNegotiation.ts +542 -0
  5. package/src/Entity.test.ts +592 -0
  6. package/src/Entity.ts +362 -0
  7. package/src/FileRouter.ts +16 -12
  8. package/src/{FileRouterCodegen.test.ts → FileRouterCodegen.todo.ts} +384 -219
  9. package/src/FileRouterCodegen.ts +6 -6
  10. package/src/FileRouterPattern.test.ts +93 -62
  11. package/src/FileRouter_files.test.ts +5 -5
  12. package/src/FileRouter_path.test.ts +121 -69
  13. package/src/FileRouter_tree.test.ts +62 -56
  14. package/src/FileSystemExtra.test.ts +46 -30
  15. package/src/Http.test.ts +319 -0
  16. package/src/Http.ts +167 -0
  17. package/src/HttpAppExtra.test.ts +39 -20
  18. package/src/HttpAppExtra.ts +0 -1
  19. package/src/HttpUtils.test.ts +35 -18
  20. package/src/HttpUtils.ts +2 -0
  21. package/src/PathPattern.test.ts +648 -0
  22. package/src/PathPattern.ts +485 -0
  23. package/src/Route.ts +266 -1069
  24. package/src/RouteBody.test.ts +234 -0
  25. package/src/RouteBody.ts +193 -0
  26. package/src/RouteHook.test.ts +40 -0
  27. package/src/RouteHook.ts +106 -0
  28. package/src/RouteHttp.test.ts +2906 -0
  29. package/src/RouteHttp.ts +427 -0
  30. package/src/RouteHttpTracer.ts +92 -0
  31. package/src/RouteMount.test.ts +481 -0
  32. package/src/RouteMount.ts +470 -0
  33. package/src/RouteSchema.test.ts +427 -0
  34. package/src/RouteSchema.ts +423 -0
  35. package/src/RouteTree.test.ts +494 -0
  36. package/src/RouteTree.ts +219 -0
  37. package/src/RouteTrie.test.ts +322 -0
  38. package/src/RouteTrie.ts +224 -0
  39. package/src/RouterPattern.test.ts +569 -548
  40. package/src/RouterPattern.ts +7 -7
  41. package/src/Start.ts +3 -3
  42. package/src/StreamExtra.ts +21 -1
  43. package/src/TuplePathPattern.ts +64 -0
  44. package/src/Values.test.ts +263 -0
  45. package/src/Values.ts +76 -0
  46. package/src/bun/BunBundle.test.ts +36 -42
  47. package/src/bun/BunBundle.ts +2 -2
  48. package/src/bun/BunBundle_imports.test.ts +4 -6
  49. package/src/bun/BunHttpServer.test.ts +183 -6
  50. package/src/bun/BunHttpServer.ts +72 -32
  51. package/src/bun/BunHttpServer_web.ts +18 -6
  52. package/src/bun/BunImportTrackerPlugin.test.ts +3 -3
  53. package/src/bun/BunRoute.test.ts +124 -442
  54. package/src/bun/BunRoute.ts +146 -286
  55. package/src/{BundleHttp.test.ts → bundler/BundleHttp.test.ts} +34 -60
  56. package/src/{BundleHttp.ts → bundler/BundleHttp.ts} +1 -2
  57. package/src/client/index.ts +1 -1
  58. package/src/{Effect_HttpRouter.test.ts → effect/HttpRouter.test.ts} +69 -90
  59. package/src/experimental/EncryptedCookies.test.ts +125 -64
  60. package/src/experimental/SseHttpResponse.ts +0 -1
  61. package/src/hyper/Hyper.ts +89 -0
  62. package/src/{HyperHtml.test.ts → hyper/HyperHtml.test.ts} +13 -13
  63. package/src/{HyperHtml.ts → hyper/HyperHtml.ts} +2 -2
  64. package/src/{jsx.d.ts → hyper/jsx.d.ts} +1 -1
  65. package/src/index.ts +3 -4
  66. package/src/middlewares/BasicAuthMiddleware.test.ts +29 -19
  67. package/src/{NodeFileSystem.ts → node/FileSystem.ts} +6 -2
  68. package/src/testing/TestHttpClient.test.ts +26 -26
  69. package/src/testing/TestLogger.test.ts +27 -14
  70. package/src/testing/TestLogger.ts +15 -9
  71. package/src/x/datastar/Datastar.test.ts +47 -48
  72. package/src/x/datastar/Datastar.ts +1 -1
  73. package/src/x/tailwind/TailwindPlugin.test.ts +56 -58
  74. package/src/x/tailwind/plugin.ts +1 -1
  75. package/src/FileHttpRouter.test.ts +0 -239
  76. package/src/FileHttpRouter.ts +0 -194
  77. package/src/Hyper.ts +0 -194
  78. package/src/Route.test.ts +0 -1370
  79. package/src/RouteRender.ts +0 -40
  80. package/src/Router.test.ts +0 -375
  81. package/src/Router.ts +0 -255
  82. package/src/bun/BunRoute_bundles.test.ts +0 -219
  83. /package/src/{Bundle.ts → bundler/Bundle.ts} +0 -0
  84. /package/src/{BundleFiles.ts → bundler/BundleFiles.ts} +0 -0
  85. /package/src/{HyperNode.ts → hyper/HyperNode.ts} +0 -0
  86. /package/src/{jsx-runtime.ts → hyper/jsx-runtime.ts} +0 -0
  87. /package/src/{NodeUtils.ts → node/Utils.ts} +0 -0
@@ -0,0 +1,322 @@
1
+ import * as test from "bun:test"
2
+ import * as Route from "./Route.ts"
3
+ import * as RouteMount from "./RouteMount.ts"
4
+ import * as RouteTrie from "./RouteTrie.ts"
5
+
6
+ test.describe(RouteTrie.make, () => {
7
+ test.it("creates trie from route set", () => {
8
+ const routes = Route
9
+ .add("/about", Route.get(Route.text("About")))
10
+
11
+ const trie = RouteTrie.make(routes)
12
+
13
+ test
14
+ .expect(trie.methods["GET"])
15
+ .toBeDefined()
16
+ })
17
+ })
18
+
19
+ test.describe(RouteTrie.lookup, () => {
20
+ test.it("matches exact static path", () => {
21
+ const routes = Route
22
+ .add("/about", Route.get(Route.text("About")))
23
+ const trie = RouteTrie.make(routes)
24
+
25
+ const results = RouteTrie.lookup(trie, "GET", "/about")
26
+
27
+ test
28
+ .expect(results.length)
29
+ .toBe(1)
30
+ test
31
+ .expect(results[0].params)
32
+ .toEqual({})
33
+ })
34
+
35
+ test.it("returns empty for non-matching path", () => {
36
+ const routes = Route
37
+ .add("/about", Route.get(Route.text("About")))
38
+ const trie = RouteTrie.make(routes)
39
+
40
+ const results = RouteTrie.lookup(trie, "GET", "/contact")
41
+
42
+ test
43
+ .expect(results.length)
44
+ .toBe(0)
45
+ })
46
+
47
+ test.it("matches path with single param", () => {
48
+ const routes = Route
49
+ .add("/users/:id", Route.get(Route.text("User")))
50
+ const trie = RouteTrie.make(routes)
51
+
52
+ const results = RouteTrie.lookup(trie, "GET", "/users/123")
53
+
54
+ test
55
+ .expect(results.length)
56
+ .toBe(1)
57
+ test
58
+ .expect(results[0].params)
59
+ .toEqual({ id: "123" })
60
+ })
61
+
62
+ test.it("matches path with multiple params", () => {
63
+ const routes = Route
64
+ .add("/users/:userId/posts/:postId", Route.get(Route.text("Post")))
65
+ const trie = RouteTrie.make(routes)
66
+
67
+ const results = RouteTrie.lookup(trie, "GET", "/users/42/posts/7")
68
+
69
+ test
70
+ .expect(results.length)
71
+ .toBe(1)
72
+ test
73
+ .expect(results[0].params)
74
+ .toEqual({
75
+ userId: "42",
76
+ postId: "7",
77
+ })
78
+ })
79
+
80
+ test.it("matches path with optional param present", () => {
81
+ const routes = Route
82
+ .add("/files/:name?", Route.get(Route.text("File")))
83
+ const trie = RouteTrie.make(routes)
84
+
85
+ const results = RouteTrie.lookup(trie, "GET", "/files/readme")
86
+
87
+ test
88
+ .expect(results.length)
89
+ .toBe(1)
90
+ test
91
+ .expect(results[0].params)
92
+ .toEqual({ name: "readme" })
93
+ })
94
+
95
+ test.it("matches path with optional param absent", () => {
96
+ const routes = Route
97
+ .add("/files/:name?", Route.get(Route.text("File")))
98
+ const trie = RouteTrie.make(routes)
99
+
100
+ const results = RouteTrie.lookup(trie, "GET", "/files")
101
+
102
+ test
103
+ .expect(results.length)
104
+ .toBe(1)
105
+ test
106
+ .expect(results[0].params)
107
+ .toEqual({})
108
+ })
109
+
110
+ test.it("matches path with optional wildcard param", () => {
111
+ const routes = Route
112
+ .add("/docs/:path*", Route.get(Route.text("Docs")))
113
+ const trie = RouteTrie.make(routes)
114
+
115
+ const results = RouteTrie.lookup(trie, "GET", "/docs/api/users/create")
116
+
117
+ test
118
+ .expect(results.length)
119
+ .toBe(1)
120
+ test
121
+ .expect(results[0].params)
122
+ .toEqual({
123
+ path: "api/users/create",
124
+ })
125
+ })
126
+
127
+ test.it("matches path with optional wildcard when empty", () => {
128
+ const routes = Route
129
+ .add("/docs/:path*", Route.get(Route.text("Docs")))
130
+ const trie = RouteTrie.make(routes)
131
+
132
+ const results = RouteTrie.lookup(trie, "GET", "/docs")
133
+
134
+ test
135
+ .expect(results.length)
136
+ .toBe(1)
137
+ test
138
+ .expect(results[0].params)
139
+ .toEqual({})
140
+ })
141
+
142
+ test.it("matches path with required wildcard param", () => {
143
+ const routes = Route
144
+ .add("/docs/:path+", Route.get(Route.text("Docs")))
145
+ const trie = RouteTrie.make(routes)
146
+
147
+ const results = RouteTrie.lookup(trie, "GET", "/docs/api/users/create")
148
+
149
+ test
150
+ .expect(results.length)
151
+ .toBe(1)
152
+ test
153
+ .expect(results[0].params)
154
+ .toEqual({
155
+ path: "api/users/create",
156
+ })
157
+ })
158
+
159
+ test.it("does not match required wildcard when empty", () => {
160
+ const routes = Route
161
+ .add("/docs/:path+", Route.get(Route.text("Docs")))
162
+ const trie = RouteTrie.make(routes)
163
+
164
+ const results = RouteTrie.lookup(trie, "GET", "/docs")
165
+
166
+ test
167
+ .expect(results.length)
168
+ .toBe(0)
169
+ })
170
+
171
+ test.it("required wildcard beats optional wildcard in priority", () => {
172
+ const routes = Route
173
+ .add("/files/:path*", Route.get(Route.text("Optional")))
174
+ .add("/files/:path+", Route.get(Route.text("Required")))
175
+ const trie = RouteTrie.make(routes)
176
+
177
+ const multiResults = RouteTrie.lookup(trie, "GET", "/files/a/b/c")
178
+
179
+ test
180
+ .expect(multiResults.length)
181
+ .toBe(2)
182
+ test
183
+ .expect(multiResults[0].params)
184
+ .toEqual({ path: "a/b/c" })
185
+ test
186
+ .expect(multiResults[1].params)
187
+ .toEqual({ path: "a/b/c" })
188
+ })
189
+
190
+ test.it("optional wildcard matches when required cannot", () => {
191
+ const routes = Route
192
+ .add("/files/:path*", Route.get(Route.text("Optional")))
193
+ .add("/files/:path+", Route.get(Route.text("Required")))
194
+ const trie = RouteTrie.make(routes)
195
+
196
+ const emptyResults = RouteTrie.lookup(trie, "GET", "/files")
197
+
198
+ test
199
+ .expect(emptyResults.length)
200
+ .toBe(1)
201
+ test
202
+ .expect(emptyResults[0].params)
203
+ .toEqual({})
204
+ })
205
+
206
+ test.it("prioritizes static over param routes", () => {
207
+ const routes = Route
208
+ .add("/users/:id", Route.get(Route.text("User by ID")))
209
+ .add("/users/me", Route.get(Route.text("Current user")))
210
+ const trie = RouteTrie.make(routes)
211
+
212
+ const results = RouteTrie.lookup(trie, "GET", "/users/me")
213
+
214
+ test
215
+ .expect(results.length)
216
+ .toBe(2)
217
+ test
218
+ .expect(results[0].params)
219
+ .toEqual({})
220
+ test
221
+ .expect(results[1].params)
222
+ .toEqual({ id: "me" })
223
+ })
224
+
225
+ test.it("matches nested mounted routes", () => {
226
+ const routes = Route
227
+ .add(
228
+ "/admin",
229
+ Route
230
+ .add("/users", Route.get(Route.text("Admin users")))
231
+ .add("/settings", Route.get(Route.text("Admin settings"))),
232
+ )
233
+ const trie = RouteTrie.make(routes)
234
+
235
+ const results = RouteTrie.lookup(trie, "GET", "/admin/users")
236
+
237
+ test
238
+ .expect(results.length)
239
+ .toBe(1)
240
+ })
241
+
242
+ test.it("normalizes paths with trailing slashes", () => {
243
+ const routes = Route
244
+ .add("/about", Route.get(Route.text("About")))
245
+ const trie = RouteTrie.make(routes)
246
+
247
+ const results = RouteTrie.lookup(trie, "GET", "/about/")
248
+
249
+ test
250
+ .expect(results.length)
251
+ .toBe(1)
252
+ test
253
+ .expect(results[0].params)
254
+ .toEqual({})
255
+ })
256
+
257
+ test.it("matches root path", () => {
258
+ const routes = Route
259
+ .add("/", Route.get(Route.text("Home")))
260
+ const trie = RouteTrie.make(routes)
261
+
262
+ const results = RouteTrie.lookup(trie, "GET", "/")
263
+
264
+ test
265
+ .expect(results.length)
266
+ .toBe(1)
267
+ test
268
+ .expect(results[0].params)
269
+ .toEqual({})
270
+ })
271
+
272
+ test.it("matches method-specific routes", () => {
273
+ const routes = Route
274
+ .add("/users", Route.get(Route.text("List users")))
275
+ .add("/users", Route.post(Route.text("Create user")))
276
+ const trie = RouteTrie.make(routes)
277
+
278
+ const getResults = RouteTrie.lookup(trie, "GET", "/users")
279
+ const postResults = RouteTrie.lookup(trie, "POST", "/users")
280
+
281
+ test
282
+ .expect(getResults.length)
283
+ .toBe(1)
284
+ test
285
+ .expect(postResults.length)
286
+ .toBe(1)
287
+ })
288
+
289
+ test.it("matches wildcard method routes", () => {
290
+ const routes = Route
291
+ .add("/health", Route.use(Route.text("OK")))
292
+ const trie = RouteTrie.make(routes)
293
+
294
+ const getResults = RouteTrie.lookup(trie, "GET", "/health")
295
+ const postResults = RouteTrie.lookup(trie, "POST", "/health")
296
+
297
+ test
298
+ .expect(getResults.length)
299
+ .toBe(1)
300
+ test
301
+ .expect(postResults.length)
302
+ .toBe(1)
303
+ })
304
+
305
+ test.it("returns multiple matches for content negotiation", () => {
306
+ const routes = Route
307
+ .add(
308
+ "/page",
309
+ Route.get(
310
+ Route.text("Plain text"),
311
+ Route.html("<p>HTML</p>"),
312
+ ),
313
+ )
314
+ const trie = RouteTrie.make(routes)
315
+
316
+ const results = RouteTrie.lookup(trie, "GET", "/page")
317
+
318
+ test
319
+ .expect(results.length)
320
+ .toBe(2)
321
+ })
322
+ })
@@ -0,0 +1,224 @@
1
+ import * as PathPattern from "./PathPattern.ts"
2
+ import * as Route from "./Route.ts"
3
+
4
+ export interface Node {
5
+ children: Record<string, Node>
6
+ paramChild: Node | null
7
+ paramName: string | null
8
+ requiredWildcardChild: Node | null
9
+ requiredWildcardName: string | null
10
+ optionalWildcardChild: Node | null
11
+ optionalWildcardName: string | null
12
+ routes: Route.Route.Route[]
13
+ }
14
+
15
+ export interface RouteTrie {
16
+ readonly methods: Record<string, Node>
17
+ }
18
+
19
+ export interface LookupResult {
20
+ route: Route.Route.Route
21
+ params: Record<string, string>
22
+ }
23
+
24
+ function createNode(): Node {
25
+ return {
26
+ children: {},
27
+ paramChild: null,
28
+ paramName: null,
29
+ requiredWildcardChild: null,
30
+ requiredWildcardName: null,
31
+ optionalWildcardChild: null,
32
+ optionalWildcardName: null,
33
+ routes: [],
34
+ }
35
+ }
36
+
37
+ function insertRoute(
38
+ node: Node,
39
+ segments: string[],
40
+ route: Route.Route.Route,
41
+ ): void {
42
+ if (segments.length === 0) {
43
+ node.routes.push(route)
44
+ return
45
+ }
46
+
47
+ const segment = segments[0]
48
+ const rest = segments.slice(1)
49
+
50
+ if (segment.startsWith(":")) {
51
+ const name = segment.slice(1)
52
+
53
+ if (name.endsWith("+")) {
54
+ if (!node.requiredWildcardChild) {
55
+ node.requiredWildcardChild = createNode()
56
+ }
57
+ node.requiredWildcardChild.requiredWildcardName = name.slice(0, -1)
58
+ node.requiredWildcardChild.routes.push(route)
59
+ } else if (name.endsWith("*")) {
60
+ if (!node.optionalWildcardChild) {
61
+ node.optionalWildcardChild = createNode()
62
+ }
63
+ node.optionalWildcardChild.optionalWildcardName = name.slice(0, -1)
64
+ node.optionalWildcardChild.routes.push(route)
65
+ } else if (name.endsWith("?")) {
66
+ if (!node.paramChild) {
67
+ node.paramChild = createNode()
68
+ }
69
+ node.paramChild.paramName = name.slice(0, -1)
70
+ insertRoute(node.paramChild, rest, route)
71
+ insertRoute(node, rest, route)
72
+ } else {
73
+ if (!node.paramChild) {
74
+ node.paramChild = createNode()
75
+ }
76
+ node.paramChild.paramName = name
77
+ insertRoute(node.paramChild, rest, route)
78
+ }
79
+ } else {
80
+ if (!node.children[segment]) {
81
+ node.children[segment] = createNode()
82
+ }
83
+ insertRoute(node.children[segment], rest, route)
84
+ }
85
+ }
86
+
87
+ interface CollectedRoute {
88
+ route: Route.Route.Route
89
+ method: string
90
+ path: string
91
+ }
92
+
93
+ function collectRoutes(
94
+ items: Route.Route.Tuple,
95
+ parentPath: string,
96
+ parentMethod: string,
97
+ ): CollectedRoute[] {
98
+ const results: CollectedRoute[] = []
99
+
100
+ for (const item of items) {
101
+ const desc = Route.descriptor(item) as { path?: string; method?: string }
102
+ const currentPath = typeof desc?.path === "string"
103
+ ? parentPath + desc.path
104
+ : parentPath
105
+ const currentMethod = desc?.method ?? parentMethod
106
+
107
+ if (Route.isRoute(item)) {
108
+ if (currentPath !== "") {
109
+ results.push({
110
+ route: item,
111
+ method: currentMethod,
112
+ path: currentPath,
113
+ })
114
+ }
115
+ } else {
116
+ const nestedItems = Route.items(item)
117
+ results.push(...collectRoutes(nestedItems, currentPath, currentMethod))
118
+ }
119
+ }
120
+
121
+ return results
122
+ }
123
+
124
+ export function make(set: Route.RouteSet.Any): RouteTrie {
125
+ const methods: Record<string, Node> = {}
126
+ const collected = collectRoutes(Route.items(set), "", "*")
127
+
128
+ for (const { route, method, path } of collected) {
129
+ if (!methods[method]) {
130
+ methods[method] = createNode()
131
+ }
132
+ const result = PathPattern.validate(path)
133
+ if (!result.ok) {
134
+ throw new Error(result.error)
135
+ }
136
+ insertRoute(methods[method], result.segments, route)
137
+ }
138
+
139
+ return { methods }
140
+ }
141
+
142
+ function lookupNode(
143
+ node: Node,
144
+ segments: string[],
145
+ params: Record<string, string>,
146
+ ): LookupResult[] {
147
+ const results: LookupResult[] = []
148
+
149
+ if (segments.length === 0) {
150
+ for (const route of node.routes) {
151
+ results.push({ route, params })
152
+ }
153
+ if (
154
+ node.optionalWildcardChild
155
+ && node.optionalWildcardChild.optionalWildcardName
156
+ ) {
157
+ for (const route of node.optionalWildcardChild.routes) {
158
+ results.push({ route, params })
159
+ }
160
+ }
161
+ return results
162
+ }
163
+
164
+ const segment = segments[0]
165
+ const rest = segments.slice(1)
166
+
167
+ if (node.children[segment]) {
168
+ results.push(...lookupNode(node.children[segment], rest, params))
169
+ }
170
+
171
+ if (node.paramChild && node.paramChild.paramName) {
172
+ const newParams = { ...params, [node.paramChild.paramName]: segment }
173
+ results.push(...lookupNode(node.paramChild, rest, newParams))
174
+ }
175
+
176
+ if (
177
+ node.requiredWildcardChild
178
+ && node.requiredWildcardChild.requiredWildcardName
179
+ ) {
180
+ const wildcardValue = segments.join("/")
181
+ const newParams = {
182
+ ...params,
183
+ [node.requiredWildcardChild.requiredWildcardName]: wildcardValue,
184
+ }
185
+ for (const route of node.requiredWildcardChild.routes) {
186
+ results.push({ route, params: newParams })
187
+ }
188
+ }
189
+
190
+ if (
191
+ node.optionalWildcardChild
192
+ && node.optionalWildcardChild.optionalWildcardName
193
+ ) {
194
+ const wildcardValue = segments.join("/")
195
+ const newParams = {
196
+ ...params,
197
+ [node.optionalWildcardChild.optionalWildcardName]: wildcardValue,
198
+ }
199
+ for (const route of node.optionalWildcardChild.routes) {
200
+ results.push({ route, params: newParams })
201
+ }
202
+ }
203
+
204
+ return results
205
+ }
206
+
207
+ export function lookup(
208
+ trie: RouteTrie,
209
+ method: string,
210
+ path: string,
211
+ ): LookupResult[] {
212
+ const segments = path.split("/").filter(Boolean)
213
+ const results: LookupResult[] = []
214
+
215
+ if (trie.methods[method]) {
216
+ results.push(...lookupNode(trie.methods[method], segments, {}))
217
+ }
218
+
219
+ if (method !== "*" && trie.methods["*"]) {
220
+ results.push(...lookupNode(trie.methods["*"], segments, {}))
221
+ }
222
+
223
+ return results
224
+ }