effect-start 0.13.1 → 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 (95) hide show
  1. package/package.json +13 -14
  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 +6 -6
  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 +40 -21
  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 -37
  39. package/src/StartApp.ts +20 -16
  40. package/src/TuplePathPattern.ts +64 -0
  41. package/src/Values.ts +16 -0
  42. package/src/bun/BunBundle.test.ts +37 -43
  43. package/src/bun/BunBundle.ts +2 -2
  44. package/src/bun/BunBundle_imports.test.ts +6 -8
  45. package/src/bun/BunHttpServer.test.ts +183 -6
  46. package/src/bun/BunHttpServer.ts +56 -32
  47. package/src/bun/BunHttpServer_web.ts +18 -6
  48. package/src/bun/BunImportTrackerPlugin.test.ts +3 -3
  49. package/src/bun/BunRoute.ts +29 -210
  50. package/src/{Bundle.ts → bundler/Bundle.ts} +0 -35
  51. package/src/{BundleHttp.test.ts → bundler/BundleHttp.test.ts} +36 -64
  52. package/src/{BundleHttp.ts → bundler/BundleHttp.ts} +1 -1
  53. package/src/client/index.ts +1 -1
  54. package/src/{Effect_HttpRouter.test.ts → effect/HttpRouter.test.ts} +69 -91
  55. package/src/{EncryptedCookies.test.ts → experimental/EncryptedCookies.test.ts} +125 -64
  56. package/src/{SseHttpResponse.ts → experimental/SseHttpResponse.ts} +1 -1
  57. package/src/experimental/index.ts +2 -0
  58. package/src/hyper/Hyper.ts +89 -0
  59. package/src/{HyperHtml.test.ts → hyper/HyperHtml.test.ts} +13 -13
  60. package/src/{HyperHtml.ts → hyper/HyperHtml.ts} +2 -2
  61. package/src/{jsx.d.ts → hyper/jsx.d.ts} +1 -1
  62. package/src/index.ts +1 -21
  63. package/src/middlewares/BasicAuthMiddleware.test.ts +29 -19
  64. package/src/middlewares/index.ts +1 -0
  65. package/src/{NodeFileSystem.ts → node/FileSystem.ts} +6 -2
  66. package/src/{TestHttpClient.test.ts → testing/TestHttpClient.test.ts} +27 -27
  67. package/src/{TestHttpClient.ts → testing/TestHttpClient.ts} +0 -1
  68. package/src/{TestLogger.test.ts → testing/TestLogger.test.ts} +27 -11
  69. package/src/testing/index.ts +3 -0
  70. package/src/x/datastar/Datastar.test.ts +47 -48
  71. package/src/x/datastar/Datastar.ts +1 -1
  72. package/src/x/tailwind/TailwindPlugin.test.ts +56 -58
  73. package/src/x/tailwind/TailwindPlugin.ts +23 -17
  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/JsModule.test.ts +0 -14
  79. package/src/JsModule.ts +0 -116
  80. package/src/PublicDirectory.test.ts +0 -280
  81. package/src/PublicDirectory.ts +0 -108
  82. package/src/Route.test.ts +0 -1370
  83. package/src/RouteRender.ts +0 -40
  84. package/src/Router.test.ts +0 -375
  85. package/src/Router.ts +0 -255
  86. package/src/StartHttp.ts +0 -42
  87. package/src/bun/BunRoute.test.ts +0 -480
  88. package/src/bun/BunRoute_bundles.test.ts +0 -219
  89. /package/src/{BundleFiles.ts → bundler/BundleFiles.ts} +0 -0
  90. /package/src/{EncryptedCookies.ts → experimental/EncryptedCookies.ts} +0 -0
  91. /package/src/{HyperNode.ts → hyper/HyperNode.ts} +0 -0
  92. /package/src/{jsx-runtime.ts → hyper/jsx-runtime.ts} +0 -0
  93. /package/src/{NodeUtils.ts → node/Utils.ts} +0 -0
  94. /package/src/{TestLogger.ts → testing/TestLogger.ts} +0 -0
  95. /package/src/{testing.ts → testing/utils.ts} +0 -0
