effect-start 0.9.0 → 0.10.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 (44) hide show
  1. package/package.json +12 -13
  2. package/src/BundleHttp.test.ts +1 -1
  3. package/src/Commander.test.ts +15 -15
  4. package/src/Commander.ts +58 -88
  5. package/src/EncryptedCookies.test.ts +4 -4
  6. package/src/FileHttpRouter.test.ts +81 -12
  7. package/src/FileHttpRouter.ts +115 -26
  8. package/src/FileRouter.ts +60 -162
  9. package/src/FileRouterCodegen.test.ts +250 -64
  10. package/src/FileRouterCodegen.ts +13 -56
  11. package/src/FileRouterPattern.test.ts +116 -0
  12. package/src/FileRouterPattern.ts +59 -0
  13. package/src/FileRouter_path.test.ts +63 -102
  14. package/src/FileSystemExtra.test.ts +226 -0
  15. package/src/FileSystemExtra.ts +24 -60
  16. package/src/HttpUtils.test.ts +68 -0
  17. package/src/HttpUtils.ts +15 -0
  18. package/src/HyperHtml.ts +24 -5
  19. package/src/JsModule.test.ts +1 -1
  20. package/src/NodeFileSystem.ts +764 -0
  21. package/src/Random.ts +59 -0
  22. package/src/Route.test.ts +471 -0
  23. package/src/Route.ts +298 -153
  24. package/src/RouteRender.ts +38 -0
  25. package/src/Router.ts +11 -33
  26. package/src/RouterPattern.test.ts +629 -0
  27. package/src/RouterPattern.ts +391 -0
  28. package/src/Start.ts +14 -52
  29. package/src/bun/BunBundle.test.ts +0 -3
  30. package/src/bun/BunHttpServer.ts +246 -0
  31. package/src/bun/BunHttpServer_web.ts +384 -0
  32. package/src/bun/BunRoute.test.ts +341 -0
  33. package/src/bun/BunRoute.ts +326 -0
  34. package/src/bun/BunRoute_bundles.test.ts +218 -0
  35. package/src/bun/BunRuntime.ts +33 -0
  36. package/src/bun/BunTailwindPlugin.test.ts +1 -1
  37. package/src/bun/_empty.html +1 -0
  38. package/src/bun/index.ts +2 -1
  39. package/src/testing.ts +12 -3
  40. package/src/Datastar.test.ts +0 -267
  41. package/src/Datastar.ts +0 -68
  42. package/src/bun/BunFullstackServer.ts +0 -45
  43. package/src/bun/BunFullstackServer_httpServer.ts +0 -541
  44. package/src/jsx-datastar.d.ts +0 -63
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "effect-start",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "exports": {
7
7
  ".": "./src/index.ts",
8
8
  "./Router": "./src/Router.ts",
9
9
  "./Route": "./src/Route.ts",
10
- "./Datastar": "./src/Datastar.ts",
11
10
  "./EncryptedCookies": "./src/EncryptedCookies.ts",
12
11
  "./bun": "./src/bun/index.ts",
13
12
  "./client": "./src/client/index.ts",
@@ -20,14 +19,14 @@
20
19
  },
21
20
  "scripts": {
22
21
  "format": "bunx dprint fmt",
22
+ "test": "bun test src/",
23
23
  "deploy": "bunx npm publish --access public"
24
24
  },
25
25
  "dependencies": {
26
- "@effect/platform": ">=0.82",
27
- "@effect/platform-bun": ">=0.65"
26
+ "@effect/platform": "^0.93.3"
28
27
  },
29
28
  "peerDependencies": {
30
- "typescript": "^5.8.3",
29
+ "typescript": "^5.9.3",
31
30
  "effect": ">=3.16.4"
32
31
  },
