effect-start 0.11.0 → 0.12.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.
- package/README.md +42 -73
- package/package.json +8 -9
- package/src/Commander.test.ts +3 -4
- package/src/FileHttpRouter.test.ts +6 -43
- package/src/FileHttpRouter.ts +0 -15
- package/src/FileRouter.ts +79 -37
- package/src/FileRouterCodegen.test.ts +449 -265
- package/src/FileRouterCodegen.ts +67 -22
- package/src/Route.ts +1 -2
- package/src/Router.test.ts +11 -52
- package/src/Router.ts +55 -137
- package/src/SchemaExtra.ts +102 -0
- package/src/Start.ts +2 -2
- package/src/TestLogger.test.ts +38 -0
- package/src/TestLogger.ts +56 -0
- package/src/bun/BunHttpServer.ts +21 -14
- package/src/bun/BunRoute.test.ts +17 -51
- package/src/bun/BunRoute.ts +7 -61
- package/src/bun/BunRoute_bundles.test.ts +6 -5
|
@@ -1,12 +1,41 @@
|
|
|
1
|
+
import * as Error from "@effect/platform/Error"
|
|
1
2
|
import * as FileSystem from "@effect/platform/FileSystem"
|
|
2
3
|
import * as t from "bun:test"
|
|
3
|
-
import { MemoryFileSystem } from "effect-memfs"
|
|
4
4
|
import * as Effect from "effect/Effect"
|
|
5
|
+
import * as Schema from "effect/Schema"
|
|
6
|
+
import * as Scope from "effect/Scope"
|
|
7
|
+
import * as path from "node:path"
|
|
8
|
+
import * as FileRouter from "./FileRouter.ts"
|
|
5
9
|
import { parseRoute } from "./FileRouter.ts"
|
|
6
10
|
import type { RouteHandle } from "./FileRouter.ts"
|
|
7
11
|
import * as FileRouterCodegen from "./FileRouterCodegen.ts"
|
|
12
|
+
import * as NodeFileSystem from "./NodeFileSystem.ts"
|
|
8
13
|
import * as Route from "./Route.ts"
|
|
9
|
-
import
|
|
14
|
+
import * as SchemaExtra from "./SchemaExtra.ts"
|
|
15
|
+
import * as TestLogger from "./TestLogger.ts"
|
|
16
|
+
|
|
17
|
+
function createTempDirWithFiles(
|
|
18
|
+
files: Record<string, string>,
|
|
19
|
+
): Effect.Effect<
|
|
20
|
+
string,
|
|
21
|
+
Error.PlatformError,
|
|
22
|
+
FileSystem.FileSystem | Scope.Scope
|
|
23
|
+
> {
|
|
24
|
+
return Effect.gen(function*() {
|
|
25
|
+
const fs = yield* FileSystem.FileSystem
|
|
26
|
+
const tempDir = yield* fs.makeTempDirectoryScoped()
|
|
27
|
+
|
|
28
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
29
|
+
const fullPath = path.join(tempDir, filePath)
|
|
30
|
+
const dir = path.dirname(fullPath)
|
|
31
|
+
|
|
32
|
+
yield* fs.makeDirectory(dir, { recursive: true })
|
|
33
|
+
yield* fs.writeFileString(fullPath, content)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return tempDir
|
|
37
|
+
})
|
|
38
|
+
}
|
|
10
39
|
|
|
11
40
|
t.it("generates code for routes only", () => {
|
|
12
41
|
const handles: RouteHandle[] = [
|
|
@@ -34,9 +63,7 @@ export const routes = [
|
|
|
34
63
|
] as const
|
|
35
64
|
`
|
|
36
65
|
|
|
37
|
-
t
|
|
38
|
-
.expect(code)
|
|
39
|
-
.toBe(expected)
|
|
66
|
+
t.expect(code).toBe(expected)
|
|
40
67
|
})
|
|
41
68
|
|
|
42
69
|
t.it("generates code with layers", () => {
|
|
@@ -72,9 +99,7 @@ export const routes = [
|
|
|
72
99
|
] as const
|
|
73
100
|
`
|
|
74
101
|
|
|
75
|
-
t
|
|
76
|
-
.expect(code)
|
|
77
|
-
.toBe(expected)
|
|
102
|
+
t.expect(code).toBe(expected)
|
|
78
103
|
})
|
|
79
104
|
|
|
80
105
|
t.it("generates code with nested layers", () => {
|
|
@@ -113,9 +138,7 @@ export const routes = [
|
|
|
113
138
|
] as const
|
|
114
139
|
`
|
|
115
140
|
|
|
116
|
-
t
|
|
117
|
-
.expect(code)
|
|
118
|
-
.toBe(expected)
|
|
141
|
+
t.expect(code).toBe(expected)
|
|
119
142
|
})
|
|
120
143
|
|
|
121
144
|
t.it("only includes group layers for routes in that group", () => {
|
|
@@ -128,22 +151,14 @@ t.it("only includes group layers for routes in that group", () => {
|
|
|
128
151
|
|
|
129
152
|
const code = FileRouterCodegen.generateCode(handles)
|
|
130
153
|
|
|
131
|
-
t
|
|
132
|
-
.expect(code)
|
|
133
|
-
.toContain("path: \"/users\"")
|
|
154
|
+
t.expect(code).toContain("path: \"/users\"")
|
|
134
155
|
|
|
135
|
-
t
|
|
136
|
-
.expect(code)
|
|
137
|
-
.toContain("path: \"/movies\"")
|
|
156
|
+
t.expect(code).toContain("path: \"/movies\"")
|
|
138
157
|
|
|
139
158
|
// /users should have both root layer and (admin) layer
|
|
140
|
-
t
|
|
141
|
-
.expect(code)
|
|
142
|
-
.toContain("() => import(\"./layer.tsx\")")
|
|
159
|
+
t.expect(code).toContain("() => import(\"./layer.tsx\")")
|
|
143
160
|
|
|
144
|
-
t
|
|
145
|
-
.expect(code)
|
|
146
|
-
.toContain("() => import(\"./(admin)/layer.ts\")")
|
|
161
|
+
t.expect(code).toContain("() => import(\"./(admin)/layer.ts\")")
|
|
147
162
|
|
|
148
163
|
// /movies should only have root layer, not (admin) layer
|
|
149
164
|
const expectedMovies = ` {
|
|
@@ -154,9 +169,7 @@ t.it("only includes group layers for routes in that group", () => {
|
|
|
154
169
|
],
|
|
155
170
|
},`
|
|
156
171
|
|
|
157
|
-
t
|
|
158
|
-
.expect(code)
|
|
159
|
-
.toContain(expectedMovies)
|
|
172
|
+
t.expect(code).toContain(expectedMovies)
|
|
160
173
|
})
|
|
161
174
|
|
|
162
175
|
t.it("handles dynamic routes with params", () => {
|
|
@@ -168,15 +181,9 @@ t.it("handles dynamic routes with params", () => {
|
|
|
168
181
|
|
|
169
182
|
const code = FileRouterCodegen.generateCode(handles)
|
|
170
183
|
|
|
171
|
-
t
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
t
|
|
175
|
-
.expect(code)
|
|
176
|
-
.toContain("path: \"/users/[userId]\"")
|
|
177
|
-
t
|
|
178
|
-
.expect(code)
|
|
179
|
-
.toContain("path: \"/posts/[postId]/comments/[commentId]\"")
|
|
184
|
+
t.expect(code).toContain("path: \"/users\"")
|
|
185
|
+
t.expect(code).toContain("path: \"/users/[userId]\"")
|
|
186
|
+
t.expect(code).toContain("path: \"/posts/[postId]/comments/[commentId]\"")
|
|
180
187
|
})
|
|
181
188
|
|
|
182
189
|
t.it("handles rest parameters", () => {
|
|
@@ -187,12 +194,8 @@ t.it("handles rest parameters", () => {
|
|
|
187
194
|
|
|
188
195
|
const code = FileRouterCodegen.generateCode(handles)
|
|
189
196
|
|
|
190
|
-
t
|
|
191
|
-
|
|
192
|
-
.toContain("path: \"/docs/[[...slug]]\"")
|
|
193
|
-
t
|
|
194
|
-
.expect(code)
|
|
195
|
-
.toContain("path: \"/api/[...path]\"")
|
|
197
|
+
t.expect(code).toContain("path: \"/docs/[[...slug]]\"")
|
|
198
|
+
t.expect(code).toContain("path: \"/api/[...path]\"")
|
|
196
199
|
})
|
|
197
200
|
|
|
198
201
|
t.it("handles groups in path", () => {
|
|
@@ -203,12 +206,10 @@ t.it("handles groups in path", () => {
|
|
|
203
206
|
|
|
204
207
|
const code = FileRouterCodegen.generateCode(handles)
|
|
205
208
|
|
|
206
|
-
t
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
.expect(code)
|
|
211
|
-
.toContain("layers: [\n () => import(\"./(admin)/layer.tsx\"),\n ]")
|
|
209
|
+
t.expect(code).toContain("path: \"/users\"") // groups stripped from URL
|
|
210
|
+
t.expect(code).toContain(
|
|
211
|
+
"layers: [\n () => import(\"./(admin)/layer.tsx\"),\n ]",
|
|
212
|
+
)
|
|
212
213
|
})
|
|
213
214
|
|
|
214
215
|
t.it("generates correct variable names for root routes", () => {
|
|
@@ -218,9 +219,7 @@ t.it("generates correct variable names for root routes", () => {
|
|
|
218
219
|
|
|
219
220
|
const code = FileRouterCodegen.generateCode(handles)
|
|
220
221
|
|
|
221
|
-
t
|
|
222
|
-
.expect(code)
|
|
223
|
-
.toContain("path: \"/\"")
|
|
222
|
+
t.expect(code).toContain("path: \"/\"")
|
|
224
223
|
})
|
|
225
224
|
|
|
226
225
|
t.it("handles routes with dots in path segments", () => {
|
|
@@ -231,12 +230,8 @@ t.it("handles routes with dots in path segments", () => {
|
|
|
231
230
|
|
|
232
231
|
const code = FileRouterCodegen.generateCode(handles)
|
|
233
232
|
|
|
234
|
-
t
|
|
235
|
-
|
|
236
|
-
.toContain("path: \"/events.json\"")
|
|
237
|
-
t
|
|
238
|
-
.expect(code)
|
|
239
|
-
.toContain("path: \"/config.yaml.backup\"")
|
|
233
|
+
t.expect(code).toContain("path: \"/events.json\"")
|
|
234
|
+
t.expect(code).toContain("path: \"/config.yaml.backup\"")
|
|
240
235
|
})
|
|
241
236
|
|
|
242
237
|
t.it("uses default module identifier", () => {
|
|
@@ -246,9 +241,7 @@ t.it("uses default module identifier", () => {
|
|
|
246
241
|
|
|
247
242
|
const code = FileRouterCodegen.generateCode(handles)
|
|
248
243
|
|
|
249
|
-
t
|
|
250
|
-
.expect(code)
|
|
251
|
-
.toContain("import type { Router } from \"effect-start\"")
|
|
244
|
+
t.expect(code).toContain("import type { Router } from \"effect-start\"")
|
|
252
245
|
})
|
|
253
246
|
|
|
254
247
|
t.it("generates empty routes array when no handles provided", () => {
|
|
@@ -256,9 +249,7 @@ t.it("generates empty routes array when no handles provided", () => {
|
|
|
256
249
|
|
|
257
250
|
const code = FileRouterCodegen.generateCode(handles)
|
|
258
251
|
|
|
259
|
-
t
|
|
260
|
-
.expect(code)
|
|
261
|
-
.toContain("export const routes = [] as const")
|
|
252
|
+
t.expect(code).toContain("export const routes = [] as const")
|
|
262
253
|
})
|
|
263
254
|
|
|
264
255
|
t.it("only includes routes, not layers", () => {
|
|
@@ -269,9 +260,7 @@ t.it("only includes routes, not layers", () => {
|
|
|
269
260
|
|
|
270
261
|
const code = FileRouterCodegen.generateCode(handles)
|
|
271
262
|
|
|
272
|
-
t
|
|
273
|
-
.expect(code)
|
|
274
|
-
.toContain("export const routes = [] as const")
|
|
263
|
+
t.expect(code).toContain("export const routes = [] as const")
|
|
275
264
|
})
|
|
276
265
|
|
|
277
266
|
t.it("complex nested routes with multiple layers", () => {
|
|
@@ -287,29 +276,15 @@ t.it("complex nested routes with multiple layers", () => {
|
|
|
287
276
|
|
|
288
277
|
const code = FileRouterCodegen.generateCode(handles)
|
|
289
278
|
|
|
290
|
-
t
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
t
|
|
294
|
-
.expect(code)
|
|
295
|
-
.toContain("path: \"/signup\"") // group stripped
|
|
296
|
-
t
|
|
297
|
-
.expect(code)
|
|
298
|
-
.toContain("path: \"/dashboard\"")
|
|
299
|
-
t
|
|
300
|
-
.expect(code)
|
|
301
|
-
.toContain("path: \"/dashboard/settings\"")
|
|
279
|
+
t.expect(code).toContain("path: \"/login\"") // group stripped
|
|
280
|
+
t.expect(code).toContain("path: \"/signup\"") // group stripped
|
|
281
|
+
t.expect(code).toContain("path: \"/dashboard\"")
|
|
282
|
+
t.expect(code).toContain("path: \"/dashboard/settings\"")
|
|
302
283
|
|
|
303
284
|
// Check layers are properly inherited
|
|
304
|
-
t
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
t
|
|
308
|
-
.expect(code)
|
|
309
|
-
.toContain("() => import(\"./(auth)/layer.tsx\")")
|
|
310
|
-
t
|
|
311
|
-
.expect(code)
|
|
312
|
-
.toContain("() => import(\"./dashboard/layer.tsx\")")
|
|
285
|
+
t.expect(code).toContain("() => import(\"./layer.tsx\")")
|
|
286
|
+
t.expect(code).toContain("() => import(\"./(auth)/layer.tsx\")")
|
|
287
|
+
t.expect(code).toContain("() => import(\"./dashboard/layer.tsx\")")
|
|
313
288
|
})
|
|
314
289
|
|
|
315
290
|
t.it("handles routes with hyphens and underscores in path segments", () => {
|
|
@@ -320,78 +295,52 @@ t.it("handles routes with hyphens and underscores in path segments", () => {
|
|
|
320
295
|
|
|
321
296
|
const code = FileRouterCodegen.generateCode(handles)
|
|
322
297
|
|
|
323
|
-
t
|
|
324
|
-
|
|
325
|
-
.toContain("path: \"/api-v1\"")
|
|
326
|
-
t
|
|
327
|
-
.expect(code)
|
|
328
|
-
.toContain("path: \"/my_resource\"")
|
|
298
|
+
t.expect(code).toContain("path: \"/api-v1\"")
|
|
299
|
+
t.expect(code).toContain("path: \"/my_resource\"")
|
|
329
300
|
})
|
|
330
301
|
|
|
331
302
|
t.it("validateRouteModule returns true for valid modules", () => {
|
|
332
303
|
const validRoute = Route.text("Hello")
|
|
333
304
|
|
|
334
305
|
t
|
|
335
|
-
.expect(
|
|
336
|
-
FileRouterCodegen.validateRouteModule({ default: validRoute }),
|
|
337
|
-
)
|
|
306
|
+
.expect(FileRouterCodegen.validateRouteModule({ default: validRoute }))
|
|
338
307
|
.toBe(true)
|
|
339
308
|
|
|
340
309
|
t
|
|
341
|
-
.expect(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}),
|
|
345
|
-
)
|
|
310
|
+
.expect(FileRouterCodegen.validateRouteModule({
|
|
311
|
+
default: Route.html(Effect.succeed("<div>Hello</div>")),
|
|
312
|
+
}))
|
|
346
313
|
.toBe(true)
|
|
347
314
|
|
|
348
315
|
t
|
|
349
|
-
.expect(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}),
|
|
353
|
-
)
|
|
316
|
+
.expect(FileRouterCodegen.validateRouteModule({
|
|
317
|
+
default: Route.json({ message: "Hello" }),
|
|
318
|
+
}))
|
|
354
319
|
.toBe(true)
|
|
355
320
|
})
|
|
356
321
|
|
|
357
322
|
t.it("validateRouteModule returns false for invalid modules", () => {
|
|
358
|
-
t
|
|
359
|
-
.expect(FileRouterCodegen.validateRouteModule({}))
|
|
360
|
-
.toBe(false)
|
|
323
|
+
t.expect(FileRouterCodegen.validateRouteModule({})).toBe(false)
|
|
361
324
|
|
|
362
325
|
t
|
|
363
|
-
.expect(
|
|
364
|
-
FileRouterCodegen.validateRouteModule({ default: {} }),
|
|
365
|
-
)
|
|
326
|
+
.expect(FileRouterCodegen.validateRouteModule({ default: {} }))
|
|
366
327
|
.toBe(false)
|
|
367
328
|
|
|
368
329
|
t
|
|
369
|
-
.expect(
|
|
370
|
-
FileRouterCodegen.validateRouteModule({ default: "not a route" }),
|
|
371
|
-
)
|
|
330
|
+
.expect(FileRouterCodegen.validateRouteModule({ default: "not a route" }))
|
|
372
331
|
.toBe(false)
|
|
373
332
|
|
|
374
333
|
t
|
|
375
|
-
.expect(
|
|
376
|
-
FileRouterCodegen.validateRouteModule({ foo: "bar" }),
|
|
377
|
-
)
|
|
334
|
+
.expect(FileRouterCodegen.validateRouteModule({ foo: "bar" }))
|
|
378
335
|
.toBe(false)
|
|
379
336
|
|
|
380
|
-
t
|
|
381
|
-
.expect(FileRouterCodegen.validateRouteModule(null))
|
|
382
|
-
.toBe(false)
|
|
337
|
+
t.expect(FileRouterCodegen.validateRouteModule(null)).toBe(false)
|
|
383
338
|
|
|
384
|
-
t
|
|
385
|
-
.expect(FileRouterCodegen.validateRouteModule(undefined))
|
|
386
|
-
.toBe(false)
|
|
339
|
+
t.expect(FileRouterCodegen.validateRouteModule(undefined)).toBe(false)
|
|
387
340
|
|
|
388
|
-
t
|
|
389
|
-
.expect(FileRouterCodegen.validateRouteModule("string"))
|
|
390
|
-
.toBe(false)
|
|
341
|
+
t.expect(FileRouterCodegen.validateRouteModule("string")).toBe(false)
|
|
391
342
|
|
|
392
|
-
t
|
|
393
|
-
.expect(FileRouterCodegen.validateRouteModule(42))
|
|
394
|
-
.toBe(false)
|
|
343
|
+
t.expect(FileRouterCodegen.validateRouteModule(42)).toBe(false)
|
|
395
344
|
})
|
|
396
345
|
|
|
397
346
|
t.it("mixed params and rest in same route", () => {
|
|
@@ -401,9 +350,7 @@ t.it("mixed params and rest in same route", () => {
|
|
|
401
350
|
|
|
402
351
|
const code = FileRouterCodegen.generateCode(handles)
|
|
403
352
|
|
|
404
|
-
t
|
|
405
|
-
.expect(code)
|
|
406
|
-
.toContain("path: \"/users/[userId]/files/[...path]\"")
|
|
353
|
+
t.expect(code).toContain("path: \"/users/[userId]/files/[...path]\"")
|
|
407
354
|
})
|
|
408
355
|
|
|
409
356
|
t.describe("layerMatchesRoute", () => {
|
|
@@ -424,18 +371,14 @@ t.describe("layerMatchesRoute", () => {
|
|
|
424
371
|
],
|
|
425
372
|
},`
|
|
426
373
|
|
|
427
|
-
t
|
|
428
|
-
.expect(code)
|
|
429
|
-
.toContain(expectedUserIdPosts)
|
|
374
|
+
t.expect(code).toContain(expectedUserIdPosts)
|
|
430
375
|
|
|
431
376
|
const expectedOtherId = ` {
|
|
432
377
|
path: "/[otherId]",
|
|
433
378
|
load: () => import("./[otherId]/route.tsx"),
|
|
434
379
|
},`
|
|
435
380
|
|
|
436
|
-
t
|
|
437
|
-
.expect(code)
|
|
438
|
-
.toContain(expectedOtherId)
|
|
381
|
+
t.expect(code).toContain(expectedOtherId)
|
|
439
382
|
})
|
|
440
383
|
|
|
441
384
|
t.it("nested groups only apply to routes in those groups", () => {
|
|
@@ -458,9 +401,7 @@ t.describe("layerMatchesRoute", () => {
|
|
|
458
401
|
],
|
|
459
402
|
},`
|
|
460
403
|
|
|
461
|
-
t
|
|
462
|
-
.expect(code)
|
|
463
|
-
.toContain(expectedAdminDashboardUsers)
|
|
404
|
+
t.expect(code).toContain(expectedAdminDashboardUsers)
|
|
464
405
|
|
|
465
406
|
const expectedAdminSettings = ` {
|
|
466
407
|
path: "/settings",
|
|
@@ -470,9 +411,7 @@ t.describe("layerMatchesRoute", () => {
|
|
|
470
411
|
],
|
|
471
412
|
},`
|
|
472
413
|
|
|
473
|
-
t
|
|
474
|
-
.expect(code)
|
|
475
|
-
.toContain(expectedAdminSettings)
|
|
414
|
+
t.expect(code).toContain(expectedAdminSettings)
|
|
476
415
|
|
|
477
416
|
const expectedOtherDashboard = ` {
|
|
478
417
|
path: "/",
|
|
@@ -482,9 +421,7 @@ t.describe("layerMatchesRoute", () => {
|
|
|
482
421
|
],
|
|
483
422
|
},`
|
|
484
423
|
|
|
485
|
-
t
|
|
486
|
-
.expect(code)
|
|
487
|
-
.toContain(expectedOtherDashboard)
|
|
424
|
+
t.expect(code).toContain(expectedOtherDashboard)
|
|
488
425
|
})
|
|
489
426
|
|
|
490
427
|
t.it("similar directory names do not match (user vs users)", () => {
|
|
@@ -504,18 +441,14 @@ t.describe("layerMatchesRoute", () => {
|
|
|
504
441
|
],
|
|
505
442
|
},`
|
|
506
443
|
|
|
507
|
-
t
|
|
508
|
-
.expect(code)
|
|
509
|
-
.toContain(expectedUser)
|
|
444
|
+
t.expect(code).toContain(expectedUser)
|
|
510
445
|
|
|
511
446
|
const expectedUsers = ` {
|
|
512
447
|
path: "/users",
|
|
513
448
|
load: () => import("./users/route.tsx"),
|
|
514
449
|
},`
|
|
515
450
|
|
|
516
|
-
t
|
|
517
|
-
.expect(code)
|
|
518
|
-
.toContain(expectedUsers)
|
|
451
|
+
t.expect(code).toContain(expectedUsers)
|
|
519
452
|
})
|
|
520
453
|
|
|
521
454
|
t.it("mixed groups and literals layer matching", () => {
|
|
@@ -536,27 +469,21 @@ t.describe("layerMatchesRoute", () => {
|
|
|
536
469
|
],
|
|
537
470
|
},`
|
|
538
471
|
|
|
539
|
-
t
|
|
540
|
-
.expect(code)
|
|
541
|
-
.toContain(expectedAdminUsersId)
|
|
472
|
+
t.expect(code).toContain(expectedAdminUsersId)
|
|
542
473
|
|
|
543
474
|
const expectedUsers = ` {
|
|
544
475
|
path: "/users",
|
|
545
476
|
load: () => import("./users/route.tsx"),
|
|
546
477
|
},`
|
|
547
478
|
|
|
548
|
-
t
|
|
549
|
-
.expect(code)
|
|
550
|
-
.toContain(expectedUsers)
|
|
479
|
+
t.expect(code).toContain(expectedUsers)
|
|
551
480
|
|
|
552
481
|
const expectedAdminPosts = ` {
|
|
553
482
|
path: "/posts",
|
|
554
483
|
load: () => import("./(admin)/posts/route.tsx"),
|
|
555
484
|
},`
|
|
556
485
|
|
|
557
|
-
t
|
|
558
|
-
.expect(code)
|
|
559
|
-
.toContain(expectedAdminPosts)
|
|
486
|
+
t.expect(code).toContain(expectedAdminPosts)
|
|
560
487
|
})
|
|
561
488
|
|
|
562
489
|
t.it("param directory layer only applies to routes in that dir", () => {
|
|
@@ -576,18 +503,14 @@ t.describe("layerMatchesRoute", () => {
|
|
|
576
503
|
],
|
|
577
504
|
},`
|
|
578
505
|
|
|
579
|
-
t
|
|
580
|
-
.expect(code)
|
|
581
|
-
.toContain(expectedTenantSettings)
|
|
506
|
+
t.expect(code).toContain(expectedTenantSettings)
|
|
582
507
|
|
|
583
508
|
const expectedOther = ` {
|
|
584
509
|
path: "/other",
|
|
585
510
|
load: () => import("./other/route.tsx"),
|
|
586
511
|
},`
|
|
587
512
|
|
|
588
|
-
t
|
|
589
|
-
.expect(code)
|
|
590
|
-
.toContain(expectedOther)
|
|
513
|
+
t.expect(code).toContain(expectedOther)
|
|
591
514
|
})
|
|
592
515
|
|
|
593
516
|
t.it(
|
|
@@ -609,18 +532,14 @@ t.describe("layerMatchesRoute", () => {
|
|
|
609
532
|
],
|
|
610
533
|
},`
|
|
611
534
|
|
|
612
|
-
t
|
|
613
|
-
.expect(code)
|
|
614
|
-
.toContain(expectedIdSettings)
|
|
535
|
+
t.expect(code).toContain(expectedIdSettings)
|
|
615
536
|
|
|
616
537
|
const expectedOther = ` {
|
|
617
538
|
path: "/other",
|
|
618
539
|
load: () => import("./other/route.tsx"),
|
|
619
540
|
},`
|
|
620
541
|
|
|
621
|
-
t
|
|
622
|
-
.expect(code)
|
|
623
|
-
.toContain(expectedOther)
|
|
542
|
+
t.expect(code).toContain(expectedOther)
|
|
624
543
|
},
|
|
625
544
|
)
|
|
626
545
|
|
|
@@ -640,145 +559,410 @@ t.describe("layerMatchesRoute", () => {
|
|
|
640
559
|
],
|
|
641
560
|
},`
|
|
642
561
|
|
|
643
|
-
t
|
|
644
|
-
.expect(code)
|
|
645
|
-
.toContain(expected)
|
|
562
|
+
t.expect(code).toContain(expected)
|
|
646
563
|
})
|
|
647
564
|
})
|
|
648
565
|
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
"/routes/_manifest.ts": "",
|
|
655
|
-
}
|
|
566
|
+
const simpleRouteContent = `import * as Route from "${
|
|
567
|
+
path.resolve(import.meta.dirname, "./Route.ts")
|
|
568
|
+
}"
|
|
569
|
+
export default Route.text("Hello")
|
|
570
|
+
`
|
|
656
571
|
|
|
657
572
|
t.it("update() > writes file", () =>
|
|
658
573
|
Effect
|
|
659
574
|
.gen(function*() {
|
|
660
|
-
yield* FileRouterCodegen.update("/routes")
|
|
661
|
-
|
|
662
575
|
const fs = yield* FileSystem.FileSystem
|
|
663
|
-
const
|
|
576
|
+
const tempDir = yield* createTempDirWithFiles({
|
|
577
|
+
"routes/route.tsx": simpleRouteContent,
|
|
578
|
+
"routes/about/route.tsx": simpleRouteContent,
|
|
579
|
+
})
|
|
580
|
+
const routesPath = path.join(tempDir, "routes")
|
|
581
|
+
|
|
582
|
+
yield* FileRouterCodegen.update(routesPath)
|
|
664
583
|
|
|
665
|
-
|
|
666
|
-
.
|
|
667
|
-
|
|
584
|
+
const content = yield* fs.readFileString(
|
|
585
|
+
path.join(routesPath, "manifest.ts"),
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
t.expect(content).toContain("export const routes =")
|
|
668
589
|
})
|
|
669
590
|
.pipe(
|
|
670
|
-
Effect.
|
|
591
|
+
Effect.scoped,
|
|
592
|
+
Effect.provide(NodeFileSystem.layer),
|
|
671
593
|
Effect.runPromise,
|
|
672
594
|
))
|
|
673
595
|
|
|
674
596
|
t.it("update() > writes only when it changes", () =>
|
|
675
597
|
Effect
|
|
676
598
|
.gen(function*() {
|
|
677
|
-
yield* FileRouterCodegen.update("/routes")
|
|
678
|
-
|
|
679
599
|
const fs = yield* FileSystem.FileSystem
|
|
680
|
-
const
|
|
600
|
+
const tempDir = yield* createTempDirWithFiles({
|
|
601
|
+
"routes/route.tsx": simpleRouteContent,
|
|
602
|
+
"routes/about/route.tsx": simpleRouteContent,
|
|
603
|
+
})
|
|
604
|
+
const routesPath = path.join(tempDir, "routes")
|
|
605
|
+
|
|
606
|
+
yield* FileRouterCodegen.update(routesPath)
|
|
681
607
|
|
|
682
|
-
yield*
|
|
608
|
+
const content = yield* fs.readFileString(
|
|
609
|
+
path.join(routesPath, "manifest.ts"),
|
|
610
|
+
)
|
|
683
611
|
|
|
684
|
-
|
|
612
|
+
yield* FileRouterCodegen.update(routesPath)
|
|
685
613
|
|
|
686
|
-
|
|
687
|
-
.
|
|
688
|
-
|
|
689
|
-
.toBe("")
|
|
614
|
+
const content2 = yield* fs.readFileString(
|
|
615
|
+
path.join(routesPath, "manifest.ts"),
|
|
616
|
+
)
|
|
690
617
|
|
|
691
|
-
t
|
|
692
|
-
|
|
693
|
-
.toBe(content)
|
|
618
|
+
t.expect(content2).not.toBe("")
|
|
619
|
+
t.expect(content2).toBe(content)
|
|
694
620
|
})
|
|
695
621
|
.pipe(
|
|
696
|
-
Effect.
|
|
622
|
+
Effect.scoped,
|
|
623
|
+
Effect.provide(NodeFileSystem.layer),
|
|
697
624
|
Effect.runPromise,
|
|
698
625
|
))
|
|
699
626
|
|
|
700
|
-
t.it(
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
627
|
+
t.it(
|
|
628
|
+
"update() > removes deleted routes from manifest",
|
|
629
|
+
() =>
|
|
630
|
+
Effect
|
|
631
|
+
.gen(function*() {
|
|
632
|
+
const fs = yield* FileSystem.FileSystem
|
|
633
|
+
const tempDir = yield* createTempDirWithFiles({
|
|
634
|
+
"routes/route.tsx": simpleRouteContent,
|
|
635
|
+
"routes/about/route.tsx": simpleRouteContent,
|
|
636
|
+
})
|
|
637
|
+
const routesPath = path.join(tempDir, "routes")
|
|
638
|
+
|
|
639
|
+
yield* FileRouterCodegen.update(routesPath)
|
|
640
|
+
|
|
641
|
+
const content = yield* fs.readFileString(
|
|
642
|
+
path.join(routesPath, "manifest.ts"),
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
t.expect(content).toContain("path: \"/\"")
|
|
646
|
+
t.expect(content).toContain("path: \"/about\"")
|
|
647
|
+
|
|
648
|
+
yield* fs.remove(path.join(routesPath, "about/route.tsx"))
|
|
649
|
+
|
|
650
|
+
yield* FileRouterCodegen.update(routesPath)
|
|
651
|
+
|
|
652
|
+
const content2 = yield* fs.readFileString(
|
|
653
|
+
path.join(routesPath, "manifest.ts"),
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
t.expect(content2).toContain("path: \"/\"")
|
|
657
|
+
t.expect(content2).not.toContain("path: \"/about\"")
|
|
658
|
+
})
|
|
659
|
+
.pipe(
|
|
660
|
+
Effect.scoped,
|
|
661
|
+
Effect.provide(NodeFileSystem.layer),
|
|
662
|
+
Effect.runPromise,
|
|
663
|
+
),
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
t.it(
|
|
667
|
+
"update() > removes routes when entire directory is deleted",
|
|
668
|
+
() =>
|
|
669
|
+
Effect
|
|
670
|
+
.gen(function*() {
|
|
671
|
+
const fs = yield* FileSystem.FileSystem
|
|
672
|
+
const tempDir = yield* createTempDirWithFiles({
|
|
673
|
+
"routes/route.tsx": simpleRouteContent,
|
|
674
|
+
"routes/about/route.tsx": simpleRouteContent,
|
|
675
|
+
"routes/users/route.tsx": simpleRouteContent,
|
|
676
|
+
})
|
|
677
|
+
const routesPath = path.join(tempDir, "routes")
|
|
678
|
+
|
|
679
|
+
yield* FileRouterCodegen.update(routesPath)
|
|
680
|
+
|
|
681
|
+
const content = yield* fs.readFileString(
|
|
682
|
+
path.join(routesPath, "manifest.ts"),
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
t.expect(content).toContain("path: \"/\"")
|
|
686
|
+
t.expect(content).toContain("path: \"/about\"")
|
|
687
|
+
t.expect(content).toContain("path: \"/users\"")
|
|
688
|
+
|
|
689
|
+
yield* fs.remove(path.join(routesPath, "users"), {
|
|
690
|
+
recursive: true,
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
yield* FileRouterCodegen.update(routesPath)
|
|
694
|
+
|
|
695
|
+
const content2 = yield* fs.readFileString(
|
|
696
|
+
path.join(routesPath, "manifest.ts"),
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
t.expect(content2).toContain("path: \"/\"")
|
|
700
|
+
t.expect(content2).toContain("path: \"/about\"")
|
|
701
|
+
t.expect(content2).not.toContain("path: \"/users\"")
|
|
702
|
+
})
|
|
703
|
+
.pipe(
|
|
704
|
+
Effect.scoped,
|
|
705
|
+
Effect.provide(NodeFileSystem.layer),
|
|
706
|
+
Effect.runPromise,
|
|
707
|
+
),
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
t.describe("PathParams schema generation and validation", () => {
|
|
711
|
+
t.describe("generatePathParamsSchema", () => {
|
|
712
|
+
t.it("returns null for routes with no params", () => {
|
|
713
|
+
const handle = parseRoute("users/route.tsx")
|
|
714
|
+
const schema = FileRouterCodegen.generatePathParamsSchema(handle.segments)
|
|
715
|
+
t.expect(schema).toBe(null)
|
|
716
|
+
})
|
|
706
717
|
|
|
707
|
-
|
|
718
|
+
t.it("generates schema for single required param", () => {
|
|
719
|
+
const handle = parseRoute("users/[id]/route.tsx")
|
|
720
|
+
const schema = FileRouterCodegen.generatePathParamsSchema(handle.segments)
|
|
721
|
+
t.expect(schema).not.toBe(null)
|
|
722
|
+
t.expect(Object.keys(schema!.fields)).toEqual(["id"])
|
|
723
|
+
})
|
|
708
724
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
725
|
+
t.it("generates schema for single optional param", () => {
|
|
726
|
+
const handle = parseRoute("about/[[section]]/route.tsx")
|
|
727
|
+
const schema = FileRouterCodegen.generatePathParamsSchema(handle.segments)
|
|
728
|
+
t.expect(schema).not.toBe(null)
|
|
729
|
+
t.expect(Object.keys(schema!.fields)).toEqual(["section"])
|
|
730
|
+
})
|
|
712
731
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
732
|
+
t.it("generates schema for rest segment", () => {
|
|
733
|
+
const handle = parseRoute("docs/[...path]/route.tsx")
|
|
734
|
+
const schema = FileRouterCodegen.generatePathParamsSchema(handle.segments)
|
|
735
|
+
t.expect(schema).not.toBe(null)
|
|
736
|
+
t.expect(Object.keys(schema!.fields)).toEqual(["path"])
|
|
737
|
+
})
|
|
716
738
|
|
|
717
|
-
|
|
739
|
+
t.it("rest segment should capture path starting with /", () => {
|
|
740
|
+
const handle = parseRoute("docs/[...path]/route.tsx")
|
|
741
|
+
const schema = FileRouterCodegen.generatePathParamsSchema(handle.segments)
|
|
742
|
+
t.expect(schema).not.toBe(null)
|
|
718
743
|
|
|
719
|
-
|
|
744
|
+
// Rest segments capture remaining path as string
|
|
745
|
+
// For route /docs/[...path] matching /docs/guide/getting-started
|
|
746
|
+
// The path param should be: "/guide/getting-started" (with leading /)
|
|
747
|
+
const formatted = SchemaExtra.formatSchemaCode(schema!)
|
|
748
|
+
t.expect(formatted).toBe("{ path: Schema.String }")
|
|
749
|
+
})
|
|
720
750
|
|
|
721
|
-
|
|
751
|
+
t.it("generates schema for optional rest segment", () => {
|
|
752
|
+
const handle = parseRoute("docs/[[...slug]]/route.tsx")
|
|
753
|
+
const schema = FileRouterCodegen.generatePathParamsSchema(handle.segments)
|
|
754
|
+
t.expect(schema).not.toBe(null)
|
|
755
|
+
t.expect(Object.keys(schema!.fields)).toEqual(["slug"])
|
|
756
|
+
})
|
|
722
757
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
758
|
+
t.it("generates schema for multiple params", () => {
|
|
759
|
+
const handle = parseRoute("posts/[postId]/comments/[commentId]/route.tsx")
|
|
760
|
+
const schema = FileRouterCodegen.generatePathParamsSchema(handle.segments)
|
|
761
|
+
t.expect(schema).not.toBe(null)
|
|
762
|
+
t.expect(Object.keys(schema!.fields).sort()).toEqual([
|
|
763
|
+
"commentId",
|
|
764
|
+
"postId",
|
|
765
|
+
])
|
|
766
|
+
})
|
|
726
767
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
768
|
+
t.it("generates schema for mixed required and optional params", () => {
|
|
769
|
+
const handle = parseRoute("users/[userId]/posts/[[postId]]/route.tsx")
|
|
770
|
+
const schema = FileRouterCodegen.generatePathParamsSchema(handle.segments)
|
|
771
|
+
t.expect(schema).not.toBe(null)
|
|
772
|
+
t.expect(Object.keys(schema!.fields).sort()).toEqual(["postId", "userId"])
|
|
731
773
|
})
|
|
732
|
-
.pipe(
|
|
733
|
-
Effect.provide(MemoryFileSystem.layerWith(update_FilesWithRoutes)),
|
|
734
|
-
Effect.runPromise,
|
|
735
|
-
))
|
|
736
774
|
|
|
737
|
-
t.it("
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
775
|
+
t.it("ignores group segments", () => {
|
|
776
|
+
const handle = parseRoute("(admin)/users/[id]/route.tsx")
|
|
777
|
+
const schema = FileRouterCodegen.generatePathParamsSchema(handle.segments)
|
|
778
|
+
t.expect(schema).not.toBe(null)
|
|
779
|
+
t.expect(Object.keys(schema!.fields)).toEqual(["id"])
|
|
780
|
+
})
|
|
781
|
+
})
|
|
741
782
|
|
|
742
|
-
|
|
783
|
+
t.describe("schemaEqual", () => {
|
|
784
|
+
t.it("returns true when both schemas are undefined/null", () => {
|
|
785
|
+
t.expect(SchemaExtra.schemaEqual(undefined, null)).toBe(true)
|
|
786
|
+
})
|
|
743
787
|
|
|
744
|
-
|
|
788
|
+
t.it("returns false when only one schema is undefined", () => {
|
|
789
|
+
const schema = Schema.Struct({ id: Schema.String })
|
|
790
|
+
t.expect(SchemaExtra.schemaEqual(undefined, schema)).toBe(false)
|
|
791
|
+
t.expect(SchemaExtra.schemaEqual(schema, null)).toBe(false)
|
|
792
|
+
})
|
|
745
793
|
|
|
746
|
-
|
|
794
|
+
t.it("returns true for exact matches", () => {
|
|
795
|
+
const schema1 = Schema.Struct({ id: Schema.String })
|
|
796
|
+
const schema2 = Schema.Struct({ id: Schema.String })
|
|
797
|
+
t.expect(SchemaExtra.schemaEqual(schema1, schema2)).toBe(true)
|
|
798
|
+
})
|
|
747
799
|
|
|
748
|
-
|
|
800
|
+
t.it("returns true for refinement matches (UUID = String)", () => {
|
|
801
|
+
const userSchema = Schema.Struct({ id: Schema.UUID })
|
|
802
|
+
const expectedSchema = Schema.Struct({ id: Schema.String })
|
|
803
|
+
t.expect(SchemaExtra.schemaEqual(userSchema, expectedSchema)).toBe(true)
|
|
804
|
+
})
|
|
749
805
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
806
|
+
t.it("returns false for type mismatches", () => {
|
|
807
|
+
const schema1 = Schema.Struct({ id: Schema.String })
|
|
808
|
+
const schema2 = Schema.Struct({ id: Schema.Number })
|
|
809
|
+
t.expect(SchemaExtra.schemaEqual(schema1, schema2)).toBe(false)
|
|
810
|
+
})
|
|
753
811
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
812
|
+
t.it("returns false for field name mismatches", () => {
|
|
813
|
+
const schema1 = Schema.Struct({ id: Schema.String })
|
|
814
|
+
const schema2 = Schema.Struct({ userId: Schema.String })
|
|
815
|
+
t.expect(SchemaExtra.schemaEqual(schema1, schema2)).toBe(false)
|
|
816
|
+
})
|
|
757
817
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
818
|
+
t.it("returns false for field count mismatches", () => {
|
|
819
|
+
const schema1 = Schema.Struct({ id: Schema.String })
|
|
820
|
+
const schema2 = Schema.Struct({ id: Schema.String, name: Schema.String })
|
|
821
|
+
t.expect(SchemaExtra.schemaEqual(schema1, schema2)).toBe(false)
|
|
822
|
+
})
|
|
761
823
|
|
|
762
|
-
|
|
824
|
+
t.it("returns true for multiple fields with UUID refinement", () => {
|
|
825
|
+
const userSchema = Schema.Struct({
|
|
826
|
+
id: Schema.UUID,
|
|
827
|
+
name: Schema.String,
|
|
828
|
+
})
|
|
829
|
+
const expectedSchema = Schema.Struct({
|
|
830
|
+
id: Schema.String,
|
|
831
|
+
name: Schema.String,
|
|
832
|
+
})
|
|
833
|
+
t.expect(SchemaExtra.schemaEqual(userSchema, expectedSchema)).toBe(true)
|
|
834
|
+
})
|
|
763
835
|
|
|
764
|
-
|
|
836
|
+
t.it("handles optional fields correctly", () => {
|
|
837
|
+
const schema1 = Schema.Struct({
|
|
838
|
+
id: Schema.String,
|
|
839
|
+
name: Schema.optional(Schema.String),
|
|
840
|
+
})
|
|
841
|
+
const schema2 = Schema.Struct({
|
|
842
|
+
id: Schema.String,
|
|
843
|
+
name: Schema.optional(Schema.String),
|
|
844
|
+
})
|
|
845
|
+
t.expect(SchemaExtra.schemaEqual(schema1, schema2)).toBe(true)
|
|
846
|
+
})
|
|
847
|
+
})
|
|
765
848
|
|
|
766
|
-
|
|
849
|
+
t.describe("formatSchemaCode", () => {
|
|
850
|
+
t.it("formats single required field", () => {
|
|
851
|
+
const schema = Schema.Struct({ id: Schema.String })
|
|
852
|
+
const formatted = SchemaExtra.formatSchemaCode(schema)
|
|
853
|
+
t.expect(formatted).toBe("{ id: Schema.String }")
|
|
854
|
+
})
|
|
767
855
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
856
|
+
t.it("formats multiple fields", () => {
|
|
857
|
+
const schema = Schema.Struct({
|
|
858
|
+
id: Schema.String,
|
|
859
|
+
count: Schema.Number,
|
|
860
|
+
})
|
|
861
|
+
const formatted = SchemaExtra.formatSchemaCode(schema)
|
|
862
|
+
t.expect(formatted).toContain("id: Schema.String")
|
|
863
|
+
t.expect(formatted).toContain("count: Schema.Number")
|
|
864
|
+
})
|
|
771
865
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
866
|
+
t.it("formats optional fields with ? marker", () => {
|
|
867
|
+
const schema = Schema.Struct({
|
|
868
|
+
id: Schema.String,
|
|
869
|
+
name: Schema.optional(Schema.String),
|
|
870
|
+
})
|
|
871
|
+
const formatted = SchemaExtra.formatSchemaCode(schema)
|
|
872
|
+
t.expect(formatted).toContain("id: Schema.String")
|
|
873
|
+
t.expect(formatted).toContain("name")
|
|
874
|
+
})
|
|
775
875
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
.
|
|
779
|
-
|
|
876
|
+
t.it("formats boolean fields", () => {
|
|
877
|
+
const schema = Schema.Struct({
|
|
878
|
+
active: Schema.Boolean,
|
|
879
|
+
})
|
|
880
|
+
const formatted = SchemaExtra.formatSchemaCode(schema)
|
|
881
|
+
t.expect(formatted).toBe("{ active: Schema.Boolean }")
|
|
780
882
|
})
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
t.describe("validateRouteModules", () => {
|
|
886
|
+
t.it(
|
|
887
|
+
"does not log when PathParams schema is missing",
|
|
888
|
+
() =>
|
|
889
|
+
Effect
|
|
890
|
+
.gen(function*() {
|
|
891
|
+
const fs = yield* FileSystem.FileSystem
|
|
892
|
+
const routeContent = `import * as Route from "${
|
|
893
|
+
path.resolve(import.meta.dirname, "./Route.ts")
|
|
894
|
+
}"
|
|
895
|
+
export default Route.text("User")
|
|
896
|
+
`
|
|
897
|
+
const tempDir = yield* createTempDirWithFiles({
|
|
898
|
+
"routes/users/[id]/route.tsx": routeContent,
|
|
899
|
+
})
|
|
900
|
+
const routesPath = path.join(tempDir, "routes")
|
|
901
|
+
|
|
902
|
+
const files = yield* fs.readDirectory(routesPath, {
|
|
903
|
+
recursive: true,
|
|
904
|
+
})
|
|
905
|
+
const handles = FileRouter.getRouteHandlesFromPaths(files)
|
|
906
|
+
|
|
907
|
+
yield* FileRouterCodegen.validateRouteModules(routesPath, handles)
|
|
908
|
+
|
|
909
|
+
// Verify no logs were created
|
|
910
|
+
const messages = yield* TestLogger.messages
|
|
911
|
+
t.expect(messages).toHaveLength(0)
|
|
912
|
+
})
|
|
913
|
+
.pipe(
|
|
914
|
+
Effect.scoped,
|
|
915
|
+
Effect.provide([
|
|
916
|
+
TestLogger.layer(),
|
|
917
|
+
NodeFileSystem.layer,
|
|
918
|
+
]),
|
|
919
|
+
Effect.runPromise,
|
|
920
|
+
),
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
t.it(
|
|
924
|
+
"logs error when PathParams schema is incorrect",
|
|
925
|
+
() =>
|
|
926
|
+
Effect
|
|
927
|
+
.gen(function*() {
|
|
928
|
+
const fs = yield* FileSystem.FileSystem
|
|
929
|
+
const schemaPath = path.resolve(
|
|
930
|
+
import.meta.dirname,
|
|
931
|
+
"../node_modules/effect/Schema",
|
|
932
|
+
)
|
|
933
|
+
const routeContent = `import * as Route from "${
|
|
934
|
+
path.resolve(import.meta.dirname, "./Route.ts")
|
|
935
|
+
}"
|
|
936
|
+
import * as Schema from "${schemaPath}"
|
|
937
|
+
export default Route.text("User").schemaPathParams({ userId: Schema.String })
|
|
938
|
+
`
|
|
939
|
+
const tempDir = yield* createTempDirWithFiles({
|
|
940
|
+
"routes/users/[id]/route.tsx": routeContent,
|
|
941
|
+
})
|
|
942
|
+
const routesPath = path.join(tempDir, "routes")
|
|
943
|
+
|
|
944
|
+
const files = yield* fs.readDirectory(routesPath, {
|
|
945
|
+
recursive: true,
|
|
946
|
+
})
|
|
947
|
+
const handles = FileRouter.getRouteHandlesFromPaths(files)
|
|
948
|
+
|
|
949
|
+
yield* FileRouterCodegen.validateRouteModules(routesPath, handles)
|
|
950
|
+
|
|
951
|
+
// Verify error was logged
|
|
952
|
+
const messages = yield* TestLogger.messages
|
|
953
|
+
t.expect(messages).toHaveLength(1)
|
|
954
|
+
t.expect(messages[0]).toContain("[Error]")
|
|
955
|
+
t.expect(messages[0]).toContain("incorrect PathParams schema")
|
|
956
|
+
t.expect(messages[0]).toContain("expected schemaPathParams")
|
|
957
|
+
})
|
|
958
|
+
.pipe(
|
|
959
|
+
Effect.scoped,
|
|
960
|
+
Effect.provide([
|
|
961
|
+
TestLogger.layer(),
|
|
962
|
+
NodeFileSystem.layer,
|
|
963
|
+
]),
|
|
964
|
+
Effect.runPromise,
|
|
965
|
+
),
|
|
966
|
+
)
|
|
967
|
+
})
|
|
968
|
+
})
|