@@ -1,5 +1,4 @@
1
- import * as t from "bun:test"
2
-
1
+ import * as test from "bun:test"
3
2
  import { extractClassNames } from "./TailwindPlugin.ts"
4
3
 
5
4
  // Keep the old broad implementation for comparison tests
@@ -17,70 +16,70 @@ function extractClassNamesBroad(source: string): Set<string> {
17
16
  )
18
17
  }
19
18
 
20
- t.describe(`${extractClassNames.name}`, () => {
21
- t.test("Basic HTML class attributes", () => {
19
+ test.describe(`${extractClassNames.name}`, () => {
20
+ test.it("Basic HTML class attributes", () => {
22
21
  const source = `<div class="bg-red-500 text-white">Hello</div>`
23
22
  const result = extractClassNames(source)
24
23
 
25
- t
24
+ test
26
25
  .expect([...result].sort())
27
26
  .toEqual(["bg-red-500", "text-white"])
28
27
  })
29
28
 
30
- t.test("Basic JSX className attributes", () => {
29
+ test.it("Basic JSX className attributes", () => {
31
30
  const source =
32
31
  `<div className="flex items-center justify-between">Content</div>`
33
32
  const result = extractClassNames(source)
34
33
 
35
- t
34
+ test
36
35
  .expect([...result].sort())
37
36
  .toEqual(["flex", "items-center", "justify-between"])
38
37
  })
39
38
 
40
- t.test("Single quotes", () => {
39
+ test.it("Single quotes", () => {
41
40
  const source = `<div class='bg-blue-500 hover:bg-blue-600'>Button</div>`
42
41
  const result = extractClassNames(source)
43
42
 
44
- t
43
+ test
45
44
  .expect([...result].sort())
46
45
  .toEqual(["bg-blue-500", "hover:bg-blue-600"])
47
46
  })
48
47
 
49
- t.test("Template literals in JSX", () => {
48
+ test.it("Template literals in JSX", () => {
50
49
  const source = `<div className={\`bg-\${color} text-lg\`}>Dynamic</div>`
51
50
  const result = extractClassNames(source)
52
51
 
53
52
  // Should extract valid static class names from template literals
54
- t
53
+ test
55
54
  .expect([...result].sort())
56
55
  .toEqual(["text-lg"])
57
56
  })
58
57
 
59
- t.test("JSX with quoted strings", () => {
58
+ test.it("JSX with quoted strings", () => {
60
59
  const source = `<div className={"p-4 m-2"}>Static in braces</div>`
61
60
  const result = extractClassNames(source)
62
61
 
63
- t
62
+ test
64
63
  .expect([...result].sort())
65
64
  .toEqual(["m-2", "p-4"])
66
65
  })
67
66
 
68
- t.test("Multi-line attributes", () => {
69
- const source = `<div
67
+ test.it("Multi-line attributes", () => {
68
+ const source = `<div
70
69
  className="
71
- grid
72
- grid-cols-3
70
+ grid
71
+ grid-cols-3
73
72
  gap-4
74
73
  "
75
74
  >Grid</div>`
76
75
  const result = extractClassNames(source)
77
76
 
78
- t
77
+ test
79
78
  .expect([...result].sort())
80
79
  .toEqual(["gap-4", "grid", "grid-cols-3"])
81
80
  })
82
81
 
83
- t.test("Whitespace variations around equals", () => {
82
+ test.it("Whitespace variations around equals", () => {
84
83
  const cases = [
85
84
  `<div class="text-sm">Normal</div>`,
86
85
  `<div class ="text-md">Space before</div>`,
@@ -91,37 +90,37 @@ t.describe(`${extractClassNames.name}`, () => {
91
90
  for (const source of cases) {
92
91
  const result = extractClassNames(source)
93
92
 
94
- t
93
+ test
95
94
  .expect(result.size)
96
95
  .toBe(1)
97
96
  }
98
97
  })
99
98
 
100
- t.test("Arbitrary value classes", () => {
99
+ test.it("Arbitrary value classes", () => {
101
100
  const source =
102
101
  `<div className="w-[32px] bg-[#ff0000] text-[1.5rem]">Arbitrary</div>`
103
102
  const result = extractClassNames(source)
104
103
 
105
- t
104
+ test
106
105
  .expect([...result].sort())
107
106
  .toEqual(["bg-[#ff0000]", "text-[1.5rem]", "w-[32px]"])
108
107
  })
109
108
 
110
- t.test("Fraction classes", () => {
109
+ test.it("Fraction classes", () => {
111
110
  const source = `<div className="w-1/2 h-3/4">Fractions</div>`
112
111
  const result = extractClassNames(source)
113
112
 
114
- t
113
+ test
115
114
  .expect([...result].sort())
116
115
  .toEqual(["h-3/4", "w-1/2"])
117
116
  })
118
117
 
119
- t.test("Complex Tailwind classes", () => {
118
+ test.it("Complex Tailwind classes", () => {
120
119
  const source =
121
120
  `<div className="sm:w-1/2 md:w-1/3 lg:w-1/4 hover:bg-gray-100 focus:ring-2">Responsive</div>`
122
121
  const result = extractClassNames(source)
123
122
 
124
- t
123
+ test
125
124
  .expect([...result].sort())
126
125
  .toEqual([
127
126
  "focus:ring-2",
@@ -132,71 +131,71 @@ t.describe(`${extractClassNames.name}`, () => {
132
131
  ])
133
132
  })
134
133
 
135
- t.test("Should ignore similar attribute names", () => {
134
+ test.it("Should ignore similar attribute names", () => {
136
135
  const source =
137
136
  `<div data-class="should-ignore" myclass="also-ignore" class="keep-this">Test</div>`
138
137
  const result = extractClassNames(source)
139
138
 
140
- t
139
+ test
141
140
  .expect([...result])
142
141
  .toEqual(["keep-this"])
143
142
  })
144
143
 
145
- t.test("Should handle case sensitivity", () => {
144
+ test.it("Should handle case sensitivity", () => {
146
145
  const source =
147
146
  `<div Class="uppercase-class" class="lowercase-class">Mixed case</div>`
148
147
  const result = extractClassNames(source)
149
148
 
150
149
  // Our current implementation only matches lowercase 'class'
151
- t
150
+ test
152
151
  .expect([...result])
153
152
  .toEqual(["lowercase-class"])
154
153
  })
155
154
 
156
- t.test("Empty class attributes", () => {
155
+ test.it("Empty class attributes", () => {
157
156
  const source = `<div class="" className=''>Empty</div>`
158
157
  const result = extractClassNames(source)
159
158
 
160
- t
159
+ test
161
160
  .expect(result.size)
162
161
  .toBe(0)
163
162
  })
164
163
 
165
- t.test("Classes with special characters", () => {
164
+ test.it("Classes with special characters", () => {
166
165
  const source =
167
166
  `<div className="group-hover:text-blue-500 peer-focus:ring-2">Special chars</div>`
168
167
  const result = extractClassNames(source)
169
168
 
170
- t
169
+ test
171
170
  .expect([...result].sort())
172
171
  .toEqual(["group-hover:text-blue-500", "peer-focus:ring-2"])
173
172
  })
174
173
 
175
- t.test("Should not match classes in comments", () => {
174
+ test.it("Should not match classes in comments", () => {
176
175
  const source = `
177
176
  <!-- <div class="commented-out">Should not match</div> -->
178
177
  <div class="real-class">Should match</div>
179
178
  `
180
179
  const result = extractClassNames(source)
181
180
 
182
- t
181
+ test
183
182
  .expect([...result])
184
183
  .toEqual(["real-class"])
185
184
  })
186
185
 
187
- t.test("Should not match classes in strings", () => {
186
+ test.it("Should not match classes in strings", () => {
188
187
  const source = `
189
188
  const message = "This class='fake-class' should not match";
190
189
  <div class="real-class">Real element</div>
191
190
  `
192
191
  const result = extractClassNames(source)
193
192
 
194
- t
193
+ test
195
194
  .expect([...result])
196
195
  .toEqual(["real-class"])
197
196
  })
198
197
 
199
- t.test("Complex JSX expressions should be ignored", () => {
198
+ test.it("Complex JSX expressions should be ignored", () => {
200
199
  const source = `
201
200
  <div className={condition ? "conditional-class" : "other-class"}>Conditional</div>
202
201
  <div className={\`template-\${variable}\`}>Template</div>
@@ -206,12 +205,12 @@ t.describe(`${extractClassNames.name}`, () => {
206
205
  const result = extractClassNames(source)
207
206
 
208
207
  // Only the static class should match with our strict implementation
209
- t
208
+ test
210
209
  .expect([...result])
211
210
  .toEqual(["static-class"])
212
211
  })
213
212
 
214
- t.test("Vue.js class bindings should be ignored", () => {
213
+ test.it("Vue.js class bindings should be ignored", () => {
215
214
  const source = `
216
215
  <div :class="{ 'active': isActive }">Vue object</div>
217
216
  <div :class="['base', condition && 'active']">Vue array</div>
@@ -220,34 +219,34 @@ t.describe(`${extractClassNames.name}`, () => {
220
219
  const result = extractClassNames(source)
221
220
 
222
221
  // Only static class should match
223
- t
222
+ test
224
223
  .expect([...result])
225
224
  .toEqual(["static-vue-class"])
226
225
  })
227
226
 
228
- t.test("Svelte class directives should be ignored", () => {
227
+ test.it("Svelte class directives should be ignored", () => {
229
228
  const source = `
230
229
  <div class:active={condition}>Svelte directive</div>
231
230
  <div class="static-svelte-class">Static Svelte</div>
232
231
  `
233
232
  const result = extractClassNames(source)
234
233
 
235
- t
234
+ test
236
235
  .expect([...result])
237
236
  .toEqual(["static-svelte-class"])
238
237
  })
239
238
 
240
- t.test("Escaped quotes should be handled", () => {
239
+ test.it("Escaped quotes should be handled", () => {
241
240
  const source =
242
241
  `<div class="text-sm before:content-['Hello']">Escaped quotes</div>`
243
242
  const result = extractClassNames(source)
244
243
 
245
- t
244
+ test
246
245
  .expect([...result].sort())
247
246
  .toEqual(["before:content-['Hello']", "text-sm"])
248
247
  })
249
248
 
250
- t.test("Current broad implementation comparison", () => {
249
+ test.it("Current broad implementation comparison", () => {
251
250
  const source = `
252
251
  <div class="bg-red-500 text-white">Element</div>
253
252
  <p>Some random-text-with-hyphens in content</p>
@@ -258,27 +257,26 @@ t.describe(`${extractClassNames.name}`, () => {
258
257
  const strictResult = extractClassNames(source)
259
258
 
260
259
  // Broad should pick up more tokens
261
- t
260
+ test
262
261
  .expect(broadResult.size)
263
262
  .toBeGreaterThan(strictResult.size)
264
-
265
263
  // Strict should only have the actual class names
266
- t
264
+ test
267
265
  .expect([...strictResult].sort())
268
266
  .toEqual(["bg-red-500", "text-white"])
269
267
  })
270
268
 
271
- t.test("Component names with dots", () => {
269
+ test.it("Component names with dots", () => {
272
270
  const source =
273
271
  `<Toast.Toast class="toast toast-top toast-center fixed top-8 z-10">Content</Toast.Toast>`
274
272
  const result = extractClassNames(source)
275
273
 
276
- t
274
+ test
277
275
  .expect([...result].sort())
278
276
  .toEqual(["fixed", "toast", "toast-center", "toast-top", "top-8", "z-10"])
279
277
  })
280
278
 
281
- t.test("Complex component names and attributes", () => {
279
+ test.it("Complex component names and attributes", () => {
282
280
  const source = `
283
281
  <My.Component.Name className="flex items-center">Content</My.Component.Name>
284
282
  <Component-with-dashes class="bg-red-500">Content</Component-with-dashes>
@@ -287,7 +285,7 @@ t.describe(`${extractClassNames.name}`, () => {
287
285
  `
288
286
  const result = extractClassNames(source)
289
287
 
290
- t
288
+ test
291
289
  .expect([...result].sort())
292
290
  .toEqual([
293
291
  "bg-red-500",
@@ -298,7 +296,7 @@ t.describe(`${extractClassNames.name}`, () => {
298
296
  ])
299
297
  })
300
298
 
301
- t.test("Conditional JSX with Toast component", () => {
299
+ test.it("Conditional JSX with Toast component", () => {
302
300
  const source = `{toastParam !== undefined && (
303
301
  <Toast.Toast class="toast toast-top toast-center fixed top-8 z-10">
304
302
  <div class="alert alert-success">
@@ -310,7 +308,7 @@ t.describe(`${extractClassNames.name}`, () => {
310
308
  )}`
311
309
  const result = extractClassNames(source)
312
310
 
313
- t
311
+ test
314
312
  .expect([...result].sort())
315
313
  .toEqual([
316
314
  "alert",
@@ -324,11 +322,11 @@ t.describe(`${extractClassNames.name}`, () => {
324
322
  ])
325
323
  })
326
324
 
327
- t.test("Template literals with expressions", () => {
325
+ test.it("Template literals with expressions", () => {
328
326
  const source = `<div class={\`toast \${props.class ?? ""}\`}>Content</div>`
329
327
  const result = extractClassNames(source)
330
328
 
331
- t
329
+ test
332
330
  .expect([...result].sort())
333
331
  .toEqual(["toast"])
334
332
  })
@@ -44,21 +44,22 @@ export const make = (opts?: {
44
44
  // (imported path) -> (importer paths)
45
45
  const importDescendants = new Map<string, Set<string>>()
46
46
 
47
- if (opts?.scanPath) {
48
- const candidates = await scanFiles(opts.scanPath)
47
+ const prepopulateCandidates = opts?.scanPath
48
+ ? async () => {
49
+ const candidates = await scanFiles(opts.scanPath!)
49
50
 
50
- candidates.forEach(candidate => scannedCandidates.add(candidate))
51
- }
51
+ scannedCandidates.clear()
52
52
 
53
- /**
54
- * Track import relationships when dynamically scanning
55
- * from tailwind entrypoints.
56
- *
57
- * As of Bun 1.3 this pathway break for Bun Full-Stack server.
58
- * Better to pass scanPath explicitly.
59
- * @see https://github.com/oven-sh/bun/issues/20877
60
- */
61
- if (!opts?.scanPath) {
53
+ candidates.forEach(candidate => scannedCandidates.add(candidate))
54
+ }
55
+ : null
56
+
57
+ // Track import relationships when dynamically scanning
58
+ // from tailwind entrypoints.
59
+ // As of Bun 1.3 this pathway break for Bun Full-Stack server.
60
+ // Better to pass scanPath explicitly.
61
+ // @see https://github.com/oven-sh/bun/issues/20877
62
+ if (!prepopulateCandidates) {
62
63
  builder.onResolve({
63
64
  filter: /.*/,
64
65
  }, (args) => {
@@ -130,14 +131,15 @@ export const make = (opts?: {
130
131
  onDependency: (path) => {},
131
132
  })
132
133
 
134
+ await prepopulateCandidates?.()
135
+
133
136
  // wait for other files to be loaded so we can collect class name candidates
134
137
  await args.defer()
135
138
 
136
- const candidates = new Set<string>()
139
+ const candidates = new Set<string>(scannedCandidates)
137
140
 
138
- scannedCandidates.forEach(candidate => candidates.add(candidate))
139
-
140
- {
141
+ // when we scan a path, we don't need to track candidate tree
142
+ if (!prepopulateCandidates) {
141
143
  const pendingModules = [
142
144
  // get class name candidates from all modules that import this one
143
145
  ...(importAncestors.get(args.path) ?? []),
@@ -295,6 +297,10 @@ async function scanFiles(dir: string): Promise<Set<string>> {
295
297
  absolute: true,
296
298
  })
297
299
  ) {
300
+ if (filePath.includes("/node_modules/")) {
301
+ continue
302
+ }
303
+
298
304
  const contents = await Bun.file(filePath).text()
299
305
  const classNames = extractClassNames(contents)
300
306
 
@@ -1,5 +1,5 @@
1
1
  import * as NPath from "node:path"
2
- import * as NodeUtils from "../../NodeUtils.ts"
2
+ import * as NodeUtils from "../../node/Utils.ts"
3
3
  import * as TailwindPlugin from "./TailwindPlugin.ts"
4
4
 
5
5
  // Append `?dir=` to module identifier to pass custom directory to scan
@@ -1,239 +0,0 @@
1
- import * as Error from "@effect/platform/Error"
2
- import * as FileSystem from "@effect/platform/FileSystem"
3
- import * as HttpRouter from "@effect/platform/HttpRouter"
4
- import * as t from "bun:test"
5
- import * as Data from "effect/Data"
6
- import * as Effect from "effect/Effect"
7
- import * as FileHttpRouter from "./FileHttpRouter.ts"
8
- import * as FileRouter from "./FileRouter.ts"
9
- import * as Route from "./Route.ts"
10
- import * as TestHttpClient from "./TestHttpClient.ts"
11
- import { effectFn } from "./testing.ts"
12
-
13
- class CustomError extends Data.TaggedError("CustomError") {}
14
-
15
- const SampleRoutes = [
16
- {
17
- path: "/users",
18
- load: async () => ({
19
- default: Route
20
- .html(Effect.succeed("Users list"))
21
- .post(Route.html(Effect.succeed("User created"))),
22
- }),
23
- },
24
- {
25
- path: "/articles",
26
- load: async () => ({
27
- default: Route.html(Effect.succeed("Articles list")),
28
- }),
29
- },
30
- ] as const
31
-
32
- const effect = effectFn()
33
-
34
- t.it("HttpRouter Requirement and Error types infers", () =>
35
- effect(function*() {
36
- const router = yield* FileHttpRouter.make(SampleRoutes)
37
-
38
- // This should fail to compile if the router type is HttpRouter<any, any>
39
- const _typeCheck: typeof router extends HttpRouter.HttpRouter<
40
- Error.SystemError | "PostError" | CustomError,
41
- FileSystem.FileSystem | "PostService"
42
- > ? true
43
- : false = true
44
- }))
45
-
46
- t.it("HTTP methods", () =>
47
- effect(function*() {
48
- const allMethodsRoute: FileRouter.LazyRoute = {
49
- path: "/",
50
- load: async () => ({
51
- default: Route
52
- .html(Effect.succeed("GET"))
53
- .post(Route.html(Effect.succeed("POST")))
54
- .put(Route.html(Effect.succeed("PUT")))
55
- .patch(Route.html(Effect.succeed("PATCH")))
56
- .delete(Route.html(Effect.succeed("DELETE")))
57
- .options(Route.html(Effect.succeed("OPTIONS")))
58
- .head(Route.html(Effect.succeed("HEAD"))),
59
- }),
60
- }
61
-
62
- const router = yield* FileHttpRouter.make([allMethodsRoute])
63
- const routesList = Array.from(router.routes)
64
-
65
- t
66
- .expect(routesList)
67
- .toEqual(
68
- t.expect.arrayContaining([
69
- t.expect.objectContaining({ path: "/", method: "GET" }),
70
- t.expect.objectContaining({ path: "/", method: "POST" }),
71
- t.expect.objectContaining({ path: "/", method: "PUT" }),
72
- t.expect.objectContaining({ path: "/", method: "PATCH" }),
73
- t.expect.objectContaining({ path: "/", method: "DELETE" }),
74
- t.expect.objectContaining({ path: "/", method: "OPTIONS" }),
75
- t.expect.objectContaining({ path: "/", method: "HEAD" }),
76
- ]),
77
- )
78
- }))
79
-
80
- t.it("router handles requests correctly", () =>
81
- effect(function*() {
82
- const router = yield* FileHttpRouter.make(SampleRoutes)
83
- const client = TestHttpClient.make(router)
84
-
85
- const getUsersResponse = yield* client.get("/users")
86
-
87
- t
88
- .expect(getUsersResponse.status)
89
- .toBe(200)
90
-
91
- t
92
- .expect(yield* getUsersResponse.text)
93
- .toBe("Users list")
94
-
95
- const postUsersResponse = yield* client.post("/users")
96
-
97
- t
98
- .expect(postUsersResponse.status)
99
- .toBe(200)
100
-
101
- t
102
- .expect(yield* postUsersResponse.text)
103
- .toBe("User created")
104
- }))
105
-
106
- t.it(
107
- "handles routes with special characters (tilde and hyphen)",
108
- () =>
109
- effect(function*() {
110
- const specialCharRoutes: FileRouter.LazyRoute[] = [
111
- {
112
- path: "/api-v1",
113
- load: async () => ({
114
- default: Route.text("API v1"),
115
- }),
116
- },
117
- {
118
- path: "/files~backup",
119
- load: async () => ({
120
- default: Route.text("Backup files"),
121
- }),
122
- },
123
- {
124
- path: "/test-route~temp",
125
- load: async () => ({
126
- default: Route.post(Route.text("Test route")),
127
- }),
128
- },
129
- ]
130
-
131
- const router = yield* FileHttpRouter.make(specialCharRoutes)
132
- const client = TestHttpClient.make(router)
133
-
134
- const apiResponse = yield* client.get("/api-v1")
135
-
136
- t
137
- .expect(apiResponse.status)
138
- .toBe(200)
139
-
140
- t
141
- .expect(yield* apiResponse.text)
142
- .toBe("API v1")
143
-
144
- const backupResponse = yield* client.get("/files~backup")
145
-
146
- t
147
- .expect(backupResponse.status)
148
- .toBe(200)
149
-
150
- t
151
- .expect(yield* backupResponse.text)
152
- .toBe("Backup files")
153
-
154
- const testResponse = yield* client.post("/test-route~temp")
155
-
156
- t
157
- .expect(testResponse.status)
158
- .toBe(200)
159
-
160
- t
161
- .expect(yield* testResponse.text)
162
- .toBe("Test route")
163
- }),
164
- )
165
-
166
- t.it(
167
- "layer routes can wrap inner routes using next()",
168
- () =>
169
- effect(function*() {
170
- const routeWithLayer: FileRouter.LazyRoute = {
171
- path: "/page",
172
- load: async () => ({
173
- default: Route.html(Effect.succeed("<h1>Page Content</h1>")),
174
- }),
175
- layers: [
176
- async () => ({
177
- default: Route.layer(
178
- Route.html(function*(context) {
179
- const innerContent = yield* context.next()
180
- return `<html><body>${innerContent}</body></html>`
181
- }),
182
- ),
183
- }),
184
- ],
185
- }
186
-
187
- const router = yield* FileHttpRouter.make([routeWithLayer])
188
- const client = TestHttpClient.make(router)
189
-
190
- const response = yield* client.get("/page")
191
-
192
- t.expect(response.status).toBe(200)
193
-
194
- const html = yield* response.text
195
-
196
- t.expect(html).toBe("<html><body><h1>Page Content</h1></body></html>")
197
- }),
198
- )
199
-
200
- t.it("nested layers compose correctly with next()", () =>
201
- effect(function*() {
202
- const routeWithNestedLayers: FileRouter.LazyRoute = {
203
- path: "/nested",
204
- load: async () => ({
205
- default: Route.html(Effect.succeed("content")),
206
- }),
207
- layers: [
208
- async () => ({
209
- default: Route.layer(
210
- Route.html(function*(context) {
211
- const inner = yield* context.next()
212
- return `<div class="outer">${inner}</div>`
213
- }),
214
- ),
215
- }),
216
- async () => ({
217
- default: Route.layer(
218
- Route.html(function*(context) {
219
- const inner = yield* context.next()
220
- return `<div class="inner">${inner}</div>`
221
- }),
222
- ),
223
- }),
224
- ],
225
- }
226
-
227
- const router = yield* FileHttpRouter.make([routeWithNestedLayers])
228
- const client = TestHttpClient.make(router)
229
-
230
- const response = yield* client.get("/nested")
231
-
232
- t.expect(response.status).toBe(200)
233
-
234
- const html = yield* response.text
235
-
236
- t.expect(html).toBe(
237
- "<div class=\"outer\"><div class=\"inner\">content</div></div>",
238
- )
239
- }))