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,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taken from @effect/platform-bun@0.65.0
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
BunContext,
|
|
6
|
+
BunHttpPlatform as Platform,
|
|
7
|
+
BunMultipart as MultipartBun,
|
|
8
|
+
} from "@effect/platform-bun"
|
|
9
|
+
import * as Cookies from "@effect/platform/Cookies"
|
|
10
|
+
import * as Etag from "@effect/platform/Etag"
|
|
11
|
+
import * as FetchHttpClient from "@effect/platform/FetchHttpClient"
|
|
12
|
+
import type * as FileSystem from "@effect/platform/FileSystem"
|
|
13
|
+
import * as Headers from "@effect/platform/Headers"
|
|
14
|
+
import * as App from "@effect/platform/HttpApp"
|
|
15
|
+
import * as IncomingMessage from "@effect/platform/HttpIncomingMessage"
|
|
16
|
+
import type { HttpMethod } from "@effect/platform/HttpMethod"
|
|
17
|
+
import * as Server from "@effect/platform/HttpServer"
|
|
18
|
+
import * as Error from "@effect/platform/HttpServerError"
|
|
19
|
+
import * as ServerRequest from "@effect/platform/HttpServerRequest"
|
|
20
|
+
import type * as ServerResponse from "@effect/platform/HttpServerResponse"
|
|
21
|
+
import type * as Multipart from "@effect/platform/Multipart"
|
|
22
|
+
import type * as Path from "@effect/platform/Path"
|
|
23
|
+
import * as Socket from "@effect/platform/Socket"
|
|
24
|
+
import * as UrlParams from "@effect/platform/UrlParams"
|
|
25
|
+
import type {
|
|
26
|
+
ServeOptions,
|
|
27
|
+
Server as BunServer,
|
|
28
|
+
ServerWebSocket,
|
|
29
|
+
} from "bun"
|
|
30
|
+
import * as Config from "effect/Config"
|
|
31
|
+
import * as Deferred from "effect/Deferred"
|
|
32
|
+
import * as Effect from "effect/Effect"
|
|
33
|
+
import * as Exit from "effect/Exit"
|
|
34
|
+
import * as FiberSet from "effect/FiberSet"
|
|
35
|
+
import * as Inspectable from "effect/Inspectable"
|
|
36
|
+
import * as Layer from "effect/Layer"
|
|
37
|
+
import * as Option from "effect/Option"
|
|
38
|
+
import type { ReadonlyRecord } from "effect/Record"
|
|
39
|
+
import type * as Runtime from "effect/Runtime"
|
|
40
|
+
import type * as Scope from "effect/Scope"
|
|
41
|
+
import * as Stream from "effect/Stream"
|
|
42
|
+
|
|
43
|
+
/** @internal */
|
|
44
|
+
export const make = (
|
|
45
|
+
options: Omit<ServeOptions, "fetch" | "error">,
|
|
46
|
+
): Effect.Effect<Server.HttpServer, never, Scope.Scope> =>
|
|
47
|
+
Effect.gen(function*() {
|
|
48
|
+
const handlerStack: Array<
|
|
49
|
+
(request: Request, server: BunServer) => Response | Promise<Response>
|
|
50
|
+
> = [
|
|
51
|
+
function(_request, _server) {
|
|
52
|
+
return new Response("not found", { status: 404 })
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
const server = Bun.serve({
|
|
56
|
+
...options,
|
|
57
|
+
fetch: handlerStack[0],
|
|
58
|
+
// @ts-ignore
|
|
59
|
+
websocket: {
|
|
60
|
+
open(ws) {
|
|
61
|
+
Deferred.unsafeDone(ws.data.deferred, Exit.succeed(ws))
|
|
62
|
+
},
|
|
63
|
+
message(ws, message) {
|
|
64
|
+
ws.data.run(message)
|
|
65
|
+
},
|
|
66
|
+
close(ws, code, closeReason) {
|
|
67
|
+
Deferred.unsafeDone(
|
|
68
|
+
ws.data.closeDeferred,
|
|
69
|
+
Socket.defaultCloseCodeIsError(code)
|
|
70
|
+
? Exit.fail(
|
|
71
|
+
new Socket.SocketCloseError({
|
|
72
|
+
reason: "Close",
|
|
73
|
+
code,
|
|
74
|
+
closeReason,
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
: Exit.void,
|
|
78
|
+
)
|
|
79
|
+
},
|
|
80
|
+
} as any,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
yield* Effect.addFinalizer(() =>
|
|
84
|
+
Effect.sync(() => {
|
|
85
|
+
server.stop()
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return Server.make({
|
|
90
|
+
address: {
|
|
91
|
+
_tag: "TcpAddress",
|
|
92
|
+
port: server.port,
|
|
93
|
+
hostname: server.hostname,
|
|
94
|
+
} as any,
|
|
95
|
+
serve(httpApp, middleware) {
|
|
96
|
+
return Effect.gen(function*() {
|
|
97
|
+
const runFork = yield* FiberSet.makeRuntime<never>()
|
|
98
|
+
const runtime = yield* Effect.runtime<never>()
|
|
99
|
+
const app = App.toHandled(
|
|
100
|
+
httpApp,
|
|
101
|
+
(request, response) =>
|
|
102
|
+
Effect.sync(() => {
|
|
103
|
+
;(request as ServerRequestImpl).resolve(
|
|
104
|
+
makeResponse(request, response, runtime),
|
|
105
|
+
)
|
|
106
|
+
}),
|
|
107
|
+
middleware,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
function handler(request: Request, server: BunServer) {
|
|
111
|
+
return new Promise<Response>((resolve, _reject) => {
|
|
112
|
+
const fiber = runFork(Effect.provideService(
|
|
113
|
+
app,
|
|
114
|
+
ServerRequest.HttpServerRequest,
|
|
115
|
+
new ServerRequestImpl(
|
|
116
|
+
request,
|
|
117
|
+
resolve,
|
|
118
|
+
removeHost(request.url),
|
|
119
|
+
server,
|
|
120
|
+
),
|
|
121
|
+
))
|
|
122
|
+
request.signal.addEventListener("abort", () => {
|
|
123
|
+
runFork(fiber.interruptAsFork(Error.clientAbortFiberId))
|
|
124
|
+
}, { once: true })
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
yield* Effect.acquireRelease(
|
|
129
|
+
Effect.sync(() => {
|
|
130
|
+
handlerStack.push(handler)
|
|
131
|
+
server.reload({
|
|
132
|
+
fetch: handler,
|
|
133
|
+
// @ts-expect-error current effect veresion doesn't support routes
|
|
134
|
+
routes: options.routes,
|
|
135
|
+
} as ServeOptions)
|
|
136
|
+
}),
|
|
137
|
+
() =>
|
|
138
|
+
Effect.sync(() => {
|
|
139
|
+
handlerStack.pop()
|
|
140
|
+
server.reload(
|
|
141
|
+
{
|
|
142
|
+
fetch: handlerStack[handlerStack.length - 1],
|
|
143
|
+
// @ts-expect-error current effect veresion doesn't support routes
|
|
144
|
+
routes: options.routes,
|
|
145
|
+
} as ServeOptions,
|
|
146
|
+
)
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const makeResponse = (
|
|
155
|
+
request: ServerRequest.HttpServerRequest,
|
|
156
|
+
response: ServerResponse.HttpServerResponse,
|
|
157
|
+
runtime: Runtime.Runtime<never>,
|
|
158
|
+
): Response => {
|
|
159
|
+
const fields: {
|
|
160
|
+
headers: globalThis.Headers
|
|
161
|
+
status?: number
|
|
162
|
+
statusText?: string
|
|
163
|
+
} = {
|
|
164
|
+
headers: new globalThis.Headers(response.headers),
|
|
165
|
+
status: response.status,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!Cookies.isEmpty(response.cookies)) {
|
|
169
|
+
for (const header of Cookies.toSetCookieHeaders(response.cookies)) {
|
|
170
|
+
fields.headers.append("set-cookie", header)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (response.statusText !== undefined) {
|
|
175
|
+
fields.statusText = response.statusText
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (request.method === "HEAD") {
|
|
179
|
+
return new Response(undefined, fields)
|
|
180
|
+
}
|
|
181
|
+
const body = response.body
|
|
182
|
+
switch (body._tag) {
|
|
183
|
+
case "Empty": {
|
|
184
|
+
return new Response(undefined, fields)
|
|
185
|
+
}
|
|
186
|
+
case "Uint8Array":
|
|
187
|
+
case "Raw": {
|
|
188
|
+
return new Response(body.body as any, fields)
|
|
189
|
+
}
|
|
190
|
+
case "FormData": {
|
|
191
|
+
return new Response(body.formData as any, fields)
|
|
192
|
+
}
|
|
193
|
+
case "Stream": {
|
|
194
|
+
return new Response(
|
|
195
|
+
Stream.toReadableStreamRuntime(body.stream, runtime),
|
|
196
|
+
fields,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** @internal */
|
|
203
|
+
export const layerServer = (
|
|
204
|
+
options: Omit<ServeOptions, "fetch" | "error">,
|
|
205
|
+
) => Layer.scoped(Server.HttpServer, make(options))
|
|
206
|
+
|
|
207
|
+
/** @internal */
|
|
208
|
+
export const layerContext = Layer.mergeAll(
|
|
209
|
+
Platform.layer,
|
|
210
|
+
Etag.layerWeak,
|
|
211
|
+
BunContext.layer,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
/** @internal */
|
|
215
|
+
export const layer = (
|
|
216
|
+
options: Omit<ServeOptions, "fetch" | "error">,
|
|
217
|
+
) =>
|
|
218
|
+
Layer.mergeAll(
|
|
219
|
+
Layer.scoped(Server.HttpServer, make(options)),
|
|
220
|
+
layerContext,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
/** @internal */
|
|
224
|
+
export const layerTest = Server.layerTestClient.pipe(
|
|
225
|
+
Layer.provide(FetchHttpClient.layer.pipe(
|
|
226
|
+
Layer.provide(
|
|
227
|
+
Layer.succeed(FetchHttpClient.RequestInit, { keepalive: false }),
|
|
228
|
+
),
|
|
229
|
+
)),
|
|
230
|
+
Layer.provideMerge(layer({ port: 0 })),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
/** @internal */
|
|
234
|
+
export const layerConfig = (
|
|
235
|
+
options: Config.Config.Wrap<Omit<ServeOptions, "fetch" | "error">>,
|
|
236
|
+
) =>
|
|
237
|
+
Layer.mergeAll(
|
|
238
|
+
Layer.scoped(
|
|
239
|
+
Server.HttpServer,
|
|
240
|
+
Effect.flatMap(Config.unwrap(options), make),
|
|
241
|
+
),
|
|
242
|
+
layerContext,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
interface WebSocketContext {
|
|
246
|
+
readonly deferred: Deferred.Deferred<ServerWebSocket<WebSocketContext>>
|
|
247
|
+
readonly closeDeferred: Deferred.Deferred<void, Socket.SocketError>
|
|
248
|
+
readonly buffer: Array<Uint8Array | string>
|
|
249
|
+
run: (_: Uint8Array | string) => void
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function wsDefaultRun(this: WebSocketContext, _: Uint8Array | string) {
|
|
253
|
+
this.buffer.push(_)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
class ServerRequestImpl extends Inspectable.Class
|
|
257
|
+
implements ServerRequest.HttpServerRequest
|
|
258
|
+
{
|
|
259
|
+
readonly [ServerRequest.TypeId]: ServerRequest.TypeId
|
|
260
|
+
readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId
|
|
261
|
+
constructor(
|
|
262
|
+
readonly source: Request,
|
|
263
|
+
public resolve: (response: Response) => void,
|
|
264
|
+
readonly url: string,
|
|
265
|
+
private bunServer: BunServer,
|
|
266
|
+
public headersOverride?: Headers.Headers,
|
|
267
|
+
private remoteAddressOverride?: string,
|
|
268
|
+
) {
|
|
269
|
+
super()
|
|
270
|
+
this[ServerRequest.TypeId] = ServerRequest.TypeId
|
|
271
|
+
this[IncomingMessage.TypeId] = IncomingMessage.TypeId
|
|
272
|
+
}
|
|
273
|
+
toJSON(): unknown {
|
|
274
|
+
return IncomingMessage.inspect(this, {
|
|
275
|
+
_id: "@effect/platform/HttpServerRequest",
|
|
276
|
+
method: this.method,
|
|
277
|
+
url: this.originalUrl,
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
modify(
|
|
281
|
+
options: {
|
|
282
|
+
readonly url?: string | undefined
|
|
283
|
+
readonly headers?: Headers.Headers | undefined
|
|
284
|
+
readonly remoteAddress?: string | undefined
|
|
285
|
+
},
|
|
286
|
+
) {
|
|
287
|
+
return new ServerRequestImpl(
|
|
288
|
+
this.source,
|
|
289
|
+
this.resolve,
|
|
290
|
+
options.url ?? this.url,
|
|
291
|
+
this.bunServer,
|
|
292
|
+
options.headers ?? this.headersOverride,
|
|
293
|
+
options.remoteAddress ?? this.remoteAddressOverride,
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
get method(): HttpMethod {
|
|
297
|
+
return this.source.method.toUpperCase() as HttpMethod
|
|
298
|
+
}
|
|
299
|
+
get originalUrl() {
|
|
300
|
+
return this.source.url
|
|
301
|
+
}
|
|
302
|
+
get remoteAddress(): Option.Option<string> {
|
|
303
|
+
return this.remoteAddressOverride
|
|
304
|
+
? Option.some(this.remoteAddressOverride)
|
|
305
|
+
: Option.fromNullable(this.bunServer.requestIP(this.source)?.address)
|
|
306
|
+
}
|
|
307
|
+
get headers(): Headers.Headers {
|
|
308
|
+
this.headersOverride ??= Headers.fromInput(this.source.headers)
|
|
309
|
+
return this.headersOverride
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private cachedCookies: ReadonlyRecord<string, string> | undefined
|
|
313
|
+
get cookies() {
|
|
314
|
+
if (this.cachedCookies) {
|
|
315
|
+
return this.cachedCookies
|
|
316
|
+
}
|
|
317
|
+
return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "")
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
get stream(): Stream.Stream<Uint8Array, Error.RequestError> {
|
|
321
|
+
return this.source.body
|
|
322
|
+
? Stream.fromReadableStream(
|
|
323
|
+
() => this.source.body as any,
|
|
324
|
+
(cause) =>
|
|
325
|
+
new Error.RequestError({
|
|
326
|
+
request: this,
|
|
327
|
+
reason: "Decode",
|
|
328
|
+
cause,
|
|
329
|
+
}),
|
|
330
|
+
)
|
|
331
|
+
: Stream.fail(
|
|
332
|
+
new Error.RequestError({
|
|
333
|
+
request: this,
|
|
334
|
+
reason: "Decode",
|
|
335
|
+
description: "can not create stream from empty body",
|
|
336
|
+
}),
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private textEffect: Effect.Effect<string, Error.RequestError> | undefined
|
|
341
|
+
get text(): Effect.Effect<string, Error.RequestError> {
|
|
342
|
+
if (this.textEffect) {
|
|
343
|
+
return this.textEffect
|
|
344
|
+
}
|
|
345
|
+
this.textEffect = Effect.runSync(Effect.cached(
|
|
346
|
+
Effect.tryPromise({
|
|
347
|
+
try: () => this.source.text(),
|
|
348
|
+
catch: (cause) =>
|
|
349
|
+
new Error.RequestError({
|
|
350
|
+
request: this,
|
|
351
|
+
reason: "Decode",
|
|
352
|
+
cause,
|
|
353
|
+
}),
|
|
354
|
+
}),
|
|
355
|
+
))
|
|
356
|
+
return this.textEffect
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
get json(): Effect.Effect<unknown, Error.RequestError> {
|
|
360
|
+
return Effect.tryMap(this.text, {
|
|
361
|
+
try: (_) => JSON.parse(_) as unknown,
|
|
362
|
+
catch: (cause) =>
|
|
363
|
+
new Error.RequestError({
|
|
364
|
+
request: this,
|
|
365
|
+
reason: "Decode",
|
|
366
|
+
cause,
|
|
367
|
+
}),
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, Error.RequestError> {
|
|
372
|
+
return Effect.flatMap(this.text, (_) =>
|
|
373
|
+
Effect.try({
|
|
374
|
+
try: () => UrlParams.fromInput(new URLSearchParams(_)),
|
|
375
|
+
catch: (cause) =>
|
|
376
|
+
new Error.RequestError({
|
|
377
|
+
request: this,
|
|
378
|
+
reason: "Decode",
|
|
379
|
+
cause,
|
|
380
|
+
}),
|
|
381
|
+
}))
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private multipartEffect:
|
|
385
|
+
| Effect.Effect<
|
|
386
|
+
Multipart.Persisted,
|
|
387
|
+
Multipart.MultipartError,
|
|
388
|
+
Scope.Scope | FileSystem.FileSystem | Path.Path
|
|
389
|
+
>
|
|
390
|
+
| undefined
|
|
391
|
+
get multipart(): Effect.Effect<
|
|
392
|
+
Multipart.Persisted,
|
|
393
|
+
Multipart.MultipartError,
|
|
394
|
+
Scope.Scope | FileSystem.FileSystem | Path.Path
|
|
395
|
+
> {
|
|
396
|
+
if (this.multipartEffect) {
|
|
397
|
+
return this.multipartEffect
|
|
398
|
+
}
|
|
399
|
+
this.multipartEffect = Effect.runSync(Effect.cached(
|
|
400
|
+
MultipartBun.persisted(this.source),
|
|
401
|
+
))
|
|
402
|
+
return this.multipartEffect
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
get multipartStream(): Stream.Stream<
|
|
406
|
+
Multipart.Part,
|
|
407
|
+
Multipart.MultipartError
|
|
408
|
+
> {
|
|
409
|
+
return MultipartBun.stream(this.source)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private arrayBufferEffect:
|
|
413
|
+
| Effect.Effect<ArrayBuffer, Error.RequestError>
|
|
414
|
+
| undefined
|
|
415
|
+
get arrayBuffer(): Effect.Effect<ArrayBuffer, Error.RequestError> {
|
|
416
|
+
if (this.arrayBufferEffect) {
|
|
417
|
+
return this.arrayBufferEffect
|
|
418
|
+
}
|
|
419
|
+
this.arrayBufferEffect = Effect.runSync(Effect.cached(
|
|
420
|
+
Effect.tryPromise({
|
|
421
|
+
try: () => this.source.arrayBuffer(),
|
|
422
|
+
catch: (cause) =>
|
|
423
|
+
new Error.RequestError({
|
|
424
|
+
request: this,
|
|
425
|
+
reason: "Decode",
|
|
426
|
+
cause,
|
|
427
|
+
}),
|
|
428
|
+
}),
|
|
429
|
+
))
|
|
430
|
+
return this.arrayBufferEffect
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
get upgrade(): Effect.Effect<Socket.Socket, Error.RequestError> {
|
|
434
|
+
return Effect.flatMap(
|
|
435
|
+
Effect.all([
|
|
436
|
+
Deferred.make<ServerWebSocket<WebSocketContext>>(),
|
|
437
|
+
Deferred.make<void, Socket.SocketError>(),
|
|
438
|
+
Effect.makeSemaphore(1),
|
|
439
|
+
]),
|
|
440
|
+
([deferred, closeDeferred, semaphore]) =>
|
|
441
|
+
Effect.async<Socket.Socket, Error.RequestError>((resume) => {
|
|
442
|
+
const success = this.bunServer.upgrade<WebSocketContext>(
|
|
443
|
+
this.source,
|
|
444
|
+
{
|
|
445
|
+
data: {
|
|
446
|
+
deferred,
|
|
447
|
+
closeDeferred,
|
|
448
|
+
buffer: [],
|
|
449
|
+
run: wsDefaultRun,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
if (!success) {
|
|
454
|
+
resume(Effect.fail(
|
|
455
|
+
new Error.RequestError({
|
|
456
|
+
request: this,
|
|
457
|
+
reason: "Decode",
|
|
458
|
+
description: "Not an upgradeable ServerRequest",
|
|
459
|
+
}),
|
|
460
|
+
))
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
resume(Effect.map(Deferred.await(deferred), (ws) => {
|
|
464
|
+
const write = (chunk: Uint8Array | string | Socket.CloseEvent) =>
|
|
465
|
+
Effect.sync(() => {
|
|
466
|
+
if (typeof chunk === "string") {
|
|
467
|
+
ws.sendText(chunk)
|
|
468
|
+
} else if (Socket.isCloseEvent(chunk)) {
|
|
469
|
+
ws.close(chunk.code, chunk.reason)
|
|
470
|
+
} else {
|
|
471
|
+
ws.sendBinary(chunk)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return true
|
|
475
|
+
})
|
|
476
|
+
const writer = Effect.succeed(write)
|
|
477
|
+
const runRaw = <R, E, _>(
|
|
478
|
+
handler: (
|
|
479
|
+
_: Uint8Array | string,
|
|
480
|
+
) => Effect.Effect<_, E, R> | void,
|
|
481
|
+
): Effect.Effect<void, Socket.SocketError | E, R> =>
|
|
482
|
+
FiberSet.make<any, E>().pipe(
|
|
483
|
+
Effect.flatMap((set) =>
|
|
484
|
+
FiberSet.runtime(set)<R>().pipe(
|
|
485
|
+
Effect.flatMap((run) => {
|
|
486
|
+
function runRaw(data: Uint8Array | string) {
|
|
487
|
+
const result = handler(data)
|
|
488
|
+
if (Effect.isEffect(result)) {
|
|
489
|
+
run(result)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
ws.data.run = runRaw
|
|
493
|
+
ws.data.buffer.forEach(runRaw)
|
|
494
|
+
ws.data.buffer.length = 0
|
|
495
|
+
return FiberSet.join(set)
|
|
496
|
+
}),
|
|
497
|
+
)
|
|
498
|
+
),
|
|
499
|
+
Effect.scoped,
|
|
500
|
+
Effect.onExit((exit) =>
|
|
501
|
+
Effect.sync(() =>
|
|
502
|
+
ws.close(exit._tag === "Success" ? 1000 : 1011)
|
|
503
|
+
)
|
|
504
|
+
),
|
|
505
|
+
Effect.raceFirst(Deferred.await(closeDeferred)),
|
|
506
|
+
semaphore.withPermits(1),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
const encoder = new TextEncoder()
|
|
510
|
+
const run = <R, E, _>(
|
|
511
|
+
handler: (_: Uint8Array) => Effect.Effect<_, E, R> | void,
|
|
512
|
+
) =>
|
|
513
|
+
runRaw((data) =>
|
|
514
|
+
typeof data === "string"
|
|
515
|
+
? handler(encoder.encode(data))
|
|
516
|
+
: handler(data)
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
return Socket.Socket.of({
|
|
520
|
+
[Socket.TypeId]: Socket.TypeId,
|
|
521
|
+
run,
|
|
522
|
+
runRaw,
|
|
523
|
+
writer,
|
|
524
|
+
})
|
|
525
|
+
}))
|
|
526
|
+
}),
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const removeHost = (url: string) => {
|
|
532
|
+
if (url[0] === "/") {
|
|
533
|
+
return url
|
|
534
|
+
}
|
|
535
|
+
const index = url.indexOf("/", url.indexOf("//") + 2)
|
|
536
|
+
return index === -1 ? "/" : url.slice(index)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** @internal */
|
|
540
|
+
export const requestSource = (self: ServerRequest.HttpServerRequest) =>
|
|
541
|
+
(self as ServerRequestImpl).source
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as t from "bun:test"
|
|
2
|
+
import * as BunImportTrackerPlugin from "./BunImportTrackerPlugin.ts"
|
|
3
|
+
import * as BunVirtualFilesPlugin from "./BunVirtualFilesPlugin.ts"
|
|
4
|
+
|
|
5
|
+
const Files = {
|
|
6
|
+
"index.html": `
|
|
7
|
+
<!DOCTYPE html>
|
|
8
|
+
<html>
|
|
9
|
+
<head>
|
|
10
|
+
<title>Dashboard</title>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script src="client.tsx" />
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
17
|
+
`,
|
|
18
|
+
|
|
19
|
+
"client.ts": `
|
|
20
|
+
import { message } from "./config.ts"
|
|
21
|
+
|
|
22
|
+
alert(message)
|
|
23
|
+
`,
|
|
24
|
+
|
|
25
|
+
".config.ts": `
|
|
26
|
+
export const message = "Hello, World!"
|
|
27
|
+
`,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
t.it("virtual import", async () => {
|
|
31
|
+
const trackerPlugin = BunImportTrackerPlugin.make({
|
|
32
|
+
baseDir: Bun.fileURLToPath(import.meta.resolve("../..")),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
await Bun.build({
|
|
36
|
+
target: "bun",
|
|
37
|
+
entrypoints: [
|
|
38
|
+
import.meta.path,
|
|
39
|
+
],
|
|
40
|
+
plugins: [
|
|
41
|
+
trackerPlugin,
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
t
|
|
46
|
+
.expect(
|
|
47
|
+
[...trackerPlugin.state.entries()],
|
|
48
|
+
)
|
|
49
|
+
.toEqual([
|
|
50
|
+
[
|
|
51
|
+
"src/bun/BunImportTrackerPlugin.test.ts",
|
|
52
|
+
[
|
|
53
|
+
{
|
|
54
|
+
kind: "import-statement",
|
|
55
|
+
path: "bun:test",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
kind: "import-statement",
|
|
59
|
+
path: "src/bun/BunImportTrackerPlugin.ts",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
kind: "import-statement",
|
|
63
|
+
path: "src/bun/BunVirtualFilesPlugin.ts",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"src/bun/BunImportTrackerPlugin.ts",
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
kind: "import-statement",
|
|
72
|
+
path: "node:path",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
],
|
|
76
|
+
])
|
|
77
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BunPlugin,
|
|
3
|
+
type Import,
|
|
4
|
+
} from "bun"
|
|
5
|
+
import * as NPath from "node:path"
|
|
6
|
+
|
|
7
|
+
export type ImportMap = ReadonlyMap<string, Import[]>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tracks all imported modules.
|
|
11
|
+
* State can be accessed via 'virtual:import-tracker' module within a bundle
|
|
12
|
+
* or through `state` property returned by this function.
|
|
13
|
+
*/
|
|
14
|
+
export const make = (opts: {
|
|
15
|
+
includeNodeModules?: false
|
|
16
|
+
baseDir?: string
|
|
17
|
+
} = {}): BunPlugin & {
|
|
18
|
+
state: ImportMap
|
|
19
|
+
} => {
|
|
20
|
+
const foundImports: Map<string, Import[]> = new Map()
|
|
21
|
+
const baseDir = opts.baseDir ?? process.cwd()
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
name: "import tracker",
|
|
25
|
+
setup(build) {
|
|
26
|
+
const transpiler = new Bun.Transpiler({
|
|
27
|
+
loader: "tsx",
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Each module that goes through this onLoad callback
|
|
31
|
+
// will record its imports in `trackedImports`
|
|
32
|
+
build.onLoad({
|
|
33
|
+
filter: /\.(ts|js)x?$/,
|
|
34
|
+
}, async (args) => {
|
|
35
|
+
if (
|
|
36
|
+
!opts.includeNodeModules
|
|
37
|
+
&& args.path.includes("/node_modules/")
|
|
38
|
+
) {
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const contents = await Bun.file(args.path).arrayBuffer()
|
|
43
|
+
try {
|
|
44
|
+
const fileImport = transpiler.scanImports(contents)
|
|
45
|
+
const resolvedImports = fileImport.map(imp => {
|
|
46
|
+
const absoluteImportPath = NPath.resolve(
|
|
47
|
+
NPath.dirname(args.path),
|
|
48
|
+
// 'file' is a default namespace, trim it
|
|
49
|
+
imp.path.replace(/^file:/, ""),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...imp,
|
|
54
|
+
// keep all module identifiers with namespace intact
|
|
55
|
+
path: /(\w+):/.test(imp.path)
|
|
56
|
+
? imp.path
|
|
57
|
+
: NPath.relative(baseDir, absoluteImportPath),
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
foundImports.set(NPath.relative(baseDir, args.path), resolvedImports)
|
|
61
|
+
} catch (e) {
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
build.onResolve({
|
|
68
|
+
filter: /^virtual:import-tracker$/,
|
|
69
|
+
}, () => {
|
|
70
|
+
return {
|
|
71
|
+
namespace: "effect-start",
|
|
72
|
+
path: "virtual:import-tracker",
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
build.onLoad({
|
|
77
|
+
filter: /^virtual:import-tracker$/,
|
|
78
|
+
namespace: "effect-start",
|
|
79
|
+
}, async (args) => {
|
|
80
|
+
// Wait for all files to be loaded, ensuring
|
|
81
|
+
// that every file goes through the above `onLoad()` function
|
|
82
|
+
// and their imports tracked
|
|
83
|
+
await args.defer()
|
|
84
|
+
|
|
85
|
+
// Emit JSON containing the stats of each import
|
|
86
|
+
return {
|
|
87
|
+
contents: JSON.stringify(
|
|
88
|
+
Object.fromEntries(foundImports.entries()),
|
|
89
|
+
),
|
|
90
|
+
loader: "json",
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
state: foundImports,
|
|
96
|
+
}
|
|
97
|
+
}
|