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
@@ -0,0 +1,182 @@
1
+ import * as test from "bun:test"
2
+ import * as Effect from "effect/Effect"
3
+ import * as RouteBody from "./RouteBody.ts"
4
+ import * as RouteMount from "./RouteMount.ts"
5
+
6
+ const text = RouteBody.build<string, "text">({
7
+ format: "text",
8
+ })
9
+
10
+ test.it("infers parent descriptions", () => {
11
+ RouteMount.get(
12
+ text((ctx) =>
13
+ Effect.gen(function*() {
14
+ test
15
+ .expectTypeOf(ctx)
16
+ .toExtend<{
17
+ method: "GET"
18
+ format: "text"
19
+ }>()
20
+
21
+ return "Hello, world!"
22
+ })
23
+ ),
24
+ )
25
+ })
26
+
27
+ test.it("cannot modify context", () => {
28
+ text((ctx, next) =>
29
+ Effect.gen(function*() {
30
+ test
31
+ .expectTypeOf(next)
32
+ .parameters
33
+ .toEqualTypeOf<[]>()
34
+
35
+ return "Hello, world!"
36
+ })
37
+ )
38
+ })
39
+
40
+ test.it("enforces result value", () => {
41
+ // @ts-expect-error must return string
42
+ text((ctx, next) =>
43
+ Effect.gen(function*() {
44
+ return 1337
45
+ })
46
+ )
47
+ })
48
+
49
+ test.it("accepts value directly", () => {
50
+ const value = "Hello, world!"
51
+
52
+ test
53
+ .expectTypeOf(text)
54
+ .toBeCallableWith(value)
55
+ })
56
+
57
+ test.describe(`${RouteBody.handle.name}()`, () => {
58
+ const ctx = {}
59
+ const next = () => Effect.succeed("next" as const)
60
+
61
+ test.it("accepts all HandlerInput variants", () => {
62
+ test
63
+ .expectTypeOf<
64
+ RouteBody.HandlerInput<{ foo: string }, string, Error, never>
65
+ >()
66
+ .toExtend<Parameters<typeof RouteBody.handle>[0]>()
67
+ })
68
+
69
+ test.it("handles plain value", async () => {
70
+ const handler = RouteBody.handle("hello")
71
+
72
+ test
73
+ .expectTypeOf(handler)
74
+ .returns
75
+ .toEqualTypeOf<
76
+ Effect.Effect<string, never, never>
77
+ >()
78
+
79
+ const result = await Effect.runPromise(handler(ctx, next))
80
+ test.expect(result).toBe("hello")
81
+ })
82
+
83
+ test.it("handles Effect directly", async () => {
84
+ const handler = RouteBody.handle(Effect.succeed("from effect"))
85
+
86
+ test
87
+ .expectTypeOf(handler)
88
+ .returns
89
+ .toEqualTypeOf<
90
+ Effect.Effect<string, never, never>
91
+ >()
92
+
93
+ const result = await Effect.runPromise(handler(ctx, next))
94
+
95
+ test
96
+ .expect(result)
97
+ .toBe("from effect")
98
+ })
99
+
100
+ test.it("handles Effect with error", async () => {
101
+ const handler = RouteBody.handle(Effect.fail(new Error("oops")))
102
+
103
+ test
104
+ .expectTypeOf(handler)
105
+ .returns
106
+ .toEqualTypeOf<
107
+ Effect.Effect<never, Error, never>
108
+ >()
109
+ })
110
+
111
+ test.it("handles function", async () => {
112
+ const handler = RouteBody.handle(
113
+ (ctx: { id: number }) => Effect.succeed(ctx.id),
114
+ )
115
+
116
+ test
117
+ .expectTypeOf(handler)
118
+ .parameters
119
+ .toEqualTypeOf<
120
+ [{ id: number }, () => Effect.Effect<number>]
121
+ >()
122
+ test
123
+ .expectTypeOf(handler)
124
+ .returns
125
+ .toEqualTypeOf<
126
+ Effect.Effect<number, never, never>
127
+ >()
128
+
129
+ const result = await Effect.runPromise(
130
+ handler({ id: 42 }, () => Effect.succeed(23)),
131
+ )
132
+
133
+ test
134
+ .expect(result)
135
+ .toBe(42)
136
+ })
137
+
138
+ test.it("handles generator", async () => {
139
+ const handler = RouteBody.handle(function*(ctx: { id: number }) {
140
+ const n = yield* Effect.succeed(ctx.id)
141
+ return n * 2
142
+ })
143
+
144
+ test
145
+ .expectTypeOf(handler)
146
+ .parameters
147
+ .toEqualTypeOf<
148
+ [{ id: number }, () => Effect.Effect<number>]
149
+ >()
150
+
151
+ test
152
+ .expectTypeOf(handler)
153
+ .returns
154
+ .toEqualTypeOf<
155
+ Effect.Effect<number, never, never>
156
+ >()
157
+
158
+ const result = await Effect.runPromise(
159
+ // TODO: we should accept Effect.void in next here
160
+ handler({ id: 21 }, () => Effect.succeed(23)),
161
+ )
162
+
163
+ test
164
+ .expect(result)
165
+ .toBe(42)
166
+ })
167
+
168
+ test.it("generator can call next", async () => {
169
+ const handler = RouteBody.handle(
170
+ function*(_ctx: {}, next: () => Effect.Effect<string>) {
171
+ const fromNext = yield* next()
172
+ return `got: ${fromNext}`
173
+ },
174
+ )
175
+
176
+ const result = await Effect.runPromise(handler(ctx, next))
177
+
178
+ test
179
+ .expect(result)
180
+ .toBe("got: next")
181
+ })
182
+ })
@@ -0,0 +1,106 @@
1
+ import * as Effect from "effect/Effect"
2
+ import type * as Utils from "effect/Utils"
3
+ import * as Route from "./Route.ts"
4
+
5
+ export type Format =
6
+ | "text"
7
+ | "html"
8
+ | "json"
9
+ | "bytes"
10
+
11
+ export type HandlerInput<B, A, E, R> =
12
+ | A
13
+ | Effect.Effect<A, E, R>
14
+ | ((context: _Simplify<B>, next: () => Effect.Effect<A>) =>
15
+ | Effect.Effect<A, E, R>
16
+ | Generator<Utils.YieldWrap<Effect.Effect<any, E, R>>, A, any>)
17
+
18
+ export function handle<B, A, E, R>(
19
+ handler: (context: B, next: () => Effect.Effect<A>) =>
20
+ | Effect.Effect<A, E, R>
21
+ | Generator<Utils.YieldWrap<Effect.Effect<any, E, R>>, A, any>,
22
+ ): Route.Route.HandlerImmutable<B, A, E, R>
23
+ export function handle<A, E, R>(
24
+ handler: Effect.Effect<A, E, R>,
25
+ ): Route.Route.HandlerImmutable<{}, A, E, R>
26
+ export function handle<A>(
27
+ handler: A,
28
+ ): Route.Route.HandlerImmutable<{}, A, never, never>
29
+ export function handle<B, A, E, R>(
30
+ handler: HandlerInput<B, A, E, R>,
31
+ ): Route.Route.HandlerImmutable<B, A, E, R> {
32
+ if (typeof handler === "function") {
33
+ return (
34
+ context: B,
35
+ next: () => Effect.Effect<A>,
36
+ ): Effect.Effect<A, E, R> => {
37
+ const result = (handler as Function)(context, next)
38
+ if (Effect.isEffect(result)) {
39
+ return result as Effect.Effect<A, E, R>
40
+ }
41
+ return Effect.gen(function*() {
42
+ return yield* result
43
+ }) as Effect.Effect<A, E, R>
44
+ }
45
+ }
46
+ if (Effect.isEffect(handler)) {
47
+ return (_context, _next) => handler
48
+ }
49
+ return (_context, _next) => Effect.succeed(handler as A)
50
+ }
51
+
52
+ export function build<
53
+ Value,
54
+ F extends Format,
55
+ >(
56
+ descriptors: { format: F },
57
+ ) {
58
+ return function<
59
+ D extends Route.RouteDescriptor.Any,
60
+ B extends {},
61
+ I extends Route.Route.Tuple,
62
+ A extends Value,
63
+ E = never,
64
+ R = never,
65
+ >(
66
+ handler: HandlerInput<
67
+ NoInfer<
68
+ D & B & Route.ExtractBindings<I> & { format: F }
69
+ >,
70
+ A,
71
+ E,
72
+ R
73
+ >,
74
+ ) {
75
+ return function(
76
+ self: Route.RouteSet.RouteSet<D, B, I>,
77
+ ) {
78
+ const route = Route.make<{ format: F }, {}, A, E, R>(
79
+ handle(handler) as any,
80
+ descriptors,
81
+ )
82
+
83
+ const items: [...I, Route.Route.Route<{ format: F }, {}, A, E, R>] = [
84
+ ...Route.items(self),
85
+ route,
86
+ ]
87
+
88
+ return Route.set<
89
+ D,
90
+ B,
91
+ [...I, Route.Route.Route<{ format: F }, {}, A, E, R>]
92
+ >(
93
+ items,
94
+ Route.descriptor(self),
95
+ )
96
+ }
97
+ }
98
+ }
99
+
100
+ // used to simplify the context type passed to route handlers
101
+ // for those who prefer to write code by hand :)
102
+ type _Simplify<T> = {
103
+ -readonly [K in keyof T]: T[K] extends object
104
+ ? { -readonly [P in keyof T[K]]: T[K][P] }
105
+ : T[K]
106
+ } extends infer U ? { [K in keyof U]: U[K] } : never
@@ -0,0 +1,40 @@
1
+ import * as test from "bun:test"
2
+ import { Effect } from "effect"
3
+ import * as Route from "./Route.ts"
4
+
5
+ test.it("passes bindings", () => {
6
+ const headers = {
7
+ "origin": "nounder.org",
8
+ }
9
+ const filterResult = {
10
+ context: {
11
+ headers,
12
+ },
13
+ }
14
+
15
+ const routes = Route.empty.pipe(
16
+ Route.filter(() => Effect.succeed(filterResult)),
17
+ Route.text(context => {
18
+ test
19
+ .expectTypeOf(context)
20
+ .toExtend<typeof filterResult.context>()
21
+
22
+ return Effect.succeed(
23
+ `Origin: ${context.headers.origin}`,
24
+ )
25
+ }),
26
+ )
27
+
28
+ test
29
+ .expectTypeOf(routes)
30
+ .toExtend<
31
+ Route.RouteSet.RouteSet<{}, {}, [
32
+ Route.Route.Route<{}, typeof filterResult.context, any>,
33
+ Route.Route.Route<{ format: "text" }, {}, string>,
34
+ ]>
35
+ >()
36
+
37
+ test
38
+ .expect(Route.items(routes))
39
+ .toHaveLength(2)
40
+ })
@@ -0,0 +1,105 @@
1
+ import * as Effect from "effect/Effect"
2
+ import type * as Utils from "effect/Utils"
3
+ import * as Route from "./Route.ts"
4
+
5
+ export type FilterResult<BOut, E, R> =
6
+ | { context: BOut }
7
+ | Effect.Effect<{ context: BOut }, E, R>
8
+
9
+ export type FilterHandlerInput<BIn, BOut, E, R> =
10
+ | FilterResult<BOut, E, R>
11
+ | ((context: BIn) =>
12
+ | FilterResult<BOut, E, R>
13
+ | Generator<
14
+ Utils.YieldWrap<Effect.Effect<any, E, R>>,
15
+ { context: BOut },
16
+ any
17
+ >)
18
+
19
+ export function filter<
20
+ D extends Route.RouteDescriptor.Any,
21
+ SB extends {},
22
+ P extends Route.Route.Tuple,
23
+ BOut extends {},
24
+ E = never,
25
+ R = never,
26
+ BIn = D & SB & Route.ExtractBindings<P>,
27
+ >(
28
+ filterHandler: FilterHandlerInput<BIn, BOut, E, R>,
29
+ ) {
30
+ const normalized = normalizeFilterHandler(filterHandler)
31
+
32
+ return function(
33
+ self: Route.RouteSet.RouteSet<D, SB, P>,
34
+ ): Route.RouteSet.RouteSet<
35
+ D,
36
+ SB,
37
+ [...P, Route.Route.Route<{}, BOut, void, E, R>]
38
+ > {
39
+ const route = Route.make<
40
+ {},
41
+ BOut,
42
+ void,
43
+ E,
44
+ R
45
+ >((context: BOut, next) =>
46
+ Effect.gen(function*() {
47
+ const filterResult = yield* normalized(context as unknown as BIn)
48
+
49
+ yield* next(
50
+ filterResult
51
+ ? {
52
+ ...context,
53
+ ...filterResult.context,
54
+ }
55
+ : context,
56
+ )
57
+ })
58
+ )
59
+
60
+ return Route.set(
61
+ [
62
+ ...Route.items(self),
63
+ route,
64
+ ] as [...P, Route.Route.Route<{}, BOut, void, E, R>],
65
+ Route.descriptor(self),
66
+ )
67
+ }
68
+ }
69
+
70
+ function isGenerator(value: unknown): value is Generator {
71
+ return (
72
+ typeof value === "object"
73
+ && value !== null
74
+ && Symbol.iterator in value
75
+ && typeof (value as Generator).next === "function"
76
+ )
77
+ }
78
+
79
+ function normalizeFilterHandler<BIn, BOut, E, R>(
80
+ handler: FilterHandlerInput<BIn, BOut, E, R>,
81
+ ): (context: BIn) => Effect.Effect<{ context: BOut }, E, R> {
82
+ if (typeof handler === "function") {
83
+ return (context: BIn): Effect.Effect<{ context: BOut }, E, R> => {
84
+ const result = handler(context)
85
+
86
+ if (Effect.isEffect(result)) {
87
+ return result as Effect.Effect<{ context: BOut }, E, R>
88
+ }
89
+
90
+ if (isGenerator(result)) {
91
+ return Effect.gen(function*() {
92
+ return yield* result
93
+ }) as Effect.Effect<{ context: BOut }, E, R>
94
+ }
95
+
96
+ return Effect.succeed(result)
97
+ }
98
+ }
99
+
100
+ if (Effect.isEffect(handler)) {
101
+ return (_context) => handler as Effect.Effect<{ context: BOut }, E, R>
102
+ }
103
+
104
+ return (_context) => Effect.succeed(handler as { context: BOut })
105
+ }