effect-start 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/package.json +8 -9
  2. package/src/Commander.test.ts +507 -245
  3. package/src/ContentNegotiation.test.ts +603 -0
  4. package/src/ContentNegotiation.ts +542 -0
  5. package/src/Entity.test.ts +592 -0
  6. package/src/Entity.ts +362 -0
  7. package/src/FileRouter.ts +16 -12
  8. package/src/{FileRouterCodegen.test.ts → FileRouterCodegen.todo.ts} +384 -219
  9. package/src/FileRouterCodegen.ts +6 -6
  10. package/src/FileRouterPattern.test.ts +93 -62
  11. package/src/FileRouter_files.test.ts +5 -5
  12. package/src/FileRouter_path.test.ts +121 -69
  13. package/src/FileRouter_tree.test.ts +62 -56
  14. package/src/FileSystemExtra.test.ts +46 -30
  15. package/src/Http.test.ts +319 -0
  16. package/src/Http.ts +167 -0
  17. package/src/HttpAppExtra.test.ts +39 -20
  18. package/src/HttpAppExtra.ts +0 -1
  19. package/src/HttpUtils.test.ts +35 -18
  20. package/src/HttpUtils.ts +2 -0
  21. package/src/PathPattern.test.ts +648 -0
  22. package/src/PathPattern.ts +485 -0
  23. package/src/Route.ts +266 -1069
  24. package/src/RouteBody.test.ts +234 -0
  25. package/src/RouteBody.ts +193 -0
  26. package/src/RouteHook.test.ts +40 -0
  27. package/src/RouteHook.ts +106 -0
  28. package/src/RouteHttp.test.ts +2906 -0
  29. package/src/RouteHttp.ts +427 -0
  30. package/src/RouteHttpTracer.ts +92 -0
  31. package/src/RouteMount.test.ts +481 -0
  32. package/src/RouteMount.ts +470 -0
  33. package/src/RouteSchema.test.ts +427 -0
  34. package/src/RouteSchema.ts +423 -0
  35. package/src/RouteTree.test.ts +494 -0
  36. package/src/RouteTree.ts +219 -0
  37. package/src/RouteTrie.test.ts +322 -0
  38. package/src/RouteTrie.ts +224 -0
  39. package/src/RouterPattern.test.ts +569 -548
  40. package/src/RouterPattern.ts +7 -7
  41. package/src/Start.ts +3 -3
  42. package/src/StreamExtra.ts +21 -1
  43. package/src/TuplePathPattern.ts +64 -0
  44. package/src/Values.test.ts +263 -0
  45. package/src/Values.ts +76 -0
  46. package/src/bun/BunBundle.test.ts +36 -42
  47. package/src/bun/BunBundle.ts +2 -2
  48. package/src/bun/BunBundle_imports.test.ts +4 -6
  49. package/src/bun/BunHttpServer.test.ts +183 -6
  50. package/src/bun/BunHttpServer.ts +72 -32
  51. package/src/bun/BunHttpServer_web.ts +18 -6
  52. package/src/bun/BunImportTrackerPlugin.test.ts +3 -3
  53. package/src/bun/BunRoute.test.ts +124 -442
  54. package/src/bun/BunRoute.ts +146 -286
  55. package/src/{BundleHttp.test.ts → bundler/BundleHttp.test.ts} +34 -60
  56. package/src/{BundleHttp.ts → bundler/BundleHttp.ts} +1 -2
  57. package/src/client/index.ts +1 -1
  58. package/src/{Effect_HttpRouter.test.ts → effect/HttpRouter.test.ts} +69 -90
  59. package/src/experimental/EncryptedCookies.test.ts +125 -64
  60. package/src/experimental/SseHttpResponse.ts +0 -1
  61. package/src/hyper/Hyper.ts +89 -0
  62. package/src/{HyperHtml.test.ts → hyper/HyperHtml.test.ts} +13 -13
  63. package/src/{HyperHtml.ts → hyper/HyperHtml.ts} +2 -2
  64. package/src/{jsx.d.ts → hyper/jsx.d.ts} +1 -1
  65. package/src/index.ts +3 -4
  66. package/src/middlewares/BasicAuthMiddleware.test.ts +29 -19
  67. package/src/{NodeFileSystem.ts → node/FileSystem.ts} +6 -2
  68. package/src/testing/TestHttpClient.test.ts +26 -26
  69. package/src/testing/TestLogger.test.ts +27 -14
  70. package/src/testing/TestLogger.ts +15 -9
  71. package/src/x/datastar/Datastar.test.ts +47 -48
  72. package/src/x/datastar/Datastar.ts +1 -1
  73. package/src/x/tailwind/TailwindPlugin.test.ts +56 -58
  74. package/src/x/tailwind/plugin.ts +1 -1
  75. package/src/FileHttpRouter.test.ts +0 -239
  76. package/src/FileHttpRouter.ts +0 -194
  77. package/src/Hyper.ts +0 -194
  78. package/src/Route.test.ts +0 -1370
  79. package/src/RouteRender.ts +0 -40
  80. package/src/Router.test.ts +0 -375
  81. package/src/Router.ts +0 -255
  82. package/src/bun/BunRoute_bundles.test.ts +0 -219
  83. /package/src/{Bundle.ts → bundler/Bundle.ts} +0 -0
  84. /package/src/{BundleFiles.ts → bundler/BundleFiles.ts} +0 -0
  85. /package/src/{HyperNode.ts → hyper/HyperNode.ts} +0 -0
  86. /package/src/{jsx-runtime.ts → hyper/jsx-runtime.ts} +0 -0
  87. /package/src/{NodeUtils.ts → node/Utils.ts} +0 -0
