effect-start 0.13.1 → 0.14.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 (32) hide show
  1. package/package.json +9 -9
  2. package/src/Bundle.ts +0 -35
  3. package/src/BundleHttp.test.ts +4 -6
  4. package/src/BundleHttp.ts +2 -1
  5. package/src/Effect_HttpRouter.test.ts +2 -3
  6. package/src/FileHttpRouter.test.ts +2 -2
  7. package/src/FileRouterCodegen.test.ts +1 -1
  8. package/src/FileRouter_files.test.ts +1 -1
  9. package/src/HttpAppExtra.test.ts +1 -1
  10. package/src/Start.ts +0 -34
  11. package/src/StartApp.ts +20 -16
  12. package/src/bun/BunBundle.test.ts +1 -1
  13. package/src/bun/BunBundle_imports.test.ts +2 -2
  14. package/src/bun/BunRoute_bundles.test.ts +1 -1
  15. package/src/{SseHttpResponse.ts → experimental/SseHttpResponse.ts} +2 -1
  16. package/src/experimental/index.ts +2 -0
  17. package/src/index.ts +2 -20
  18. package/src/middlewares/index.ts +1 -0
  19. package/src/{TestHttpClient.test.ts → testing/TestHttpClient.test.ts} +1 -1
  20. package/src/{TestHttpClient.ts → testing/TestHttpClient.ts} +0 -1
  21. package/src/testing/index.ts +3 -0
  22. package/src/x/tailwind/TailwindPlugin.ts +23 -17
  23. package/src/JsModule.test.ts +0 -14
  24. package/src/JsModule.ts +0 -116
  25. package/src/PublicDirectory.test.ts +0 -280
  26. package/src/PublicDirectory.ts +0 -108
  27. package/src/StartHttp.ts +0 -42
  28. /package/src/{EncryptedCookies.test.ts → experimental/EncryptedCookies.test.ts} +0 -0
  29. /package/src/{EncryptedCookies.ts → experimental/EncryptedCookies.ts} +0 -0
  30. /package/src/{TestLogger.test.ts → testing/TestLogger.test.ts} +0 -0
  31. /package/src/{TestLogger.ts → testing/TestLogger.ts} +0 -0
  32. /package/src/{testing.ts → testing/utils.ts} +0 -0
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "name": "effect-start",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "exports": {
7
7
  ".": "./src/index.ts",
8
+ "./StartApp": "./src/StartApp.ts",
8
9
  "./FileRouter": "./src/FileRouter.ts",
9
10
  "./Route": "./src/Route.ts",
10
- "./EncryptedCookies": "./src/EncryptedCookies.ts",
11
11
  "./bun": "./src/bun/index.ts",
12
12
  "./client": "./src/client/index.ts",
13
- "./FileSystemExtra": "./src/FileSystemExtra.ts",
14
- "./package.json": "./package.json",
15
- "./jsx-runtime": "./src/jsx-runtime.ts",
16
- "./jsx-dev-runtime": "./src/jsx-runtime.ts",
17
- "./hyper": "./src/hyper/index.ts",
13
+ "./testing": "./src/testing/index.ts",
18
14
  "./x/*": "./src/x/*/index.ts",
19
15
  "./x/tailwind/plugin": "./src/x/tailwind/plugin.ts",
20
- "./middlewares/BasicAuthMiddleware": "./src/middlewares/BasicAuthMiddleware.ts",
21
- "./assets.d.ts": "./src/assets.d.ts"
16
+ "./middlewares": "./src/middlewares/index.ts",
17
+ "./experimental": "./src/experimental/index.ts",
18
+ "./assets.d.ts": "./src/assets.d.ts",
19
+ "./jsx-runtime": "./src/jsx-runtime.ts",
20
+ "./jsx-dev-runtime": "./src/jsx-runtime.ts",
21
+ "./package.json": "./package.json"
22
22
  },
23
23
  "scripts": {
24
24
  "format": "bunx dprint fmt",
package/src/Bundle.ts CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  PubSub,
7
7
  } from "effect"
