effect-start 0.9.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/package.json +57 -0
  4. package/src/Bundle.ts +167 -0
  5. package/src/BundleFiles.ts +174 -0
  6. package/src/BundleHttp.test.ts +160 -0
  7. package/src/BundleHttp.ts +259 -0
  8. package/src/Commander.test.ts +1378 -0
  9. package/src/Commander.ts +672 -0
  10. package/src/Datastar.test.ts +267 -0
  11. package/src/Datastar.ts +68 -0
  12. package/src/Effect_HttpRouter.test.ts +570 -0
  13. package/src/EncryptedCookies.test.ts +427 -0
  14. package/src/EncryptedCookies.ts +451 -0
  15. package/src/FileHttpRouter.test.ts +207 -0
  16. package/src/FileHttpRouter.ts +122 -0
  17. package/src/FileRouter.ts +405 -0
  18. package/src/FileRouterCodegen.test.ts +598 -0
  19. package/src/FileRouterCodegen.ts +251 -0
  20. package/src/FileRouter_files.test.ts +64 -0
  21. package/src/FileRouter_path.test.ts +132 -0
  22. package/src/FileRouter_tree.test.ts +126 -0
  23. package/src/FileSystemExtra.ts +102 -0
  24. package/src/HttpAppExtra.ts +127 -0
  25. package/src/Hyper.ts +194 -0
  26. package/src/HyperHtml.test.ts +90 -0
  27. package/src/HyperHtml.ts +139 -0
  28. package/src/HyperNode.ts +37 -0
  29. package/src/JsModule.test.ts +14 -0
  30. package/src/JsModule.ts +116 -0
  31. package/src/PublicDirectory.test.ts +280 -0
  32. package/src/PublicDirectory.ts +108 -0
  33. package/src/Route.test.ts +873 -0
  34. package/src/Route.ts +992 -0
  35. package/src/Router.ts +80 -0
  36. package/src/SseHttpResponse.ts +55 -0
  37. package/src/Start.ts +133 -0
  38. package/src/StartApp.ts +43 -0
  39. package/src/StartHttp.ts +42 -0
  40. package/src/StreamExtra.ts +146 -0
  41. package/src/TestHttpClient.test.ts +54 -0
  42. package/src/TestHttpClient.ts +100 -0
  43. package/src/bun/BunBundle.test.ts +277 -0
  44. package/src/bun/BunBundle.ts +309 -0
  45. package/src/bun/BunBundle_imports.test.ts +50 -0
  46. package/src/bun/BunFullstackServer.ts +45 -0
  47. package/src/bun/BunFullstackServer_httpServer.ts +541 -0
  48. package/src/bun/BunImportTrackerPlugin.test.ts +77 -0
  49. package/src/bun/BunImportTrackerPlugin.ts +97 -0
  50. package/src/bun/BunTailwindPlugin.test.ts +335 -0
  51. package/src/bun/BunTailwindPlugin.ts +322 -0
  52. package/src/bun/BunVirtualFilesPlugin.ts +59 -0
  53. package/src/bun/index.ts +4 -0
  54. package/src/client/Overlay.ts +34 -0
  55. package/src/client/ScrollState.ts +120 -0
  56. package/src/client/index.ts +101 -0
  57. package/src/index.ts +24 -0
  58. package/src/jsx-datastar.d.ts +63 -0
  59. package/src/jsx-runtime.ts +23 -0
  60. package/src/jsx.d.ts +4402 -0
  61. package/src/testing.ts +55 -0
  62. package/src/x/cloudflare/CloudflareTunnel.ts +110 -0
  63. package/src/x/cloudflare/index.ts +1 -0
  64. package/src/x/datastar/Datastar.test.ts +267 -0
  65. package/src/x/datastar/Datastar.ts +68 -0
  66. package/src/x/datastar/index.ts +4 -0
  67. package/src/x/datastar/jsx-datastar.d.ts +63 -0