package/src/Entity.ts ADDED
@@ -0,0 +1,362 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as ParseResult from "effect/ParseResult"
3
+ import * as Pipeable from "effect/Pipeable"
4
+ import * as Predicate from "effect/Predicate"
5
+ import * as Schema from "effect/Schema"
6
+ import * as Stream from "effect/Stream"
7
+ import * as StreamExtra from "./StreamExtra.ts"
8
+ import * as Values from "./Values.ts"
9
+
10
+ export const TypeId: unique symbol = Symbol.for("effect-start/Entity")
11
+ export type TypeId = typeof TypeId
12
+
13
+ const textDecoder = new TextDecoder()
14
+ const textEncoder = new TextEncoder()
15
+
16
+ function isBinary(v: unknown): v is Uint8Array | ArrayBuffer {
17
+ return v instanceof Uint8Array || v instanceof ArrayBuffer
18
+ }
19
+
20
+ /**
21
+ * Header keys are guaranteed to be lowercase.
22
+ */
23
+ export type Headers = {
24
+ [header: string]: string | null | undefined
25
+ }
26
+
27
+ export interface Entity<
28
+ T = unknown,
29
+ E = never,
30
+ > extends Pipeable.Pipeable {
31
+ readonly [TypeId]: TypeId
32
+ readonly body: T
33
+ readonly headers: Headers
34
+ /**
35
+ * Accepts any valid URI (Uniform Resource Identifier), including URLs
36
+ * (http://, https://, file://), URNs (urn:isbn:...), S3 URIs (s3://bucket/key),
37
+ * data URIs, and other schemes. While commonly called "URL" in many APIs,
38
+ * this property handles URIs as the correct superset term per RFC 3986.
39
+ */
40
+ readonly url: string | undefined
41
+ readonly status: number | undefined
42
+ readonly text: T extends string
43
+ ? Effect.Effect<T, ParseResult.ParseError | E>
44
+ : Effect.Effect<string, ParseResult.ParseError | E>
45
+ readonly json: [T] extends [Effect.Effect<infer A, any, any>]
46
+ ? Effect.Effect<
47
+ A extends string | Uint8Array | ArrayBuffer ? unknown : A,
48
+ ParseResult.ParseError | E
49
+ >
50
+ : [T] extends [Stream.Stream<any, any, any>]
51
+ ? Effect.Effect<unknown, ParseResult.ParseError | E>
52
+ : [T] extends [string | Uint8Array | ArrayBuffer]
53
+ ? Effect.Effect<unknown, ParseResult.ParseError | E>
54
+ : [T] extends [Values.Json]
55
+ ? Effect.Effect<T, ParseResult.ParseError | E>
56
+ : Effect.Effect<unknown, ParseResult.ParseError | E>
57
+ readonly bytes: Effect.Effect<Uint8Array, ParseResult.ParseError | E>
58
+ readonly stream: T extends Stream.Stream<infer A, infer E1, any>
59
+ ? Stream.Stream<A, ParseResult.ParseError | E | E1>
60
+ : Stream.Stream<Uint8Array, ParseResult.ParseError | E>
61
+ }
62
+
63
+ export interface Proto extends Pipeable.Pipeable {
64
+ readonly [TypeId]: TypeId
65
+ }
66
+
67
+ function parseJson(s: string): Effect.Effect<unknown, ParseResult.ParseError> {
68
+ try {
69
+ return Effect.succeed(JSON.parse(s))
70
+ } catch (e) {
71
+ return Effect.fail(
72
+ new ParseResult.ParseError({
73
+ issue: new ParseResult.Type(
74
+ Schema.Unknown.ast,
75
+ s,
76
+ e instanceof Error ? e.message : "Failed to parse JSON",
77
+ ),
78
+ }),
79
+ )
80
+ }
81
+ }
82
+
83
+ function getText(
84
+ self: Entity<unknown, unknown>,
85
+ ): Effect.Effect<string, ParseResult.ParseError | unknown> {
86
+ const v = self.body
87
+ if (StreamExtra.isStream(v)) {
88
+ return Stream.mkString(
89
+ Stream.decodeText(v as Stream.Stream<Uint8Array, unknown, never>),
90
+ )
91
+ }
92
+ if (Effect.isEffect(v)) {
93
+ return Effect.flatMap(
94
+ v as Effect.Effect<unknown, unknown, never>,
95
+ (inner): Effect.Effect<string, ParseResult.ParseError | unknown> => {
96
+ if (isEntity(inner)) {
97
+ return inner.text
98
+ }
99
+ if (typeof inner === "string") {
100
+ return Effect.succeed(inner)
101
+ }
102
+ if (isBinary(inner)) {
103
+ return Effect.succeed(textDecoder.decode(inner))
104
+ }
105
+ return Effect.fail(mismatch(Schema.String, inner))
106
+ },
107
+ )
108
+ }
109
+ if (typeof v === "string") {
110
+ return Effect.succeed(v)
111
+ }
112
+ if (isBinary(v)) {
113
+ return Effect.succeed(textDecoder.decode(v))
114
+ }
115
+ return Effect.fail(mismatch(Schema.String, v))
116
+ }
117
+
118
+ function getJson(
119
+ self: Entity<unknown, unknown>,
120
+ ): Effect.Effect<unknown, ParseResult.ParseError | unknown> {
121
+ const v = self.body
122
+ if (StreamExtra.isStream(v)) {
123
+ return Effect.flatMap(getText(self), parseJson)
124
+ }
125
+ if (Effect.isEffect(v)) {
126
+ return Effect.flatMap(
127
+ v as Effect.Effect<unknown, unknown, never>,
128
+ (inner): Effect.Effect<unknown, ParseResult.ParseError | unknown> => {
129
+ if (isEntity(inner)) {
130
+ return inner.json
131
+ }
132
+ if (typeof inner === "object" && inner !== null && !isBinary(inner)) {
133
+ return Effect.succeed(inner)
134
+ }
135
+ if (typeof inner === "string") {
136
+ return parseJson(inner)
137
+ }
138
+ if (isBinary(inner)) {
139
+ return parseJson(textDecoder.decode(inner))
140
+ }
141
+ return Effect.fail(mismatch(Schema.Unknown, inner))
142
+ },
143
+ )
144
+ }
145
+ if (typeof v === "object" && v !== null && !isBinary(v)) {
146
+ return Effect.succeed(v)
147
+ }
148
+ if (typeof v === "string") {
149
+ return parseJson(v)
150
+ }
151
+ if (isBinary(v)) {
152
+ return parseJson(textDecoder.decode(v))
153
+ }
154
+ return Effect.fail(mismatch(Schema.Unknown, v))
155
+ }
156
+
157
+ function getBytes(
158
+ self: Entity<unknown, unknown>,
159
+ ): Effect.Effect<Uint8Array, ParseResult.ParseError | unknown> {
160
+ const v = self.body
161
+ if (StreamExtra.isStream(v)) {
162
+ return Stream.runFold(
163
+ v as Stream.Stream<Uint8Array, unknown, never>,
164
+ new Uint8Array(0),
165
+ Values.concatBytes,
166
+ )
167
+ }
168
+ if (Effect.isEffect(v)) {
169
+ return Effect.flatMap(
170
+ v as Effect.Effect<unknown, unknown, never>,
171
+ (inner): Effect.Effect<Uint8Array, ParseResult.ParseError | unknown> => {
172
+ if (isEntity(inner)) {
173
+ return inner.bytes
174
+ }
175
+ if (inner instanceof Uint8Array) {
176
+ return Effect.succeed(inner)
177
+ }
178
+ if (inner instanceof ArrayBuffer) {
179
+ return Effect.succeed(new Uint8Array(inner))
180
+ }
181
+ if (typeof inner === "string") {
182
+ return Effect.succeed(textEncoder.encode(inner))
183
+ }
184
+ return Effect.fail(mismatch(Schema.Uint8ArrayFromSelf, inner))
185
+ },
186
+ )
187
+ }
188
+ if (v instanceof Uint8Array) {
189
+ return Effect.succeed(v)
190
+ }
191
+ if (v instanceof ArrayBuffer) {
192
+ return Effect.succeed(new Uint8Array(v))
193
+ }
194
+ if (typeof v === "string") {
195
+ return Effect.succeed(textEncoder.encode(v))
196
+ }
197
+ // Allows entity.stream to work when body is a JSON object
198
+ if (typeof v === "object" && v !== null && !isBinary(v)) {
199
+ return Effect.succeed(textEncoder.encode(JSON.stringify(v)))
200
+ }
201
+ return Effect.fail(mismatch(Schema.Uint8ArrayFromSelf, v))
202
+ }
203
+
204
+ function getStream<A, E1, E2>(
205
+ self: Entity<Stream.Stream<A, E1, never>, E2>,
206
+ ): Stream.Stream<A, ParseResult.ParseError | E1 | E2>
207
+ function getStream<T, E>(
208
+ self: Entity<T, E>,
209
+ ): Stream.Stream<Uint8Array, ParseResult.ParseError | E>
210
+ function getStream(
211
+ self: Entity<unknown, unknown>,
212
+ ): Stream.Stream<unknown, unknown> {
213
+ const v = self.body
214
+ if (StreamExtra.isStream(v)) {
215
+ return v as Stream.Stream<unknown, unknown, never>
216
+ }
217
+ if (Effect.isEffect(v)) {
218
+ return Stream.unwrap(
219
+ Effect.map(
220
+ v as Effect.Effect<unknown, unknown, never>,
221
+ (inner) => {
222
+ if (isEntity(inner)) {
223
+ return inner.stream
224
+ }
225
+ return Stream.fromEffect(getBytes(make(inner)))
226
+ },
227
+ ),
228
+ )
229
+ }
230
+ return Stream.fromEffect(getBytes(self))
231
+ }
232
+
233
+ const Proto: Proto = Object.defineProperties(
234
+ Object.create(null),
235
+ {
236
+ [TypeId]: { value: TypeId },
237
+ pipe: {
238
+ value: function(this: Entity) {
239
+ return Pipeable.pipeArguments(this, arguments)
240
+ },
241
+ },
242
+ text: {
243
+ get(this: Entity<unknown, unknown>) {
244
+ return getText(this)
245
+ },
246
+ },
247
+ json: {
248
+ get(this: Entity<unknown, unknown>) {
249
+ return getJson(this)
250
+ },
251
+ },
252
+ bytes: {
253
+ get(this: Entity<unknown, unknown>) {
254
+ return getBytes(this)
255
+ },
256
+ },
257
+ stream: {
258
+ get(this: Entity<unknown, unknown>) {
259
+ return getStream(this)
260
+ },
261
+ },
262
+ },
263
+ )
264
+
265
+ export function isEntity(input: unknown): input is Entity {
266
+ return Predicate.hasProperty(input, TypeId)
267
+ }
268
+
269
+ interface Options {
270
+ readonly headers?: Headers
271
+ readonly url?: string
272
+ readonly status?: number
273
+ }
274
+
275
+ export function make<A, E>(
276
+ body: Effect.Effect<A, E, never>,
277
+ options?: Options,
278
+ ): Entity<Effect.Effect<A, E, never>, E>
279
+ export function make<A extends Uint8Array | string, E>(
280
+ body: Stream.Stream<A, E, never>,
281
+ options?: Options,
282
+ ): Entity<Stream.Stream<A, E, never>, E>
283
+ export function make<T>(body: T, options?: Options): Entity<T, never>
284
+ export function make(
285
+ body: unknown,
286
+ options?: Options,
287
+ ): Entity<unknown, unknown> {
288
+ return Object.assign(
289
+ Object.create(Proto),
290
+ {
291
+ body,
292
+ headers: options?.headers ?? {},
293
+ url: options?.url,
294
+ status: options?.status,
295
+ },
296
+ )
297
+ }
298
+
299
+ export function effect<A, E, R>(
300
+ body: Effect.Effect<Entity<A> | A, E, R>,
301
+ ): Entity<A, E> {
302
+ return make(body) as unknown as Entity<A, E>
303
+ }
304
+
305
+ export function resolve<A, E>(
306
+ entity: Entity<A, E>,
307
+ ): Effect.Effect<Entity<A, E>, E, never> {
308
+ const body = entity.body
309
+ if (Effect.isEffect(body)) {
310
+ return Effect.map(
311
+ body as Effect.Effect<Entity<A> | A, E, never>,
312
+ (inner) =>
313
+ isEntity(inner)
314
+ ? inner as Entity<A, E>
315
+ : make(inner as A, {
316
+ status: entity.status,
317
+ headers: entity.headers,
318
+ url: entity.url,
319
+ }) as Entity<A, E>,
320
+ )
321
+ }
322
+ return Effect.succeed(entity)
323
+ }
324
+
325
+ export function type(self: Entity): string {
326
+ const h = self.headers
327
+ if (h["content-type"]) {
328
+ return h["content-type"]
329
+ }
330
+ const v = self.body
331
+ if (typeof v === "string") {
332
+ return "text/plain"
333
+ }
334
+ if (typeof v === "object" && v !== null && !isBinary(v)) {
335
+ return "application/json"
336
+ }
337
+ return "application/octet-stream"
338
+ }
339
+
340
+ export function length(self: Entity): number | undefined {
341
+ const h = self.headers
342
+ if (h["content-length"]) {
343
+ return parseInt(h["content-length"], 10)
344
+ }
345
+ const v = self.body
346
+ if (typeof v === "string") {
347
+ return textEncoder.encode(v).byteLength
348
+ }
349
+ if (isBinary(v)) {
350
+ return v.byteLength
351
+ }
352
+ return undefined
353
+ }
354
+
355
+ function mismatch(
356
+ expected: Schema.Schema.Any,
357
+ actual: unknown,
358
+ ): ParseResult.ParseError {
359
+ return new ParseResult.ParseError({
360
+ issue: new ParseResult.Type(expected.ast, actual),
361
+ })
362
+ }
package/src/FileRouter.ts CHANGED
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import type { PlatformError } from "@effect/platform/Error"
2
3
  import * as FileSystem from "@effect/platform/FileSystem"