33
32
  "peerDependenciesMeta": {
@@ -39,15 +38,15 @@
39
38
  "@dprint/json": "^0.21.0",
40
39
  "@dprint/markdown": "^0.20.0",
41
40
  "@dprint/typescript": "^0.95.12",
42
- "@effect/language-service": "^0.23.3",
43
- "@tailwindcss/node": ">=4.1.8",
44
- "@types/bun": "^1.2.15",
45
- "@types/react": "^19.1.6",
46
- "@types/react-dom": "^19.1.6",
47
- "dprint-cli": "^0.4.0",
41
+ "@effect/language-service": "^0.56.0",
42
+ "@tailwindcss/node": "^4.1.17",
43
+ "@types/bun": "^1.3.3",
44
+ "@types/react": "^19.2.7",
45
+ "@types/react-dom": "^19.2.3",
46
+ "dprint-cli": "^0.4.1",
48
47
  "dprint-markup": "nounder/dprint-markup",
49
- "effect": "^3.19.4",
50
- "effect-memfs": "nounder/effect-memfs#a976bb0",
48
+ "effect": "^3.19.6",
49
+ "effect-memfs": "^0.8.0",
51
50
  "ts-namespace-import": "nounder/ts-namespace-import#140c405"
52
51
  },
53
52
  "files": [
@@ -15,7 +15,7 @@ import * as BunBundle from "./bun/BunBundle.ts"
15
15
  const effect = effectFn(
16
16
  Layer.effect(
17
17
  Bundle.ClientBundle,
18
- BunBundle.buildClient(IndexHtml),
18
+ BunBundle.buildClient(IndexHtml as any),
19
19
  ),
20
20
  )
21
21
 
@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"
3
3
  import * as Schema from "effect/Schema"
4
4
  import * as Commander from "./Commander.ts"
5
5
 
6
- t.describe("make", () => {
6
+ t.describe(`${Commander.make.name}`, () => {
7
7
  t.it("should create a basic command", () => {
8
8
  const cmd = Commander.make({
9
9
  name: "test-app",
@@ -14,7 +14,7 @@ t.describe("make", () => {
14
14
  })
15
15
  })
16
16
 
17
- t.describe("option - nested builder API", () => {
17
+ t.describe(`${Commander.option.name} - nested builder API`, () => {
18
18
  t.it("should add an option with schema", () => {
19
19
  const cmd = Commander
20
20
  .make({ name: "app" })
@@ -76,7 +76,7 @@ t.describe("option - nested builder API", () => {
76
76
  })
77
77
  })
78
78
 
79
- t.describe("parse - kebab-to-camel conversion", () => {
79
+ t.describe(`${Commander.parse.name} - kebab-to-camel conversion`, () => {
80
80
  t.it("should convert kebab-case to camelCase", async () => {
81
81
  const cmd = Commander
82
82
  .make({ name: "app" })
@@ -198,7 +198,7 @@ t.describe("parse - kebab-to-camel conversion", () => {
198
198
  })
199
199
  })
200
200
 
201
- t.describe("optionHelp", () => {
201
+ t.describe(`${Commander.optionHelp.name}`, () => {
202
202
  t.it("should add help option", () => {
203
203
  const cmd = Commander
204
204
  .make({ name: "app" })
@@ -210,7 +210,7 @@ t.describe("optionHelp", () => {
210
210
  })
211
211
  })
212
212
 
213
- t.describe("optionVersion", () => {
213
+ t.describe(`${Commander.optionVersion.name}`, () => {
214
214
  t.it("should add version option", () => {
215
215
  const cmd = Commander
216
216
  .make({ name: "app", version: "1.0.0" })
@@ -222,7 +222,7 @@ t.describe("optionVersion", () => {
222
222
  })
223
223
  })
224
224
 
225
- t.describe("handle", () => {
225
+ t.describe(`${Commander.handle.name}`, () => {
226
226
  t.it("should mark command as handled", () => {
227
227
  const handled = Commander
228
228
  .make({ name: "app" })
@@ -246,7 +246,7 @@ t.describe("handle", () => {
246
246
  })
247
247
  })
248
248
 
249
- t.describe("subcommand", () => {
249
+ t.describe(`${Commander.subcommand.name}`, () => {
250
250
  t.it("should add subcommand", () => {
251
251
  const subCmd = Commander
252
252
  .make({ name: "format" })
@@ -266,7 +266,7 @@ t.describe("subcommand", () => {
266
266
  })
267
267
  })
268
268
 
269
- t.describe("help", () => {
269
+ t.describe(`${Commander.help.name}`, () => {
270
270
  t.it("should generate help text", () => {
271
271
  const cmd = Commander
272
272
  .make({
@@ -309,7 +309,7 @@ t.describe("BooleanFromString", () => {
309
309
  })
310
310
  })
311
311
 
312
- t.describe("choice", () => {
312
+ t.describe(`${Commander.choice.name}`, () => {
313
313
  t.it("should accept valid choice", async () => {
314
314
  const ColorSchema = Schema.compose(
315
315
  Schema.String,
@@ -337,7 +337,7 @@ t.describe("choice", () => {
337
337
  })
338
338
  })
339
339
 
340
- t.describe("repeatable", () => {
340
+ t.describe(`${Commander.repeatable.name}`, () => {
341
341
  t.it("should parse comma-separated values", async () => {
342
342
  const schema = Commander.repeatable(Schema.String)
343
343
 
@@ -452,7 +452,7 @@ t.describe("integration", () => {
452
452
  })
453
453
  })
454
454
 
455
- t.describe("parse - comprehensive", () => {
455
+ t.describe(`${Commander.parse.name} - comprehensive`, () => {
456
456
  t.it("should parse with explicit args", async () => {
457
457
  const cmd = Commander
458
458
  .make({ name: "app" })
@@ -845,7 +845,7 @@ t.describe("action/handler", () => {
845
845
  })
846
846
  })
847
847
 
848
- t.describe("version", () => {
848
+ t.describe(`${Commander.optionVersion.name} - version behavior`, () => {
849
849
  t.it("should include version in command definition", () => {
850
850
  const cmd = Commander
851
851
  .make({ name: "app", version: "1.0.0" })
@@ -860,7 +860,7 @@ t.describe("version", () => {
860
860
  .make({ name: "app", version: "2.0.0" })
861
861
 
862
862
  t.expect(cmd.version).toBe("2.0.0")
863
- t.expect((cmd.options as any).version).toBeUndefined()
863
+ t.expect(cmd.options["version"]).toBeUndefined()
864
864
  })
865
865
 
866
866
  t.it("should include version option in help", () => {
@@ -875,7 +875,7 @@ t.describe("version", () => {
875
875
  })
876
876
  })
877
877
 
878
- t.describe("help - comprehensive", () => {
878
+ t.describe(`${Commander.help.name} - comprehensive`, () => {
879
879
  t.it("should generate help with description", () => {
880
880
  const cmd = Commander
881
881
  .make({
@@ -951,7 +951,7 @@ t.describe("help - comprehensive", () => {
951
951
  })
952
952
  })
953
953
 
954
- t.describe("subcommands - comprehensive", () => {
954
+ t.describe(`${Commander.subcommand.name} - comprehensive`, () => {
955
955
  t.it("should add subcommand", () => {
956
956
  const subCmd = Commander
957
957
  .make({ name: "build" })
package/src/Commander.ts CHANGED
@@ -172,6 +172,19 @@ export interface SubcommandDef<Handled extends boolean = boolean> {
172
172
  readonly command: CommanderSet<any, any, Handled>
173
173
  }
174
174
 
175
+ type Extend<
176
+ S,
177
+ NewOpts extends OptionsMap,
178
+ NewSubs extends ReadonlyArray<SubcommandDef> = [],
179
+ Handled extends boolean = false,
180
+ > = S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
181
+ & Opts
182
+ & NewOpts,
183
+ [...Subs, ...NewSubs],
184
+ Handled
185
+ >
186
+ : CommanderSet<NewOpts, NewSubs, Handled>
187
+
175
188
  type CommanderBuilder = {
176
189
  option: typeof optionMethod
177
190
  optionHelp: typeof optionHelp
@@ -222,26 +235,13 @@ const optionMethod = function<
222
235
  >(
223
236
  this: S,
224
237
  opt: Opt,
225
- ): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
226
- & Opts
227
- & {
228
- [K in Opt["name"] as OptionNameToCamelCase<K>]: Opt
229
- },
230
- Subs,
231
- false
232
- >
233
- : CommanderSet<
234
- {
235
- [K in Opt["name"] as OptionNameToCamelCase<K>]: Opt
236
- },
237
- [],
238
- false
238
+ ): Extend<S, { [K in Opt["name"] as OptionNameToCamelCase<K>]: Opt }> {
239
+ const base = (this && typeof this === "object" ? this : {}) as Partial<
240
+ CommanderSet.Instance
239
241
  >
240
- {
241
- const base = this && typeof this === "object" ? this as any : {}
242
- const baseName = base.name || ""
243
- const baseOptions: OptionsMap = base.options || {}
244
- const baseSubcommands = base.subcommands || []
242
+ const baseName = base.name ?? ""
243
+ const baseOptions: OptionsMap = base.options ?? {}
244
+ const baseSubcommands = base.subcommands ?? []
245
245
  const baseDescription = base.description
246
246
  const baseVersion = base.version
247
247
 
@@ -253,31 +253,18 @@ const optionMethod = function<
253
253
  version: baseVersion,
254
254
  options: { ...baseOptions, [camelKey]: opt },
255
255
  subcommands: baseSubcommands,
256
- }) as any
256
+ }) as Extend<S, { [K in Opt["name"] as OptionNameToCamelCase<K>]: Opt }>
257
257
  }
258
258
 
259
259
  export const optionHelp = function<S>(
260
260
  this: S,
261
- ): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
262
- & Opts
263
- & {
264
- "help": OptionBuilder<boolean, "--help">
265
- },
266
- Subs,
267
- false
268
- >
269
- : CommanderSet<
270
- {
271
- "help": OptionBuilder<boolean, "--help">
272
- },
273
- [],
274
- false
261
+ ): Extend<S, { "help": OptionBuilder<boolean, "--help"> }> {
262
+ const base = (this && typeof this === "object" ? this : {}) as Partial<
263
+ CommanderSet.Instance
275
264
  >
276
- {
277
- const base = this && typeof this === "object" ? this as any : {}
278
- const baseName = base.name || ""
279
- const baseOptions: OptionsMap = base.options || {}
280
- const baseSubcommands = base.subcommands || []
265
+ const baseName = base.name ?? ""
266
+ const baseOptions: OptionsMap = base.options ?? {}
267
+ const baseSubcommands = base.subcommands ?? []
281
268
  const baseDescription = base.description
282
269
  const baseVersion = base.version
283
270
 
@@ -296,31 +283,18 @@ export const optionHelp = function<S>(
296
283
  version: baseVersion,
297
284
  options: { ...baseOptions, help: helpOption },
298
285
  subcommands: baseSubcommands,
299
- }) as any
286
+ }) as Extend<S, { "help": OptionBuilder<boolean, "--help"> }>
300
287
  }
301
288
 
302
289
  export const optionVersion = function<S>(
303
290
  this: S,
304
- ): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
305
- & Opts
306
- & {
307
- "version": OptionBuilder<boolean, "--version">
308
- },
309
- Subs,
310
- false
311
- >
312
- : CommanderSet<
313
- {
314
- "version": OptionBuilder<boolean, "--version">
315
- },
316
- [],
317
- false
291
+ ): Extend<S, { "version": OptionBuilder<boolean, "--version"> }> {
292
+ const base = (this && typeof this === "object" ? this : {}) as Partial<
293
+ CommanderSet.Instance
318
294
  >
319
- {
320
- const base = this && typeof this === "object" ? this as any : {}
321
- const baseName = base.name || ""
322
- const baseOptions: OptionsMap = base.options || {}
323
- const baseSubcommands = base.subcommands || []
295
+ const baseName = base.name ?? ""
296
+ const baseOptions: OptionsMap = base.options ?? {}
297
+ const baseSubcommands = base.subcommands ?? []
324
298
  const baseDescription = base.description
325
299
  const baseVersion = base.version
326
300
 
@@ -339,7 +313,7 @@ export const optionVersion = function<S>(
339
313
  version: baseVersion,
340
314
  options: { ...baseOptions, version: versionOption },
341
315
  subcommands: baseSubcommands,
342
- }) as any
316
+ }) as Extend<S, { "version": OptionBuilder<boolean, "--version"> }>
343
317
  }
344
318
 
345
319
  export const subcommand = function<
@@ -350,27 +324,19 @@ export const subcommand = function<
350
324
  >(
351
325
  this: S,
352
326
  cmd: CommanderSet<SubOpts, SubSubs, SubHandled>,
353
- ): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
354
- Opts,
355
- [...Subs, SubcommandDef<SubHandled>],
356
- false
327
+ ): Extend<S, {}, [SubcommandDef<SubHandled>]> {
328
+ const base = (this && typeof this === "object" ? this : {}) as Partial<
329
+ CommanderSet.Instance
357
330
  >
358
- : CommanderSet<
359
- {},
360
- [SubcommandDef<SubHandled>],
361
- false
362
- >
363
- {
364
- const base = this && typeof this === "object" ? this as any : {}
365
- const baseName = base.name || ""
366
- const baseOptions = base.options || {}
367
- const baseSubcommands = base.subcommands || []
331
+ const baseName = base.name ?? ""
332
+ const baseOptions = base.options ?? {}
333
+ const baseSubcommands = base.subcommands ?? []
368
334
  const baseDescription = base.description
369
335
  const baseVersion = base.version
370
336
 
371
- const subDef: SubcommandDef = {
337
+ const subDef: SubcommandDef<SubHandled> = {
372
338
  _tag: "SubcommandDef",
373
- command: cmd as any,
339
+ command: cmd,
374
340
  }
375
341
 
376
342
  return makeSet({
@@ -378,8 +344,8 @@ export const subcommand = function<
378
344
  description: baseDescription,
379
345
  version: baseVersion,
380
346
  options: baseOptions,
381
- subcommands: [...baseSubcommands, subDef] as any,
382
- }) as any
347
+ subcommands: [...baseSubcommands, subDef],
348
+ }) as Extend<S, {}, [SubcommandDef<SubHandled>]>
383
349
  }
384
350
 
385
351
  export const handle = function<
@@ -389,16 +355,16 @@ export const handle = function<
389
355
  this: CommanderSet<Opts, Subs, false>,
390
356
  handler: (args: ExtractOptionValues<Opts>) => Effect.Effect<void>,
391
357
  ): CommanderSet<Opts, Subs, true> {
392
- const base = this && typeof this === "object" ? this as any : {}
358
+ const base = this as CommanderSet.Instance<Opts, Subs, false>
393
359
 
394
- return makeSet({
395
- name: base.name || "",
360
+ return makeSet<Opts, Subs, true>({
361
+ name: base.name,
396
362
  description: base.description,
397
363
  version: base.version,
398
- options: base.options || {},
399
- subcommands: base.subcommands || [],
400
- handler: handler as any,
401
- }) as any
364
+ options: base.options,
365
+ subcommands: base.subcommands,
366
+ handler,
367
+ })
402
368
  }
403
369
 
404
370
  export const make = <const Name extends string>(config: {
@@ -589,12 +555,16 @@ export const runMain = <
589
555
 
590
556
  const parsedOptions = yield* parse(cmd, args)
591
557
 
592
- if ((parsedOptions as any).help) {
558
+ if (Predicate.hasProperty(parsedOptions, "help") && parsedOptions.help) {
593
559
  console.log(generateHelp(cmd))
594
560
  return
595
561
  }
596
562
 
597
- if ((parsedOptions as any).version && cmd.version) {
563
+ if (
564
+ Predicate.hasProperty(parsedOptions, "version")
565
+ && parsedOptions.version
566
+ && cmd.version
567
+ ) {
598
568
  console.log(`${cmd.name} v${cmd.version}`)
599
569
  return
600
570
  }
@@ -669,4 +639,4 @@ export const repeatable = <A>(
669
639
  })
670
640
  .pipe(
671
641
  Schema.compose(Schema.Array(schema)),
672
- ) as any
642
+ )
@@ -4,7 +4,7 @@ import * as ConfigProvider from "effect/ConfigProvider"
4
4
  import * as Effect from "effect/Effect"
5
5
  import * as EncryptedCookies from "./EncryptedCookies.ts"
6
6
 
7
- t.describe("encrypt", () => {
7
+ t.describe(`${EncryptedCookies.encrypt.name}`, () => {
8
8
  t.test("return encrypted string in correct format", async () => {
9
9
  const value = "hello world"
10
10
 
@@ -79,7 +79,7 @@ t.describe("encrypt", () => {
79
79
  })
80
80
  })
81
81
 
82
- t.describe("decrypt", () => {
82
+ t.describe(`${EncryptedCookies.decrypt.name}`, () => {
83
83
  t.test("decrypt encrypted string successfully", async () => {
84
84
  const originalValue = "hello world"
85
85
 
@@ -192,7 +192,7 @@ t.describe("decrypt", () => {
192
192
  })
193
193
  })
194
194
 
195
- t.describe("encryptCookie", () => {
195
+ t.describe(`${EncryptedCookies.encryptCookie.name}`, () => {
196
196
  t.test("preserve cookie properties and encrypt value", async () => {
197
197
  const cookie = Cookies.unsafeMakeCookie("test", "hello world")
198
198
 
@@ -211,7 +211,7 @@ t.describe("encryptCookie", () => {
211
211
  })
212
212
  })
213
213
 
214
- t.describe("decryptCookie", () => {
214
+ t.describe(`${EncryptedCookies.decryptCookie.name}`, () => {
215
215
  t.test("preserve cookie properties and decrypt value", async () => {
216
216
  const originalCookie = Cookies.unsafeMakeCookie("test", "hello world")
217
217
 
@@ -17,7 +17,6 @@ class CustomError extends Data.TaggedError("CustomError") {}
17
17
  const SampleRoutes = [
18
18
  {
19
19
  path: "/users",
20
- segments: [{ literal: "users" }],
21
20
  load: async () => ({
22
21
  default: Route
23
22
  .html(Effect.succeed("Users list"))
@@ -26,15 +25,14 @@ const SampleRoutes = [
26
25
  },
27
26
  {
28
27
  path: "/articles",
29
- segments: [{ literal: "articles" }],
30
28
  load: async () => ({
31
29
  default: Route.html(Effect.succeed("Articles list")),
32
30
  }),
33
31
  },
34
32
  ] as const
35
33
 
36
- const SampleRouteManifest: Router.RouteManifest = {
37
- modules: SampleRoutes,
34
+ const SampleRouteManifest: Router.RouterManifest = {
35
+ routes: SampleRoutes,
38
36
  }
39
37
 
40
38
  const routerLayer = Router.layerPromise(async () => SampleRouteManifest)
@@ -55,9 +53,8 @@ t.it("HttpRouter Requirement and Error types infers", () =>
55
53
 
56
54
  t.it("HTTP methods", () =>
57
55
  effect(function*() {
58
- const allMethodsRoute: Router.ServerRoute = {
56
+ const allMethodsRoute: Router.LazyRoute = {
59
57
  path: "/",
60
- segments: [],
61
58
  load: async () => ({
62
59
  default: Route
63
60
  .html(Effect.succeed("GET"))
@@ -90,8 +87,8 @@ t.it("HTTP methods", () =>
90
87
 
91
88
  t.it("router handles requests correctly", () =>
92
89
  effect(function*() {
93
- const routerContext = yield* Router.Router
94
- const client = TestHttpClient.make(routerContext.httpRouter)
90
+ const router = yield* FileHttpRouter.make(SampleRoutes)
91
+ const client = TestHttpClient.make(router)
95
92
 
96
93
  const getUsersResponse = yield* client.get("/users")
97
94
 
@@ -147,24 +144,21 @@ t.it(
147
144
  "handles routes with special characters (tilde and hyphen)",
148
145
  () =>
149
146
  effect(function*() {
150
- const specialCharRoutes: Router.ServerRoute[] = [
147
+ const specialCharRoutes: Router.LazyRoute[] = [
151
148
  {
152
149
  path: "/api-v1",
153
- segments: [{ literal: "api-v1" }],
154
150
  load: async () => ({
155
151
  default: Route.text(Effect.succeed("API v1")),
156
152
  }),
157
153
  },
158
154
  {
159
155
  path: "/files~backup",
160
- segments: [{ literal: "files~backup" }],
161
156
  load: async () => ({
162
157
  default: Route.text(Effect.succeed("Backup files")),
163
158
  }),
164
159
  },
165
160
  {
166
161
  path: "/test-route~temp",
167
- segments: [{ literal: "test-route~temp" }],
168
162
  load: async () => ({
169
163
  default: Route.post(Route.text(Effect.succeed("Test route"))),
170
164
  }),
@@ -205,3 +199,78 @@ t.it(
205
199
  .toBe("Test route")
206
200
  }),
207
201
  )
202
+
203
+ t.it(
204
+ "layer routes can wrap inner routes using next()",
205
+ () =>
206
+ effect(function*() {
207
+ const routeWithLayer: Router.LazyRoute = {
208
+ path: "/page",
209
+ load: async () => ({
210
+ default: Route.html(Effect.succeed("<h1>Page Content</h1>")),
211
+ }),
212
+ layers: [
213
+ async () => ({
214
+ default: Route.layer(
215
+ Route.html(function*(context) {
216
+ const innerContent = yield* context.next()
217
+ return `<html><body>${innerContent}</body></html>`
218
+ }),
219
+ ),
220
+ }),
221
+ ],
222
+ }
223
+
224
+ const router = yield* FileHttpRouter.make([routeWithLayer])
225
+ const client = TestHttpClient.make(router)
226
+
227
+ const response = yield* client.get("/page")
228
+
229
+ t.expect(response.status).toBe(200)
230
+
231
+ const html = yield* response.text
232
+
233
+ t.expect(html).toBe("<html><body><h1>Page Content</h1></body></html>")
234
+ }),
235
+ )
236
+
237
+ t.it("nested layers compose correctly with next()", () =>
238
+ effect(function*() {
239
+ const routeWithNestedLayers: Router.LazyRoute = {
240
+ path: "/nested",
241
+ load: async () => ({
242
+ default: Route.html(Effect.succeed("content")),
243
+ }),
244
+ layers: [
245
+ async () => ({
246
+ default: Route.layer(
247
+ Route.html(function*(context) {
248
+ const inner = yield* context.next()
249
+ return `<div class="outer">${inner}</div>`
250
+ }),
251
+ ),
252
+ }),
253
+ async () => ({
254
+ default: Route.layer(
255
+ Route.html(function*(context) {
256
+ const inner = yield* context.next()
257
+ return `<div class="inner">${inner}</div>`
258
+ }),
259
+ ),
260
+ }),
261
+ ],
262
+ }
263
+
264
+ const router = yield* FileHttpRouter.make([routeWithNestedLayers])
265
+ const client = TestHttpClient.make(router)
266
+
267
+ const response = yield* client.get("/nested")
268
+
269
+ t.expect(response.status).toBe(200)
270
+
271
+ const html = yield* response.text
272
+
273
+ t.expect(html).toBe(
274
+ "<div class=\"outer\"><div class=\"inner\">content</div></div>",
275
+ )
276
+ }))