@@ -0,0 +1,280 @@
1
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
2
+ import * as t from "bun:test"
3
+ import { MemoryFileSystem } from "effect-memfs"
4
+ import {
5
+ effectFn,
6
+ TestHttpClient,
7
+ } from "effect-start"
8
+ import * as Effect from "effect/Effect"
9
+ import * as PublicDirectory from "./PublicDirectory.ts"
10
+
11
+ const TestFiles = {
12
+ "/test-public/index.html": "<html><body>Hello World</body></html>",
13
+ "/test-public/style.css": "body { color: red; }",
14
+ "/test-public/script.js": "console.log('hello');",
15
+ "/test-public/data.json": "{\"message\": \"test\"}",
16
+ "/test-public/image.png": "fake-png-data",
17
+ "/test-public/nested/file.txt": "nested content",
18
+ }
19
+
20
+ const effect = effectFn()
21
+
22
+ t.it("serves index.html for root path", () => {
23
+ effect(function*() {
24
+ const app = PublicDirectory.make({ directory: "/test-public" })
25
+ const Client = TestHttpClient.make(app)
26
+
27
+ const res = yield* Client.get("/").pipe(
28
+ Effect.provide(MemoryFileSystem.layerWith(TestFiles)),
29
+ )
30
+
31
+ t
32
+ .expect(
33
+ res.status,
34
+ )
35
+ .toBe(200)
36
+
37
+ const body = yield* res.text
38
+ t
39
+ .expect(
40
+ body,
41
+ )
42
+ .toBe("<html><body>Hello World</body></html>")
43
+
44
+ t
45
+ .expect(
46
+ res.headers["content-type"],
47
+ )
48
+ .toBe("text/html")
49
+ })
50
+ })
51
+
52
+ t.it("serves CSS files with correct content type", () => {
53
+ effect(function*() {
54
+ const app = PublicDirectory.make({ directory: "/test-public" })
55
+ const Client = TestHttpClient.make(app)
56
+
57
+ const res = yield* Client.get("/style.css").pipe(
58
+ Effect.provide(MemoryFileSystem.layerWith(TestFiles)),
59
+ )
60
+
61
+ t
62
+ .expect(
63
+ res.status,
64
+ )
65
+ .toBe(200)
66
+
67
+ const body = yield* res.text
68
+ t
69
+ .expect(
70
+ body,
71
+ )
72
+ .toBe("body { color: red; }")
73
+
74
+ t
75
+ .expect(
76
+ res.headers["content-type"],
77
+ )
78
+ .toBe("text/css")
79
+ })
80
+ })
81
+
82
+ t.it("serves JavaScript files with correct content type", () => {
83
+ effect(function*() {
84
+ const app = PublicDirectory.make({ directory: "/test-public" })
85
+ const Client = TestHttpClient.make(app)
86
+
87
+ const res = yield* Client.get("/script.js").pipe(
88
+ Effect.provide(MemoryFileSystem.layerWith(TestFiles)),
89
+ )
90
+
91
+ t
92
+ .expect(
93
+ res.status,
94
+ )
95
+ .toBe(200)
96
+
97
+ const body = yield* res.text
98
+ t
99
+ .expect(
100
+ body,
101
+ )
102
+ .toBe("console.log('hello');")
103
+
104
+ t
105
+ .expect(
106
+ res.headers["content-type"],
107
+ )
108
+ .toBe("application/javascript")
109
+ })
110
+ })
111
+
112
+ t.it("serves JSON files with correct content type", () => {
113
+ effect(function*() {
114
+ const app = PublicDirectory.make({ directory: "/test-public" })
115
+ const Client = TestHttpClient.make(app)
116
+
117
+ const res = yield* Client.get("/data.json").pipe(
118
+ Effect.provide(MemoryFileSystem.layerWith(TestFiles)),
119
+ )
120
+
121
+ t
122
+ .expect(
123
+ res.status,
124
+ )
125
+ .toBe(200)
126
+
127
+ const body = yield* res.text
128
+ t
129
+ .expect(
130
+ body,
131
+ )
132
+ .toBe("{\"message\": \"test\"}")
133
+
134
+ t
135
+ .expect(
136
+ res.headers["content-type"],
137
+ )
138
+ .toBe("application/json")
139
+ })
140
+ })
141
+
142
+ t.it("serves nested files", () => {
143
+ effect(function*() {
144
+ const app = PublicDirectory.make({ directory: "/test-public" })
145
+ const Client = TestHttpClient.make(app)
146
+
147
+ const res = yield* Client.get("/nested/file.txt").pipe(
148
+ Effect.provide(MemoryFileSystem.layerWith(TestFiles)),
149
+ )
150
+
151
+ t
152
+ .expect(
153
+ res.status,
154
+ )
155
+ .toBe(200)
156
+
157
+ const body = yield* res.text
158
+ t
159
+ .expect(
160
+ body,
161
+ )
162
+ .toBe("nested content")
163
+
164
+ t
165
+ .expect(
166
+ res.headers["content-type"],
167
+ )
168
+ .toBe("text/plain")
169
+ })
170
+ })
171
+
172
+ t.it("returns 404 for non-existent files", () => {
173
+ effect(function*() {
174
+ const app = PublicDirectory.make({ directory: "/test-public" })
175
+ const Client = TestHttpClient.make(app)
176
+
177
+ const res = yield* Client.get("/nonexistent.txt").pipe(
178
+ Effect.provide(MemoryFileSystem.layerWith(TestFiles)),
179
+ Effect.catchTag(
180
+ "RouteNotFound",
181
+ () => HttpServerResponse.empty({ status: 404 }),
182
+ ),
183
+ )
184
+
185
+ t
186
+ .expect(
187
+ res.status,
188
+ )
189
+ .toBe(404)
190
+ })
191
+ })
192
+
193
+ t.it("prevents directory traversal attacks", () => {
194
+ effect(function*() {
195
+ const app = PublicDirectory.make({ directory: "/test-public" })
196
+ const Client = TestHttpClient.make(app)
197
+
198
+ const res = yield* Client.get("/../../../etc/passwd").pipe(
199
+ Effect.provide(MemoryFileSystem.layerWith(TestFiles)),
200
+ Effect.catchTag(
201
+ "RouteNotFound",
202
+ () => HttpServerResponse.empty({ status: 404 }),
203
+ ),
204
+ )
205
+
206
+ t
207
+ .expect(
208
+ res.status,
209
+ )
210
+ .toBe(404)
211
+ })
212
+ })
213
+
214
+ t.it("works with custom prefix", () => {
215
+ effect(function*() {
216
+ const app = PublicDirectory.make({
217
+ directory: "/test-public",
218
+ prefix: "/static",
219
+ })
220
+ const Client = TestHttpClient.make(app)
221
+
222
+ const res = yield* Client.get("/static/style.css").pipe(
223
+ Effect.provide(MemoryFileSystem.layerWith(TestFiles)),
224
+ )
225
+
226
+ t
227
+ .expect(
228
+ res.status,
229
+ )
230
+ .toBe(200)
231
+
232
+ const body = yield* res.text
233
+ t
234
+ .expect(
235
+ body,
236
+ )
237
+ .toBe("body { color: red; }")
238
+ })
239
+ })
240
+
241
+ t.it("ignores requests without prefix when prefix is set", () => {
242
+ effect(function*() {
243
+ const app = PublicDirectory.make({
244
+ directory: "/test-public",
245
+ prefix: "/static",
246
+ })
247
+ const Client = TestHttpClient.make(app)
248
+
249
+ const res = yield* Client.get("/style.css").pipe(
250
+ Effect.provide(MemoryFileSystem.layerWith(TestFiles)),
251
+ Effect.catchTag(
252
+ "RouteNotFound",
253
+ () => HttpServerResponse.empty({ status: 404 }),
254
+ ),
255
+ )
256
+
257
+ t
258
+ .expect(
259
+ res.status,
260
+ )
261
+ .toBe(404)
262
+ })
263
+ })
264
+
265
+ t.it("sets cache control headers", () => {
266
+ effect(function*() {
267
+ const app = PublicDirectory.make({ directory: "/test-public" })
268
+ const Client = TestHttpClient.make(app)
269
+
270
+ const res = yield* Client.get("/style.css").pipe(
271
+ Effect.provide(MemoryFileSystem.layerWith(TestFiles)),
272
+ )
273
+
274
+ t
275
+ .expect(
276
+ res.headers["cache-control"],
277
+ )
278
+ .toBe("public, max-age=3600")
279
+ })
280
+ })
@@ -0,0 +1,108 @@
1
+ import * as FileSystem from "@effect/platform/FileSystem"
2
+ import * as HttpApp from "@effect/platform/HttpApp"
3
+ import { RouteNotFound } from "@effect/platform/HttpServerError"
4
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
5
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
6
+ import * as Effect from "effect/Effect"
7
+ import * as Function from "effect/Function"
8
+ import * as NPath from "node:path"
9
+
10
+ export interface PublicDirectoryOptions {
11
+ readonly directory?: string
12
+ readonly prefix?: string
13
+ }
14
+
15
+ export const make = (
16
+ options: PublicDirectoryOptions = {},
17
+ ): HttpApp.Default<RouteNotFound, FileSystem.FileSystem> =>
18
+ Effect.gen(function*() {
19
+ const request = yield* HttpServerRequest.HttpServerRequest
20
+ const fs = yield* FileSystem.FileSystem
21
+
22
+ const directory = options.directory ?? NPath.join(process.cwd(), "public")
23
+ const prefix = options.prefix ?? ""
24
+
25
+ let pathname = request.url
26
+
27
+ if (prefix && !pathname.startsWith(prefix)) {
28
+ return yield* Effect.fail(new RouteNotFound({ request }))
29
+ }
30
+
31
+ if (prefix) {
32
+ pathname = pathname.slice(prefix.length)
33
+ }
34
+
35
+ if (pathname.startsWith("/")) {
36
+ pathname = pathname.slice(1)
37
+ }
38
+
39
+ if (pathname === "") {
40
+ pathname = "index.html"
41
+ }
42
+
43
+ const filePath = NPath.join(directory, pathname)
44
+
45
+ if (!filePath.startsWith(directory)) {
46
+ return yield* Effect.fail(new RouteNotFound({ request }))
47
+ }
48
+
49
+ const exists = yield* Function.pipe(
50
+ fs.exists(filePath),
51
+ Effect.catchAll(() => Effect.succeed(false)),
52
+ )
53
+
54
+ if (!exists) {
55
+ return yield* Effect.fail(new RouteNotFound({ request }))
56
+ }
57
+
58
+ const stat = yield* Function.pipe(
59
+ fs.stat(filePath),
60
+ Effect.catchAll(() => Effect.fail(new RouteNotFound({ request }))),
61
+ )
62
+
63
+ if (stat.type !== "File") {
64
+ return yield* Effect.fail(new RouteNotFound({ request }))
65
+ }
66
+
67
+ const content = yield* Function.pipe(
68
+ fs.readFile(filePath),
69
+ Effect.catchAll(() => Effect.fail(new RouteNotFound({ request }))),
70
+ )
71
+
72
+ const mimeType = getMimeType(filePath)
73
+
74
+ return HttpServerResponse.uint8Array(content, {
75
+ headers: {
76
+ "Content-Type": mimeType,
77
+ "Cache-Control": "public, max-age=3600",
78
+ },
79
+ })
80
+ })
81
+
82
+ function getMimeType(filePath: string): string {
83
+ const ext = NPath.extname(filePath).toLowerCase()
84
+
85
+ const mimeTypes: Record<string, string> = {
86
+ ".html": "text/html",
87
+ ".htm": "text/html",
88
+ ".css": "text/css",
89
+ ".js": "application/javascript",
90
+ ".mjs": "application/javascript",
91
+ ".json": "application/json",
92
+ ".png": "image/png",
93
+ ".jpg": "image/jpeg",
94
+ ".jpeg": "image/jpeg",
95
+ ".gif": "image/gif",
96
+ ".svg": "image/svg+xml",
97
+ ".ico": "image/x-icon",
98
+ ".txt": "text/plain",
99
+ ".pdf": "application/pdf",
100
+ ".woff": "font/woff",
101
+ ".woff2": "font/woff2",
102
+ ".ttf": "font/ttf",
103
+ ".otf": "font/otf",
104
+ ".eot": "application/vnd.ms-fontobject",
105
+ }
106
+
107
+ return mimeTypes[ext] ?? "application/octet-stream"
108
+ }