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/src/Random.ts ADDED
@@ -0,0 +1,59 @@
1
+ export function uuid(): string {
2
+ return base36(uuid4bytes())
3
+ }
4
+
5
+ export function uuidSorted(): string {
6
+ return base36(uuid7bytes())
7
+ }
8
+
9
+ export function token(length: number): string {
10
+ return base36(bytes(length))
11
+ }
12
+
13
+ function base36(bytes: Uint8Array): string {
14
+ if (bytes.length === 0) return ""
15
+ let zeros = 0
16
+ while (zeros < bytes.length && bytes[zeros] === 0) zeros++
17
+
18
+ let n = 0n
19
+ for (let i = zeros; i < bytes.length; i++) n = (n << 8n) + BigInt(bytes[i])
20
+
21
+ return "0".repeat(zeros) + (n === 0n ? "" : n.toString(36))
22
+ }
23
+
24
+ export function bytes(length: number): Uint8Array {
25
+ const buf = new Uint8Array(length)
26
+ crypto.getRandomValues(buf)
27
+ return buf
28
+ }
29
+
30
+ export function uuid4bytes(): Uint8Array {
31
+ const buf = bytes(16)
32
+ buf[6] = (buf[6] & 0x0f) | 0x40 // version 4
33
+ buf[8] = (buf[8] & 0x3f) | 0x80 // variant
34
+
35
+ return buf
36
+ }
37
+
38
+ function uuid7bytes(): Uint8Array {
39
+ const buf = new Uint8Array(16)
40
+ const timestamp = BigInt(Date.now())
41
+
42
+ // 48-bit timestamp (6 bytes)
43
+ buf[0] = Number((timestamp >> 40n) & 0xffn)
44
+ buf[1] = Number((timestamp >> 32n) & 0xffn)
45
+ buf[2] = Number((timestamp >> 24n) & 0xffn)
46
+ buf[3] = Number((timestamp >> 16n) & 0xffn)
47
+ buf[4] = Number((timestamp >> 8n) & 0xffn)
48
+ buf[5] = Number(timestamp & 0xffn)
49
+
50
+ // 12-bit random A (1.5 bytes)
51
+ crypto.getRandomValues(buf.subarray(6, 8))
52
+ buf[6] = (buf[6] & 0x0f) | 0x70 // version 7
53
+
54
+ // 2-bit variant + 62-bit random B (8 bytes)
55
+ crypto.getRandomValues(buf.subarray(8, 16))
56
+ buf[8] = (buf[8] & 0x3f) | 0x80 // variant
57
+
58
+ return buf
59
+ }
package/src/Route.test.ts CHANGED
@@ -1,4 +1,9 @@
1
+ import * as HttpApp from "@effect/platform/HttpApp"
2
+ import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
3
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
4
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
1
5
  import * as t from "bun:test"
6
+
2
7
  import * as Effect from "effect/Effect"
3
8
  import * as Function from "effect/Function"
4
9
  import * as Schema from "effect/Schema"
@@ -871,3 +876,469 @@ t.it("schemaHeaders accepts string and string array encoded schemas", () => {
871
876
  })
872
877
  .text(Effect.succeed("ok"))
873
878
  })