3
4
  import * as Array from "effect/Array"
@@ -12,11 +13,9 @@ import * as NUrl from "node:url"
12
13
  import * as FileRouterCodegen from "./FileRouterCodegen.ts"
13
14
  import * as FileRouterPattern from "./FileRouterPattern.ts"
14
15
  import * as FileSystemExtra from "./FileSystemExtra.ts"
15
- import * as Route from "./Route.ts"
16
- import * as Router from "./Router.ts"
17
16
 
18
17
  export type RouteModule = {
19
- default: Route.RouteSet.Default
18
+ default: RouteSet.RouteSet.Default
20
19
  }
21
20
 
22
21
  export type LazyRoute = {
@@ -187,7 +186,9 @@ export function fromManifest(
187
186
  manifest: Manifest,
188
187
  ): Effect.Effect<Router.Router.Any> {
189
188
  return Effect.gen(function*() {
190
- const loadedEntries = yield* Effect.forEach(
189
+ const mounts: Record<`/${string}`, RouteSet.RouteSet.Default> = {}
190
+
191
+ yield* Effect.forEach(
191
192
  manifest.routes,
192
193
  (lazyRoute) =>
193
194
  Effect.gen(function*() {
@@ -199,19 +200,22 @@ export function fromManifest(
199
200
  )
200
201
  : []
201
202
 
202
- const layers = layerModules
203
- .map((m: any) => m.default)
204
- .filter(Route.isRouteLayer)
203
+ // Start with the route from the route module
204
+ let mergedRouteSet: RouteSet.RouteSet.Default = routeModule.default
205
205
 
206
- return {
207
- path: lazyRoute.path,
208
- route: routeModule.default,
209
- layers,
206
+ // Concatenate each layer's routes into the routeSet
207
+ for (const m of layerModules) {
208
+ const layerRouteSet = (m as any).default
209
+ if (RouteSet.isRouteSet(layerRouteSet)) {
210
+ mergedRouteSet = Route.merge(layerRouteSet, mergedRouteSet)
211
+ }
210
212
  }
213
+
214
+ mounts[lazyRoute.path] = mergedRouteSet
211
215
  }),
212
216
  )
213
217
 
214
- return Router.make(loadedEntries, [])
218
+ return Router.make(mounts, RouteSet.make())
215
219
  })
216
220
  }
217
221