8
8
  import * as Schema from "effect/Schema"
9
- import { importBlob } from "./JsModule.ts"
10
9
 
11
10
  export const BundleEntrypointMetaKey: unique symbol = Symbol.for(
12
11
  "effect-start/BundleEntrypointMetaKey",
@@ -131,37 +130,3 @@ export type Tag = Context.Tag<
131
130
 
132
131
  export class ClientBundle extends Tag("ClientBundle")<ClientBundle>() {}
133
132
  export class ServerBundle extends Tag("ServerBundle")<ServerBundle>() {}
134
-
135
- /**
136
- * Lodas a bundle as a javascript module.
137
- * Bundle must have only one entrypoint.
138
- */
139
- export function load<M>(
140
- bundle: Effect.Effect<BundleContext, BundleError>,
141
- ): Effect.Effect<M, BundleError> {
142
- return Effect.gen(function*() {
143
- const context = yield* bundle
144
- const [artifact, ...rest] = Object.values(context.entrypoints)
145
-
146
- if (rest.length > 0) {
147
- return yield* Effect.fail(
148
- new BundleError({
149
- message: "Multiple entrypoints are not supported in load()",
150
- }),
151
- )
152
- }
153
-
154
- return yield* Effect.tryPromise({
155
- try: () => {
156
- const blob = context.getArtifact(artifact)
157
-
158
- return importBlob<M>(blob!)
159
- },
160
- catch: (e) =>
161
- new BundleError({
162
- message: "Failed to load entrypoint",
163
- cause: e,
164
- }),
165
- })
166
- })
167
- }
@@ -1,16 +1,14 @@
1
1
  import * as HttpRouter from "@effect/platform/HttpRouter"
2
2
  import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
3
3
  import * as t from "bun:test"
4
- import {
5
- Bundle,
6
- BundleHttp,
7
- effectFn,
8
- TestHttpClient,
9
- } from "effect-start"
10
4
  import * as Effect from "effect/Effect"
11
5
  import * as Layer from "effect/Layer"
12
6
  import IndexHtml from "../static/react-dashboard.html" with { type: "file" }
13
7
  import * as BunBundle from "./bun/BunBundle.ts"
8
+ import * as Bundle from "./Bundle.ts"
9
+ import * as BundleHttp from "./BundleHttp.ts"
10
+ import { effectFn } from "./testing"
11
+ import * as TestHttpClient from "./testing/TestHttpClient.ts"
14
12
 
15
13
  const effect = effectFn(
16
14
  Layer.effect(
package/src/BundleHttp.ts CHANGED
@@ -13,7 +13,8 @@ import * as Stream from "effect/Stream"
13
13
  import * as NPath from "node:path"
14
14
  import * as NUrl from "node:url"
15
15
  import * as Bundle from "./Bundle.ts"
16
- import * as SseHttpResponse from "./SseHttpResponse.ts"
16
+ import * as SseHttpResponse from "./experimental/SseHttpResponse.ts"
17
+
17
18
 
18
19
  const DefaultBundleEndpoint = "/_bundle"
19
20
 
@@ -7,9 +7,8 @@
7
7
  import * as HttpRouter from "@effect/platform/HttpRouter"
8
8
  import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
9
9
  import * as t from "bun:test"
10
- import * as Effect from "effect/Effect"
11
- import * as TestHttpClient from "../src/TestHttpClient.ts"
12
- import { effectFn } from "../src/testing.ts"
10
+ import { effectFn } from "../src/testing"
11
+ import * as TestHttpClient from "./testing/TestHttpClient.ts"
13
12
 
14
13
  const effect = effectFn()
15
14
 
@@ -7,8 +7,8 @@ import * as Effect from "effect/Effect"
7
7
  import * as FileHttpRouter from "./FileHttpRouter.ts"
8
8
  import * as FileRouter from "./FileRouter.ts"
9
9
  import * as Route from "./Route.ts"
10
- import * as TestHttpClient from "./TestHttpClient.ts"
11
- import { effectFn } from "./testing.ts"
10
+ import { effectFn } from "./testing"
11
+ import * as TestHttpClient from "./testing/TestHttpClient.ts"
12
12
 
13
13
  class CustomError extends Data.TaggedError("CustomError") {}
14
14
 
@@ -12,7 +12,7 @@ import * as FileRouterCodegen from "./FileRouterCodegen.ts"
12
12
  import * as NodeFileSystem from "./NodeFileSystem.ts"
13
13
  import * as Route from "./Route.ts"
14
14
  import * as SchemaExtra from "./SchemaExtra.ts"
15
- import * as TestLogger from "./TestLogger.ts"
15
+ import * as TestLogger from "./testing/TestLogger.ts"
16
16
 
17
17
  function createTempDirWithFiles(
18
18
  files: Record<string, string>,
@@ -2,7 +2,7 @@ import * as t from "bun:test"
2
2
  import { MemoryFileSystem } from "effect-memfs"
3
3
  import * as Effect from "effect/Effect"
4
4
  import * as FileRouter from "./FileRouter.ts"
5
- import { effectFn } from "./testing.ts"
5
+ import { effectFn } from "./testing"
6
6
 
7
7
  const Files = {
8
8
  "/routes/about/layer.tsx": "",
@@ -7,7 +7,7 @@ import {
7
7
  } from "effect"
8
8
  import * as Cause from "effect/Cause"
9
9
  import * as HttpAppExtra from "./HttpAppExtra.ts"
10
- import { effectFn } from "./testing.ts"
10
+ import { effectFn } from "./testing"
11
11
 
12
12
  const mockRequest = HttpServerRequest.HttpServerRequest.of({
13
13
  url: "http://localhost:3000/test",
package/src/Start.ts CHANGED
@@ -3,48 +3,14 @@ import * as FileSystem from "@effect/platform/FileSystem"
3
3
  import * as HttpClient from "@effect/platform/HttpClient"
4
4
  import * as HttpRouter from "@effect/platform/HttpRouter"
5
5
  import * as HttpServer from "@effect/platform/HttpServer"
6
- import * as Config from "effect/Config"
7
6
  import * as Effect from "effect/Effect"
8
7
  import * as Function from "effect/Function"
9
8
  import * as Layer from "effect/Layer"
10
- import * as Option from "effect/Option"
11
- import * as BunBundle from "./bun/BunBundle.ts"
12
9
  import * as BunHttpServer from "./bun/BunHttpServer.ts"
13
- import * as BunRoute from "./bun/BunRoute.ts"
14
10
  import * as BunRuntime from "./bun/BunRuntime.ts"
15
- import * as Bundle from "./Bundle.ts"
16
- import * as BundleHttp from "./BundleHttp.ts"
17
- import * as HttpAppExtra from "./HttpAppExtra.ts"
18
11
  import * as NodeFileSystem from "./NodeFileSystem.ts"
19
- import * as Router from "./Router.ts"
20
12
  import * as StartApp from "./StartApp.ts"
21
13
 
22
- export function bundleClient(config: BunBundle.BuildOptions | string) {
23
- const clientLayer = Layer.effect(
24
- Bundle.ClientBundle,
25
- Function.pipe(
26
- BunBundle.buildClient(config),
27
- Bundle.handleBundleErrorSilently,
28
- ),
29
- )
30
- const assetsLayer = Layer.effectDiscard(Effect.gen(function*() {
31
- const router = yield* HttpRouter.Default
32
- const app = BundleHttp.toHttpApp(Bundle.ClientBundle)
33
-
34
- yield* router.mountApp(
35
- "/_bundle",
36
- // we need to use as any here because HttpRouter.Default
37
- // only accepts default services.
38
- app as any,
39
- )
40
- }))
41
-
42
- return Layer.mergeAll(
43
- clientLayer,
44
- assetsLayer,
45
- )
46
- }
47
-
48
14
  export function layer<
49
15
  Layers extends [
50
16
  Layer.Layer<never, any, any>,
package/src/StartApp.ts CHANGED
@@ -3,23 +3,22 @@ import * as Context from "effect/Context"
3
3
  import * as Effect from "effect/Effect"
4
4
  import * as Function from "effect/Function"
5
5
  import * as Layer from "effect/Layer"
6
+ import * as PubSub from "effect/PubSub"
6
7
  import * as Ref from "effect/Ref"
7
8
 
8
- type NewType = HttpApp.Default<never, never>
9
-
10
9
  type StartMiddleware = <E, R>(
11
10
  self: HttpApp.Default<E, R>,
12
- ) => NewType
11
+ ) => HttpApp.Default<never, never>
13
12
 
14
13
  export class StartApp extends Context.Tag("effect-start/StartApp")<
15
14
  StartApp,
16
15
  {
17
16
  readonly env: "development" | "production" | string
18
- readonly relativeUrlRoot?: string
19
17
  readonly addMiddleware: (
20
18
  middleware: StartMiddleware,
21
19
  ) => Effect.Effect<void>
22
20
  readonly middleware: Ref.Ref<StartMiddleware>
21
+ readonly events: PubSub.PubSub<any>
23
22
  }
24
23
  >() {
25
24
  }
@@ -27,17 +26,22 @@ export class StartApp extends Context.Tag("effect-start/StartApp")<
27
26
  export function layer(options?: {
28
27
  env?: string
29
28
  }) {
30
- return Layer.sync(StartApp, () => {
31
- const env = options?.env ?? process.env.NODE_ENV ?? "development"
32
- const middleware = Ref.unsafeMake(
33
- Function.identity as StartMiddleware,
34
- )
29
+ return Layer.effect(
30
+ StartApp,
31
+ Effect.gen(function*() {
32
+ const env = options?.env ?? process.env.NODE_ENV ?? "development"
33
+ const middleware = yield* Ref.make(
34
+ Function.identity as StartMiddleware,
35
+ )
36
+ const events = yield* PubSub.unbounded()
35
37
 
36
- return StartApp.of({
37
- env,
38
- middleware,
39
- addMiddleware: (f) =>
40
- Ref.update(middleware, (prev) => (app) => f(prev(app))),
41
- })
42
- })
38
+ return StartApp.of({
39
+ env,
40
+ middleware,
41
+ addMiddleware: (f) =>
42
+ Ref.update(middleware, (prev) => (app) => f(prev(app))),
43
+ events,
44
+ })
45
+ }),
46
+ )
43
47
  }
@@ -7,7 +7,7 @@ import * as NOS from "node:os"
7
7
  import * as NPath from "node:path"
8
8
  import * as Bundle from "../Bundle.ts"
9
9
  import * as BundleHttp from "../BundleHttp.ts"
10
- import * as TestHttpClient from "../TestHttpClient.ts"
10
+ import * as TestHttpClient from "../testing/TestHttpClient.ts"
11
11
  import * as BunBundle from "./BunBundle.ts"
12
12
 
13
13
  t.describe("BunBundle manifest structure", () => {
@@ -1,5 +1,5 @@
1
1
  import * as t from "bun:test"
2
- import { effectFn } from "../testing.ts"
2
+ import { effectFn } from "../testing"
3
3
  import * as BunBundle from "./BunBundle.ts"
4
4
  import * as BunImportTrackerPlugin from "./BunImportTrackerPlugin.ts"
5
5
 
@@ -35,7 +35,7 @@ t.it("imports", () =>
35
35
  },
36
36
  {
37
37
  kind: "import-statement",
38
- path: "src/testing.ts",
38
+ path: "src/testing",
39
39
  },
40
40
  {
41
41
  kind: "import-statement",
@@ -2,7 +2,7 @@ import * as t from "bun:test"
2
2
  import * as Effect from "effect/Effect"
3
3
  import * as Route from "../Route.ts"
4
4
  import * as Router from "../Router.ts"
5
- import * as TestHttpClient from "../TestHttpClient.ts"
5
+ import * as TestHttpClient from "../testing/TestHttpClient.ts"
6
6
  import * as BunHttpServer from "./BunHttpServer.ts"
7
7
  import * as BunRoute from "./BunRoute.ts"
8
8
 
@@ -4,7 +4,8 @@ import * as Effect from "effect/Effect"
4
4
  import * as Function from "effect/Function"
5
5
  import * as Schedule from "effect/Schedule"
6
6
  import * as Stream from "effect/Stream"
7
- import * as StreamExtra from "./StreamExtra.ts"
7
+ import * as StreamExtra from "../StreamExtra.ts"
8
+
8
9
 
9
10
  const DefaultHeartbeatInterval = Duration.seconds(5)
10
11
 
@@ -0,0 +1,2 @@
1
+ export * as EncryptedCookies from "./EncryptedCookies.ts"
2
+ export * as SseHttpResponse from "./SseHttpResponse.ts"
package/src/index.ts CHANGED
@@ -1,24 +1,6 @@
1
- export * as Bundle from "./Bundle.ts"
2
- export * as BundleHttp from "./BundleHttp.ts"
3
-
4
- export * as StartHttp from "./StartHttp.ts"
5
-
6
- export * as HttpAppExtra from "./HttpAppExtra.ts"
7
-
8
- export * as PublicDirectory from "./PublicDirectory.ts"
9
-
10
- export * as TestHttpClient from "./TestHttpClient.ts"
11
- export {
12
- effectFn,
13
- } from "./testing.ts"
1
+ export * as Start from "./Start.ts"
14
2
 
15
- export * as FileHttpRouter from "./FileHttpRouter.ts"
16
3
  export * as FileRouter from "./FileRouter.ts"
17
-
18
4
  export * as Route from "./Route.ts"
19
- export * as Router from "./Router.ts"
20
5
 
21
- export * as Start from "./Start.ts"
22
-
23
- export * as Hyper from "./Hyper.ts"
24
- export * as HyperHtml from "./HyperHtml.ts"
6
+ export * as Bundle from "./Bundle.ts"
@@ -0,0 +1 @@
1
+ export * as BasicAuthMiddleware from "./BasicAuthMiddleware.ts"
@@ -2,8 +2,8 @@ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
2
2
  import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
3
3
  import * as t from "bun:test"
4
4
  import * as Effect from "effect/Effect"
5
+ import { effectFn } from "./index.ts"
5
6
  import * as TestHttpClient from "./TestHttpClient.ts"
6
- import { effectFn } from "./testing.ts"
7
7
 
8
8
  const App = Effect.gen(function*() {
9
9
  const req = yield* HttpServerRequest.HttpServerRequest
@@ -10,7 +10,6 @@ import * as UrlParams from "@effect/platform/UrlParams"
10
10
  import * as Effect from "effect/Effect"
11
11
  import * as Either from "effect/Either"
12
12
  import * as Function from "effect/Function"
13
- import * as Scope from "effect/Scope"
14
13
  import * as Stream from "effect/Stream"
15
14
 
16
15
  const WebHeaders = globalThis.Headers
@@ -0,0 +1,3 @@
1
+ export * as TestHttpClient from "./TestHttpClient.ts"
2
+ export * as TestLogger from "./TestLogger.ts"
3
+ export * from "./utils.ts"
@@ -44,21 +44,22 @@ export const make = (opts?: {
44
44
  // (imported path) -> (importer paths)
45
45
  const importDescendants = new Map<string, Set<string>>()
46
46
 
47
- if (opts?.scanPath) {
48
- const candidates = await scanFiles(opts.scanPath)
47
+ const prepopulateCandidates = opts?.scanPath
48
+ ? async () => {
49
+ const candidates = await scanFiles(opts.scanPath!)
49
50
 
50
- candidates.forEach(candidate => scannedCandidates.add(candidate))
51
- }
51
+ scannedCandidates.clear()
52
52
 
53
- /**
54
- * Track import relationships when dynamically scanning
55
- * from tailwind entrypoints.
56
- *
57
- * As of Bun 1.3 this pathway break for Bun Full-Stack server.
58
- * Better to pass scanPath explicitly.
59
- * @see https://github.com/oven-sh/bun/issues/20877
60
- */
61
- if (!opts?.scanPath) {
53
+ candidates.forEach(candidate => scannedCandidates.add(candidate))
54
+ }
55
+ : null
56
+
57
+ // Track import relationships when dynamically scanning
58
+ // from tailwind entrypoints.
59
+ // As of Bun 1.3 this pathway break for Bun Full-Stack server.
60
+ // Better to pass scanPath explicitly.
61
+ // @see https://github.com/oven-sh/bun/issues/20877
62
+ if (!prepopulateCandidates) {
62
63
  builder.onResolve({
63
64
  filter: /.*/,
64
65
  }, (args) => {
@@ -130,14 +131,15 @@ export const make = (opts?: {
130
131
  onDependency: (path) => {},
131
132
  })
132
133
 
134
+ await prepopulateCandidates?.()
135
+
133
136
  // wait for other files to be loaded so we can collect class name candidates
134
137
  await args.defer()
135
138
 
136
- const candidates = new Set<string>()
139
+ const candidates = new Set<string>(scannedCandidates)
137
140
 
138
- scannedCandidates.forEach(candidate => candidates.add(candidate))
139
-
140
- {
141
+ // when we scan a path, we don't need to track candidate tree
142
+ if (!prepopulateCandidates) {
141
143
  const pendingModules = [
142
144
  // get class name candidates from all modules that import this one
143
145
  ...(importAncestors.get(args.path) ?? []),
@@ -295,6 +297,10 @@ async function scanFiles(dir: string): Promise<Set<string>> {
295
297
  absolute: true,
296
298
  })
297
299
  ) {
300
+ if (filePath.includes("/node_modules/")) {
301
+ continue
302
+ }
303
+
298
304
  const contents = await Bun.file(filePath).text()
299
305
  const classNames = extractClassNames(contents)
300
306
 
@@ -1,14 +0,0 @@
1
- import * as t from "bun:test"
2
- import * as JsModule from "./JsModule.ts"
3
-
4
- t.describe(`${JsModule.importSource.name}`, () => {
5
- t.it("imports a string", async () => {
6
- const mod = await JsModule.importSource<any>(`
7
- export const b = "B"
8
- `)
9
-
10
- t
11
- .expect(mod.b)
12
- .toBe("B")
13
- })
14
- })
package/src/JsModule.ts DELETED
@@ -1,116 +0,0 @@
1
- import * as Array from "effect/Array"
2
- import * as Function from "effect/Function"
3
- import * as Iterable from "effect/Iterable"
4
- import * as Order from "effect/Order"
5
- import * as Record from "effect/Record"
6
- import * as NFS from "node:fs"
7
- import * as NFSP from "node:fs/promises"
8
- import * as NPath from "node:path"
9
- import * as process from "node:process"
10
-
11
- /**
12
- * Imports a blob as a module.
13
- * Useful for loading code from build artifacts.
14
- *
15
- * Temporary files are wrriten to closest node_modules/ for node resolver
16
- * to pick up dependencies correctly and to avoid arbitrary file watchers
17
- * from detecting them.
18
- */
19
- export async function importBlob<M = unknown>(
20
- blob: Blob,
21
- entrypoint = "index.js",
22
- ): Promise<M> {
23
- return await importBundle({
24
- [entrypoint]: blob,
25
- }, entrypoint)
26
- }
27
-
28
- /**
29
- * Imports an entrypoint from multiple blobs.
30
- * Useful for loading code from build artifacts.
31
- *
32
- * Temporary files are wrriten to closest node_modules/ for node resolver
33
- * to pick up dependencies correctly and to avoid arbitrary file watchers
34
- * from detecting them.
35
- *
36
- * WARNING: dynamic imports that happened after this function will fail
37
- */
38
- export async function importBundle<M = unknown>(
39
- blobs: {
40
- [path: string]: Blob
41
- },
42
- entrypoint: string,
43
- basePath = findNodeModules() + "/.tmp",
44
- ): Promise<M> {
45
- const sortedBlobs = Function.pipe(
46
- blobs,
47
- Record.toEntries,
48
- Array.sortWith(v => v[0], Order.string),
49
- Array.map(v => v[1]),
50
- )
51
- const bundleBlob = new Blob(sortedBlobs)
52
- const hashPrefix = await hashBuffer(await bundleBlob.arrayBuffer())
53
- .then(v => v.slice(0, 8))
54
- const dir = `${basePath}/effect-start-${hashPrefix}`
55
-
56
- await NFSP.mkdir(dir, { recursive: true })
57
-
58
- await Promise.all(Function.pipe(
59
- blobs,
60
- Record.toEntries,
61
- Array.map(([path, blob]) => {
62
- const fullPath = `${dir}/${path}`
63
-
64
- return blob
65
- .arrayBuffer()
66
- .then(v => NFSP.writeFile(fullPath, Buffer.from(v)))
67
- }),
68
- ))
69
-
70
- const bundleModule = await import(`${dir}/${entrypoint}`)
71
-
72
- await NFSP
73
- .rmdir(dir, { recursive: true })
74
- // if called concurrently, file sometimes may be deleted
75
- // safe ignore when this happens
76
- .catch(() => {})
77
-
78
- return bundleModule
79
- }
80
-
81
- export function findNodeModules(startDir = process.cwd()) {
82
- let currentDir = NPath.resolve(startDir)
83
-
84
- while (currentDir !== NPath.parse(currentDir).root) {
85
- const nodeModulesPath = NPath.join(currentDir, "node_modules")
86
- if (
87
- NFS.statSync(nodeModulesPath).isDirectory()
88
- ) {
89
- return nodeModulesPath
90
- }
91
-
92
- currentDir = NPath.dirname(currentDir)
93
- }
94
-
95
- return null
96
- }
97
-
98
- /**
99
- * Loads a JS module from a string using data: import
100
- */
101
- export async function importSource<M = unknown>(
102
- code: string,
103
- ): Promise<M> {
104
- const dataUrl = `data:text/javascript,${encodeURIComponent(code)}`
105
- return await import(dataUrl)
106
- }
107
-
108
- async function hashBuffer(buffer: BufferSource) {
109
- const hashBuffer = await crypto.subtle.digest("SHA-256", buffer)
110
-
111
- return Function.pipe(
112
- new Uint8Array(hashBuffer),
113
- Iterable.map(b => b.toString(16).padStart(2, "0")),
114
- Iterable.reduce("", (a, b) => a + b),
115
- )
116
- }
@@ -1,280 +0,0 @@
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
- })
@@ -1,108 +0,0 @@
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
- }
package/src/StartHttp.ts DELETED
@@ -1,42 +0,0 @@
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"
5
- import * as Effect from "effect/Effect"
6
- import {
7
- Bundle,
8
- BundleHttp,
9
- } from "."
10
-
11
- type SsrRenderer = (req: Request) => PromiseLike<Response>
12
-
13
- /**
14
- * Attempts to render SSR page. If the renderer returns 404,
15
- * we fall back to app.
16
- */
17
- export function ssr(renderer: SsrRenderer) {
18
- return Effect.gen(function*() {
19
- const request = yield* HttpServerRequest.HttpServerRequest
20
- const webRequest = request.source as Request
21
- const ssrRes = yield* Effect.tryPromise(() => renderer(webRequest))
22
-
23
- return HttpServerResponse.raw(ssrRes.body, {
24
- status: ssrRes.status,
25
- headers: ssrRes.headers,
26
- })
27
- })
28
- }
29
-
30
- export function withBundleAssets(opts?: {
31
- path?: string
32
- }) {
33
- return HttpMiddleware.make(app =>
34
- Effect.gen(function*() {
35
- const request = yield* HttpServerRequest.HttpServerRequest
36
- const bundleResponse = yield* BundleHttp.httpApp()
37
-
38
- // Fallback to original app
39
- return yield* app
40
- })
41
- )
42
- }
File without changes
File without changes