879
+
880
+ t.it("Route.http creates RouteMiddleware", () => {
881
+ const middleware = (app: any) => app
882
+
883
+ const spec = Route.http(middleware)
884
+
885
+ t.expect(spec._tag).toBe("RouteMiddleware")
886
+ t.expect(spec.middleware).toBe(middleware)
887
+ })
888
+
889
+ t.it("Route.layer creates RouteLayer with middleware", () => {
890
+ const middleware = (app: any) => app
891
+
892
+ const layer = Route.layer(
893
+ Route.http(middleware),
894
+ Route.html(Effect.succeed("<div>test</div>")),
895
+ )
896
+
897
+ t.expect(Route.isRouteLayer(layer)).toBe(true)
898
+ t.expect(layer.httpMiddleware).toBe(middleware)
899
+ t.expect(layer.set.length).toBe(1)
900
+ })
901
+
902
+ t.it("Route.layer merges multiple route sets", () => {
903
+ const routes1 = Route.html(Effect.succeed("<div>1</div>"))
904
+ const routes2 = Route.text(Effect.succeed("text"))
905
+
906
+ const layer = Route.layer(routes1, routes2)
907
+
908
+ t.expect(layer.set.length).toBe(2)
909
+ t.expect(Route.isRouteLayer(layer)).toBe(true)
910
+ })
911
+
912
+ t.it("Route.layer merges routes from all route sets", () => {
913
+ const routes1 = Route
914
+ .schemaPathParams({ id: Schema.String })
915
+ .html(Effect.succeed("<div>test</div>"))
916
+
917
+ const routes2 = Route
918
+ .schemaUrlParams({ page: Schema.NumberFromString })
919
+ .text(Effect.succeed("text"))
920
+
921
+ const layer = Route.layer(routes1, routes2)
922
+
923
+ t.expect(layer.set.length).toBe(2)
924
+ t.expect(layer.set[0]!.media).toBe("text/html")
925
+ t.expect(layer.set[1]!.media).toBe("text/plain")
926
+ })
927
+
928
+ t.it("Route.layer works with no middleware", () => {
929
+ const layer = Route.layer(
930
+ Route.html(Effect.succeed("<div>test</div>")),
931
+ )
932
+
933
+ t.expect(Route.isRouteLayer(layer)).toBe(true)
934
+ t.expect(layer.httpMiddleware).toBeUndefined()
935
+ t.expect(layer.set.length).toBe(1)
936
+ })
937
+
938
+ t.it("Route.layer works with no routes", () => {
939
+ const middleware = (app: any) => app
940
+
941
+ const layer = Route.layer(
942
+ Route.http(middleware),
943
+ )
944
+
945
+ t.expect(Route.isRouteLayer(layer)).toBe(true)
946
+ t.expect(layer.httpMiddleware).toBe(middleware)
947
+ t.expect(layer.set.length).toBe(0)
948
+ })
949
+
950
+ t.it("isRouteLayer type guard works correctly", () => {
951
+ const middleware = (app: any) => app
952
+ const layer = Route.layer(Route.http(middleware))
953
+ const regularRoutes = Route.html(Effect.succeed("<div>test</div>"))
954
+
955
+ t.expect(Route.isRouteLayer(layer)).toBe(true)
956
+ t.expect(Route.isRouteLayer(regularRoutes)).toBe(false)
957
+ t.expect(Route.isRouteLayer(null)).toBe(false)
958
+ t.expect(Route.isRouteLayer(undefined)).toBe(false)
959
+ t.expect(Route.isRouteLayer({})).toBe(false)
960
+ })
961
+
962
+ t.it("Route.layer composes multiple middleware in order", async () => {
963
+ const executionOrder: string[] = []
964
+
965
+ const middleware1 = HttpMiddleware.make((app) =>
966
+ Effect.gen(function*() {
967
+ executionOrder.push("middleware1-before")
968
+ const result = yield* app
969
+ executionOrder.push("middleware1-after")
970
+ return result
971
+ })
972
+ ) as Route.HttpMiddlewareFunction
973
+
974
+ const middleware2 = HttpMiddleware.make((app) =>
975
+ Effect.gen(function*() {
976
+ executionOrder.push("middleware2-before")
977
+ const result = yield* app
978
+ executionOrder.push("middleware2-after")
979
+ return result
980
+ })
981
+ ) as Route.HttpMiddlewareFunction
982
+
983
+ const middleware3 = HttpMiddleware.make((app) =>
984
+ Effect.gen(function*() {
985
+ executionOrder.push("middleware3-before")
986
+ const result = yield* app
987
+ executionOrder.push("middleware3-after")
988
+ return result
989
+ })
990
+ ) as Route.HttpMiddlewareFunction
991
+
992
+ const layer = Route.layer(
993
+ Route.http(middleware1),
994
+ Route.http(middleware2),
995
+ Route.http(middleware3),
996
+ )
997
+
998
+ t.expect(layer.httpMiddleware).toBeDefined()
999
+
1000
+ const mockApp = Effect.sync(() => {
1001
+ executionOrder.push("app")
1002
+ return HttpServerResponse.text("result")
1003
+ })
1004
+
1005
+ const composed = layer.httpMiddleware!(mockApp) as Effect.Effect<
1006
+ HttpServerResponse.HttpServerResponse,
1007
+ never,
1008
+ never
1009
+ >
1010
+ await Effect.runPromise(composed.pipe(Effect.orDie))
1011
+
1012
+ t.expect(executionOrder).toEqual([
1013
+ "middleware1-before",
1014
+ "middleware2-before",
1015
+ "middleware3-before",
1016
+ "app",
1017
+ "middleware3-after",
1018
+ "middleware2-after",
1019
+ "middleware1-after",
1020
+ ])
1021
+ })
1022
+
1023
+ t.it("Route.layer with single middleware works correctly", async () => {
1024
+ let middlewareCalled = false
1025
+
1026
+ const middleware = HttpMiddleware.make((app) =>
1027
+ Effect.gen(function*() {
1028
+ middlewareCalled = true
1029
+ return yield* app
1030
+ })
1031
+ ) as Route.HttpMiddlewareFunction
1032
+
1033
+ const layer = Route.layer(Route.http(middleware))
1034
+
1035
+ t.expect(layer.httpMiddleware).toBeDefined()
1036
+
1037
+ const mockApp = Effect.succeed(HttpServerResponse.text("result"))
1038
+ const composed = layer.httpMiddleware!(mockApp) as Effect.Effect<
1039
+ HttpServerResponse.HttpServerResponse,
1040
+ never,
1041
+ never
1042
+ >
1043
+ await Effect.runPromise(composed.pipe(Effect.orDie))
1044
+
1045
+ t.expect(middlewareCalled).toBe(true)
1046
+ })
1047
+
1048
+ t.it("Route.layer middleware can modify responses", async () => {
1049
+ const addHeader1 = HttpMiddleware.make((app) =>
1050
+ Effect.gen(function*() {
1051
+ const result = yield* app
1052
+ return HttpServerResponse.setHeader(result, "X-Custom-1", "value1")
1053
+ })
1054
+ ) as Route.HttpMiddlewareFunction
1055
+
1056
+ const addHeader2 = HttpMiddleware.make((app) =>
1057
+ Effect.gen(function*() {
1058
+ const result = yield* app
1059
+ return HttpServerResponse.setHeader(result, "X-Custom-2", "value2")
1060
+ })
1061
+ ) as Route.HttpMiddlewareFunction
1062
+
1063
+ const layer = Route.layer(
1064
+ Route.http(addHeader1),
1065
+ Route.http(addHeader2),
1066
+ )
1067
+
1068
+ const mockApp = Effect.succeed(HttpServerResponse.text("data"))
1069
+ const composed = layer.httpMiddleware!(mockApp) as Effect.Effect<
1070
+ HttpServerResponse.HttpServerResponse,
1071
+ never,
1072
+ never
1073
+ >
1074
+ const result = await Effect.runPromise(composed.pipe(Effect.orDie))
1075
+
1076
+ t.expect(result.headers["x-custom-1"]).toBe("value1")
1077
+ t.expect(result.headers["x-custom-2"]).toBe("value2")
1078
+ })
1079
+
1080
+ t.it("Route.matches returns true for exact method and media match", () => {
1081
+ const route1 = Route.get(Route.html(Effect.succeed("<div>test</div>")))
1082
+ const route2 = Route.get(Route.html(Effect.succeed("<div>other</div>")))
1083
+
1084
+ t.expect(Route.matches(route1.set[0]!, route2.set[0]!)).toBe(true)
1085
+ })
1086
+
1087
+ t.it("Route.matches returns false for different methods", () => {
1088
+ const route1 = Route.get(Route.html(Effect.succeed("<div>test</div>")))
1089
+ const route2 = Route.post(Route.html(Effect.succeed("<div>other</div>")))
1090
+
1091
+ t.expect(Route.matches(route1.set[0]!, route2.set[0]!)).toBe(false)
1092
+ })
1093
+
1094
+ t.it("Route.matches returns false for different media types", () => {
1095
+ const route1 = Route.get(Route.html(Effect.succeed("<div>test</div>")))
1096
+ const route2 = Route.get(Route.json(Effect.succeed({ data: "test" })))
1097
+
1098
+ t.expect(Route.matches(route1.set[0]!, route2.set[0]!)).toBe(false)
1099
+ })
1100
+
1101
+ t.it("Route.matches returns true when method is wildcard", () => {
1102
+ const route1 = Route.html(Effect.succeed("<div>test</div>"))
1103
+ const route2 = Route.get(Route.html(Effect.succeed("<div>other</div>")))
1104
+
1105
+ t.expect(Route.matches(route1.set[0]!, route2.set[0]!)).toBe(true)
1106
+ t.expect(Route.matches(route2.set[0]!, route1.set[0]!)).toBe(true)
1107
+ })
1108
+
1109
+ t.it("Route.matches returns true when one route has wildcard method", () => {
1110
+ const wildcardRoute = Route.html(Effect.succeed("<div>test</div>"))
1111
+ const specificRoute = Route.get(
1112
+ Route.html(Effect.succeed("<div>other</div>")),
1113
+ )
1114
+
1115
+ t.expect(Route.matches(wildcardRoute.set[0]!, specificRoute.set[0]!)).toBe(
1116
+ true,
1117
+ )
1118
+ })
1119
+
1120
+ t.describe("Route.merge", () => {
1121
+ t.it("types merged routes with union of methods", () => {
1122
+ const textRoute = Route.text(Effect.succeed("hello"))
1123
+ const htmlRoute = Route.html(Effect.succeed("<div>world</div>"))
1124
+
1125
+ const merged = Route.merge(textRoute, htmlRoute)
1126
+
1127
+ type Expected = Route.RouteSet<
1128
+ [
1129
+ Route.Route<
1130
+ "GET",
1131
+ "text/plain" | "text/html",
1132
+ Route.RouteHandler<HttpServerResponse.HttpServerResponse>
1133
+ >,
1134
+ ]
1135
+ >
1136
+
1137
+ Function.satisfies<Expected>()(merged)
1138
+ })
1139
+
1140
+ t.it("types merged routes with different methods", () => {
1141
+ const getRoute = Route.get(Route.text(Effect.succeed("get")))
1142
+ const postRoute = Route.post(Route.json(Effect.succeed({ ok: true })))
1143
+
1144
+ const merged = Route.merge(getRoute, postRoute)
1145
+
1146
+ type Expected = Route.RouteSet<
1147
+ [
1148
+ Route.Route<
1149
+ "GET" | "POST",
1150
+ "text/plain" | "application/json",
1151
+ Route.RouteHandler<HttpServerResponse.HttpServerResponse>
1152
+ >,
1153
+ ]
1154
+ >
1155
+
1156
+ Function.satisfies<Expected>()(merged)
1157
+ })
1158
+
1159
+ t.it("types merged schemas using MergeSchemas", () => {
1160
+ const routeA = Route
1161
+ .schemaPathParams({ id: Schema.NumberFromString })
1162
+ .text(Effect.succeed("a"))
1163
+
1164
+ const routeB = Route
1165
+ .schemaUrlParams({ page: Schema.NumberFromString })
1166
+ .html(Effect.succeed("<div>b</div>"))
1167
+
1168
+ const merged = Route.merge(routeA, routeB)
1169
+
1170
+ type MergedSchemas = typeof merged.schema
1171
+
1172
+ type ExpectedPathParams = {
1173
+ readonly id: typeof Schema.NumberFromString
1174
+ }
1175
+ type ExpectedUrlParams = {
1176
+ readonly page: typeof Schema.NumberFromString
1177
+ }
1178
+
1179
+ type CheckPathParams = MergedSchemas["PathParams"] extends
1180
+ Schema.Struct<ExpectedPathParams> ? true : false
1181
+ type CheckUrlParams = MergedSchemas["UrlParams"] extends
1182
+ Schema.Struct<ExpectedUrlParams> ? true : false
1183
+
1184
+ const _pathParamsCheck: CheckPathParams = true
1185
+ const _urlParamsCheck: CheckUrlParams = true
1186
+ })
1187
+
1188
+ t.it("merged route does content negotiation for text/plain", async () => {
1189
+ const textRoute = Route.text(Effect.succeed("plain text"))
1190
+ const htmlRoute = Route.html(Effect.succeed("<div>html</div>"))
1191
+
1192
+ const merged = Route.merge(textRoute, htmlRoute)
1193
+ const route = merged.set[0]!
1194
+
1195
+ const request = HttpServerRequest.fromWeb(
1196
+ new Request("http://localhost/test", {
1197
+ headers: { Accept: "text/plain" },
1198
+ }),
1199
+ )
1200
+
1201
+ const context: Route.RouteContext = {
1202
+ request,
1203
+ get url() {
1204
+ return new URL(request.url)
1205
+ },
1206
+ slots: {},
1207
+ next: () => Effect.void,
1208
+ }
1209
+
1210
+ const result = await Effect.runPromise(route.handler(context))
1211
+
1212
+ const webResponse = HttpServerResponse.toWeb(result)
1213
+ const text = await webResponse.text()
1214
+
1215
+ t.expect(text).toBe("plain text")
1216
+ t.expect(result.headers["content-type"]).toBe("text/plain")
1217
+ })
1218
+
1219
+ t.it("merged route does content negotiation for text/html", async () => {
1220
+ const textRoute = Route.text(Effect.succeed("plain text"))
1221
+ const htmlRoute = Route.html(Effect.succeed("<div>html</div>"))
1222
+
1223
+ const merged = Route.merge(textRoute, htmlRoute)
1224
+ const route = merged.set[0]!
1225
+
1226
+ const request = HttpServerRequest.fromWeb(
1227
+ new Request("http://localhost/test", {
1228
+ headers: { Accept: "text/html" },
1229
+ }),
1230
+ )
1231
+
1232
+ const context: Route.RouteContext = {
1233
+ request,
1234
+ get url() {
1235
+ return new URL(request.url)
1236
+ },
1237
+ slots: {},
1238
+ next: () => Effect.void,
1239
+ }
1240
+
1241
+ const result = await Effect.runPromise(route.handler(context))
1242
+
1243
+ const webResponse = HttpServerResponse.toWeb(result)
1244
+ const text = await webResponse.text()
1245
+
1246
+ t.expect(text).toBe("<div>html</div>")
1247
+ t.expect(result.headers["content-type"]).toContain("text/html")
1248
+ })
1249
+
1250
+ t.it(
1251
+ "merged route does content negotiation for application/json",
1252
+ async () => {
1253
+ const textRoute = Route.text(Effect.succeed("plain text"))
1254
+ const jsonRoute = Route.json(Effect.succeed({ message: "json" }))
1255
+
1256
+ const merged = Route.merge(textRoute, jsonRoute)
1257
+ const route = merged.set[0]!
1258
+
1259
+ const request = HttpServerRequest.fromWeb(
1260
+ new Request("http://localhost/test", {
1261
+ headers: { Accept: "application/json" },
1262
+ }),
1263
+ )
1264
+
1265
+ const context: Route.RouteContext = {
1266
+ request,
1267
+ get url() {
1268
+ return new URL(request.url)
1269
+ },
1270
+ slots: {},
1271
+ next: () => Effect.void,
1272
+ }
1273
+
1274
+ const result = await Effect.runPromise(route.handler(context))
1275
+
1276
+ const webResponse = HttpServerResponse.toWeb(result)
1277
+ const text = await webResponse.text()
1278
+
1279
+ t.expect(text).toBe("{\"message\":\"json\"}")
1280
+ t.expect(result.headers["content-type"]).toContain("application/json")
1281
+ },
1282
+ )
1283
+
1284
+ t.it("merged route defaults to html for */* accept", async () => {
1285
+ const textRoute = Route.text(Effect.succeed("plain text"))
1286
+ const htmlRoute = Route.html(Effect.succeed("<div>html</div>"))
1287
+
1288
+ const merged = Route.merge(textRoute, htmlRoute)
1289
+ const route = merged.set[0]!
1290
+
1291
+ const request = HttpServerRequest.fromWeb(
1292
+ new Request("http://localhost/test", {
1293
+ headers: { Accept: "*/*" },
1294
+ }),
1295
+ )
1296
+
1297
+ const context: Route.RouteContext = {
1298
+ request,
1299
+ get url() {
1300
+ return new URL(request.url)
1301
+ },
1302
+ slots: {},
1303
+ next: () => Effect.void,
1304
+ }
1305
+
1306
+ const result = await Effect.runPromise(route.handler(context))
1307
+
1308
+ const webResponse = HttpServerResponse.toWeb(result)
1309
+ const text = await webResponse.text()
1310
+
1311
+ t.expect(text).toBe("<div>html</div>")
1312
+ })
1313
+
1314
+ t.it(
1315
+ "merged route defaults to first route when no Accept header",
1316
+ async () => {
1317
+ const textRoute = Route.text(Effect.succeed("plain text"))
1318
+ const htmlRoute = Route.html(Effect.succeed("<div>html</div>"))
1319
+
1320
+ const merged = Route.merge(textRoute, htmlRoute)
1321
+ const route = merged.set[0]!
1322
+
1323
+ const request = HttpServerRequest.fromWeb(
1324
+ new Request("http://localhost/test"),
1325
+ )
1326
+
1327
+ const context: Route.RouteContext = {
1328
+ request,
1329
+ get url() {
1330
+ return new URL(request.url)
1331
+ },
1332
+ slots: {},
1333
+ next: () => Effect.void,
1334
+ }
1335
+
1336
+ const result = await Effect.runPromise(route.handler(context))
1337
+
1338
+ const webResponse = HttpServerResponse.toWeb(result)
1339
+ const text = await webResponse.text()
1340
+
1341
+ t.expect(text).toBe("<div>html</div>")
1342
+ },
1343
+ )
1344
+ })