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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/package.json +57 -0
- package/src/Bundle.ts +167 -0
- package/src/BundleFiles.ts +174 -0
- package/src/BundleHttp.test.ts +160 -0
- package/src/BundleHttp.ts +259 -0
- package/src/Commander.test.ts +1378 -0
- package/src/Commander.ts +672 -0
- package/src/Datastar.test.ts +267 -0
- package/src/Datastar.ts +68 -0
- package/src/Effect_HttpRouter.test.ts +570 -0
- package/src/EncryptedCookies.test.ts +427 -0
- package/src/EncryptedCookies.ts +451 -0
- package/src/FileHttpRouter.test.ts +207 -0
- package/src/FileHttpRouter.ts +122 -0
- package/src/FileRouter.ts +405 -0
- package/src/FileRouterCodegen.test.ts +598 -0
- package/src/FileRouterCodegen.ts +251 -0
- package/src/FileRouter_files.test.ts +64 -0
- package/src/FileRouter_path.test.ts +132 -0
- package/src/FileRouter_tree.test.ts +126 -0
- package/src/FileSystemExtra.ts +102 -0
- package/src/HttpAppExtra.ts +127 -0
- package/src/Hyper.ts +194 -0
- package/src/HyperHtml.test.ts +90 -0
- package/src/HyperHtml.ts +139 -0
- package/src/HyperNode.ts +37 -0
- package/src/JsModule.test.ts +14 -0
- package/src/JsModule.ts +116 -0
- package/src/PublicDirectory.test.ts +280 -0
- package/src/PublicDirectory.ts +108 -0
- package/src/Route.test.ts +873 -0
- package/src/Route.ts +992 -0
- package/src/Router.ts +80 -0
- package/src/SseHttpResponse.ts +55 -0
- package/src/Start.ts +133 -0
- package/src/StartApp.ts +43 -0
- package/src/StartHttp.ts +42 -0
- package/src/StreamExtra.ts +146 -0
- package/src/TestHttpClient.test.ts +54 -0
- package/src/TestHttpClient.ts +100 -0
- package/src/bun/BunBundle.test.ts +277 -0
- package/src/bun/BunBundle.ts +309 -0
- package/src/bun/BunBundle_imports.test.ts +50 -0
- package/src/bun/BunFullstackServer.ts +45 -0
- package/src/bun/BunFullstackServer_httpServer.ts +541 -0
- package/src/bun/BunImportTrackerPlugin.test.ts +77 -0
- package/src/bun/BunImportTrackerPlugin.ts +97 -0
- package/src/bun/BunTailwindPlugin.test.ts +335 -0
- package/src/bun/BunTailwindPlugin.ts +322 -0
- package/src/bun/BunVirtualFilesPlugin.ts +59 -0
- package/src/bun/index.ts +4 -0
- package/src/client/Overlay.ts +34 -0
- package/src/client/ScrollState.ts +120 -0
- package/src/client/index.ts +101 -0
- package/src/index.ts +24 -0
- package/src/jsx-datastar.d.ts +63 -0
- package/src/jsx-runtime.ts +23 -0
- package/src/jsx.d.ts +4402 -0
- package/src/testing.ts +55 -0
- package/src/x/cloudflare/CloudflareTunnel.ts +110 -0
- package/src/x/cloudflare/index.ts +1 -0
- package/src/x/datastar/Datastar.test.ts +267 -0
- package/src/x/datastar/Datastar.ts +68 -0
- package/src/x/datastar/index.ts +4 -0
- package/src/x/datastar/jsx-datastar.d.ts +63 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import * as Headers from "@effect/platform/Headers"
|
|
2
|
+
import type * as HttpApp from "@effect/platform/HttpApp"
|
|
3
|
+
import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
|
|
4
|
+
import { RouteNotFound } from "@effect/platform/HttpServerError"
|
|
5
|
+
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
6
|
+
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
7
|
+
import * as Context from "effect/Context"
|
|
8
|
+
import * as Effect from "effect/Effect"
|
|
9
|
+
import * as Function from "effect/Function"
|
|
10
|
+
import * as Option from "effect/Option"
|
|
11
|
+
import * as Scope from "effect/Scope"
|
|
12
|
+
import * as Stream from "effect/Stream"
|
|
13
|
+
import * as NPath from "node:path"
|
|
14
|
+
import * as NUrl from "node:url"
|
|
15
|
+
import * as Bundle from "./Bundle.ts"
|
|
16
|
+
import * as SseHttpResponse from "./SseHttpResponse.ts"
|
|
17
|
+
|
|
18
|
+
const DefaultBundleEndpoint = "/_bundle"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handles all entrypoints automatically.
|
|
22
|
+
* Serves HTML entrypoints without requiring explicit route definitions for each one.
|
|
23
|
+
* Examples:
|
|
24
|
+
* index.html -> /
|
|
25
|
+
* contact.html -> /contact
|
|
26
|
+
* about/index.html -> /about
|
|
27
|
+
*/
|
|
28
|
+
export function entrypoint(
|
|
29
|
+
uri?: string,
|
|
30
|
+
): HttpApp.Default<RouteNotFound, Bundle.ClientBundle> {
|
|
31
|
+
return Effect.gen(function*() {
|
|
32
|
+
uri = uri?.startsWith("file://") ? NUrl.fileURLToPath(uri) : uri
|
|
33
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
34
|
+
const bundle = yield* Bundle.ClientBundle
|
|
35
|
+
const requestPath = request.url.substring(1)
|
|
36
|
+
const pathAttempts = uri
|
|
37
|
+
? [
|
|
38
|
+
// try paths from all parent directories in case absolute path is passed,
|
|
39
|
+
// like it is the case for `import(f, { type: "file" })`
|
|
40
|
+
...uri
|
|
41
|
+
.split(NPath.sep)
|
|
42
|
+
.map((_, i, a) => NPath.join(...a.slice(i))),
|
|
43
|
+
]
|
|
44
|
+
: [
|
|
45
|
+
requestPath ? `${requestPath}.html` : null,
|
|
46
|
+
requestPath ? `${requestPath}/index.html` : null,
|
|
47
|
+
requestPath === "" ? "index.html" : "",
|
|
48
|
+
]
|
|
49
|
+
const artifact = pathAttempts
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.map(path => bundle.getArtifact(path as string))
|
|
52
|
+
.find(Boolean)
|
|
53
|
+
|
|
54
|
+
if (artifact) {
|
|
55
|
+
return yield* renderBlob(artifact)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return yield* Effect.fail(
|
|
59
|
+
new RouteNotFound({
|
|
60
|
+
request,
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function httpApp(
|
|
67
|
+
opts?: { urlPrefix?: string },
|
|
68
|
+
): HttpApp.Default<
|
|
69
|
+
RouteNotFound,
|
|
70
|
+
Scope.Scope | Bundle.ClientBundle
|
|
71
|
+
> {
|
|
72
|
+
return toHttpApp(Bundle.ClientBundle, opts)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const toHttpApp = <E, R>(
|
|
76
|
+
bundleTag: Effect.Effect<Bundle.BundleContext, E, R>,
|
|
77
|
+
opts?: { urlPrefix?: string },
|
|
78
|
+
): Effect.Effect<
|
|
79
|
+
HttpServerResponse.HttpServerResponse,
|
|
80
|
+
RouteNotFound | E,
|
|
81
|
+
HttpServerRequest.HttpServerRequest | R
|
|
82
|
+
> => {
|
|
83
|
+
return Effect.gen(function*() {
|
|
84
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
85
|
+
const bundle = yield* bundleTag
|
|
86
|
+
const path = opts?.urlPrefix && request.url.startsWith(opts.urlPrefix + "/")
|
|
87
|
+
? request.url.substring(opts.urlPrefix.length + 1)
|
|
88
|
+
: request.url.substring(1)
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Expose manifest that contains information about the bundle.
|
|
92
|
+
*/
|
|
93
|
+
if (path === "manifest.json") {
|
|
94
|
+
return HttpServerResponse.text(
|
|
95
|
+
JSON.stringify(
|
|
96
|
+
{
|
|
97
|
+
entrypoints: bundle.entrypoints,
|
|
98
|
+
artifacts: bundle.artifacts,
|
|
99
|
+
},
|
|
100
|
+
undefined,
|
|
101
|
+
2,
|
|
102
|
+
),
|
|
103
|
+
{
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Expose events endpoint if available.
|
|
113
|
+
* Useful for development to implement live reload.
|
|
114
|
+
*/
|
|
115
|
+
if (bundle.events && path === "events") {
|
|
116
|
+
return yield* SseHttpResponse.make<Bundle.BundleEvent>(
|
|
117
|
+
Stream.fromPubSub(bundle.events),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const artifact = bundle.artifacts.find((a) => a.path === path)
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Expose artifacts.
|
|
125
|
+
*/
|
|
126
|
+
if (artifact) {
|
|
127
|
+
const artifactBlob = bundle.getArtifact(path)!
|
|
128
|
+
|
|
129
|
+
return yield* renderBlob(artifactBlob)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return yield* Effect.fail(
|
|
133
|
+
new RouteNotFound({
|
|
134
|
+
request,
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Render HTML to a string.
|
|
142
|
+
* Useful for SSR.
|
|
143
|
+
*/
|
|
144
|
+
export function renderPromise(
|
|
145
|
+
clientBundle: Bundle.Tag,
|
|
146
|
+
render: (
|
|
147
|
+
request: Request,
|
|
148
|
+
resolve: (url: string) => string,
|
|
149
|
+
) => Promise<Response>,
|
|
150
|
+
) {
|
|
151
|
+
return Effect.gen(function*() {
|
|
152
|
+
const bundle = yield* clientBundle
|
|
153
|
+
const req = yield* HttpServerRequest.HttpServerRequest
|
|
154
|
+
const fetchReq = req.source as Request
|
|
155
|
+
|
|
156
|
+
// TODO: add support for file:// urls
|
|
157
|
+
// this will require handling source base path
|
|
158
|
+
const resolve = (url: string): string => {
|
|
159
|
+
const path = url.startsWith("file://")
|
|
160
|
+
? NUrl.fileURLToPath(url)
|
|
161
|
+
: url
|
|
162
|
+
const publicBase = "/.bundle"
|
|
163
|
+
const publicPath = bundle.resolve(path)
|
|
164
|
+
|
|
165
|
+
return NPath.join(publicBase, publicPath ?? path)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const output = yield* Effect.tryPromise({
|
|
169
|
+
try: () =>
|
|
170
|
+
render(
|
|
171
|
+
fetchReq,
|
|
172
|
+
resolve,
|
|
173
|
+
),
|
|
174
|
+
catch: (e) =>
|
|
175
|
+
new Bundle.BundleError({
|
|
176
|
+
message: "Failed to render",
|
|
177
|
+
cause: e,
|
|
178
|
+
}),
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
return yield* HttpServerResponse.raw(output.body, {
|
|
182
|
+
status: output.status,
|
|
183
|
+
statusText: output.statusText,
|
|
184
|
+
headers: Headers.fromInput(output.headers as any),
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const renderBlob = (blob: Blob) => {
|
|
190
|
+
return Effect.gen(function*() {
|
|
191
|
+
const bytes = yield* Effect
|
|
192
|
+
.promise(() => blob.arrayBuffer())
|
|
193
|
+
.pipe(
|
|
194
|
+
Effect.andThen(v => new Uint8Array(v)),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return HttpServerResponse.uint8Array(bytes, {
|
|
198
|
+
headers: {
|
|
199
|
+
"content-type": blob.type,
|
|
200
|
+
"content-length": String(blob.size),
|
|
201
|
+
"cache-control": "public, max-age=3600",
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Exposes bundle assets via HTTP routes.
|
|
209
|
+
* Serves bundle artifacts, manifest.json, and events endpoint at the specified path.
|
|
210
|
+
*/
|
|
211
|
+
export function withAssets(
|
|
212
|
+
opts?: { path?: string },
|
|
213
|
+
) {
|
|
214
|
+
const path = opts?.path ?? DefaultBundleEndpoint
|
|
215
|
+
|
|
216
|
+
return HttpMiddleware.make((app) =>
|
|
217
|
+
Effect.gen(function*() {
|
|
218
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
219
|
+
|
|
220
|
+
if (request.url.startsWith(path + "/")) {
|
|
221
|
+
return yield* toHttpApp(Bundle.ClientBundle, { urlPrefix: path })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return yield* app
|
|
225
|
+
})
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @see {entrypoint}
|
|
231
|
+
*/
|
|
232
|
+
export function withEntrypoints() {
|
|
233
|
+
return HttpMiddleware.make((app) =>
|
|
234
|
+
Effect.gen(function*() {
|
|
235
|
+
const entrypointResponse = yield* entrypoint().pipe(
|
|
236
|
+
Effect.option,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if (Option.isSome(entrypointResponse)) {
|
|
240
|
+
return entrypointResponse.value
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return yield* app
|
|
244
|
+
})
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Combines both withAssets and withEntrypoints.
|
|
250
|
+
* Provides complete bundle HTTP functionality in a single function call.
|
|
251
|
+
*/
|
|
252
|
+
export function withBundle(
|
|
253
|
+
opts?: { path?: string },
|
|
254
|
+
) {
|
|
255
|
+
return Function.flow(
|
|
256
|
+
withAssets(opts),
|
|
257
|
+
withEntrypoints(),
|
|
258
|
+
)
|
|
259
|
+
}
|