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,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
+ }