effect-start 0.11.1 → 0.13.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/README.md +38 -72
- package/package.json +3 -3
- package/src/Commander.test.ts +3 -4
- package/src/FileHttpRouter.test.ts +6 -43
- package/src/FileHttpRouter.ts +0 -15
- package/src/FileRouter.ts +79 -37
- package/src/FileRouterCodegen.test.ts +449 -265
- package/src/FileRouterCodegen.ts +67 -22
- package/src/NodeUtils.ts +23 -0
- package/src/Route.ts +1 -2
- package/src/Router.test.ts +11 -52
- package/src/Router.ts +55 -137
- package/src/SchemaExtra.ts +102 -0
- package/src/Start.ts +2 -2
- package/src/TestLogger.test.ts +38 -0
- package/src/TestLogger.ts +56 -0
- package/src/bun/BunHttpServer.ts +21 -14
- package/src/bun/BunRoute.test.ts +17 -51
- package/src/bun/BunRoute.ts +7 -61
- package/src/bun/BunRoute_bundles.test.ts +6 -5
- package/src/bun/BunTailwindPlugin.ts +93 -94
- package/src/x/tailwind/plugin.ts +17 -0
package/README.md
CHANGED
|
@@ -14,96 +14,62 @@ by checking out `examples/` directory.
|
|
|
14
14
|
It exports a layer that applies configuration and changes the behavior of the server:
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"src/index.html",
|
|
27
|
-
],
|
|
28
|
-
plugins: [
|
|
29
|
-
// enable TailwindCSS for client bundle
|
|
30
|
-
BunTailwindPlugin.make(),
|
|
31
|
-
],
|
|
17
|
+
import {
|
|
18
|
+
FileRouter,
|
|
19
|
+
Start,
|
|
20
|
+
} from "effect-start"
|
|
21
|
+
|
|
22
|
+
export default Start.layer(
|
|
23
|
+
FileRouter.layer({
|
|
24
|
+
load: () => import("./routes/manifest.ts"),
|
|
25
|
+
path: import.meta.resolve("./routes/manifest.ts"),
|
|
32
26
|
}),
|
|
33
27
|
)
|
|
28
|
+
|
|
29
|
+
if (import.meta.main) {
|
|
30
|
+
Start.serve(() => import("./server.ts"))
|
|
31
|
+
}
|
|
34
32
|
```
|
|
35
33
|
|
|
36
34
|
### File-based Routing
|
|
37
35
|
|
|
38
|
-
Effect Start provides automatic file-based routing with support for frontend pages, backend endpoints, and stackable
|
|
36
|
+
Effect Start provides automatic file-based routing with support for frontend pages, backend endpoints, and stackable middlewares called Route Layers.
|
|
39
37
|
|
|
40
38
|
```
|
|
41
|
-
src/routes
|
|
42
|
-
|
|
43
|
-
├──
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
│
|
|
47
|
-
|
|
48
|
-
│
|
|
49
|
-
│
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
import { FileRouter } from "effect-start"
|
|
55
|
-
|
|
56
|
-
// Generate route manifest and watch for changes
|
|
57
|
-
const routerLayer = FileRouter.layer(import.meta.resolve("routes"))
|
|
39
|
+
$ tree src/routes
|
|
40
|
+
src/routes
|
|
41
|
+
├── [[...frontend]]
|
|
42
|
+
│ └── route.ts
|
|
43
|
+
├── admin
|
|
44
|
+
│ ├── data.json
|
|
45
|
+
│ │ └── route.tsx
|
|
46
|
+
│ ├── layer.tsx
|
|
47
|
+
│ └── route.tsx
|
|
48
|
+
├── layer.tsx
|
|
49
|
+
├── manifest.ts
|
|
50
|
+
└── route.tsx
|
|
58
51
|
```
|
|
59
52
|
|
|
60
|
-
**Note:** Ensure `FileRouter.layer` is provided after any bundle layer to guarantee the manifest file is properly generated before bundling.
|
|
61
|
-
|
|
62
53
|
### Tailwind CSS Support
|
|
63
54
|
|
|
64
|
-
Effect Start
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
import { Start } from "effect-start"
|
|
68
|
-
import { BunTailwindPlugin } from "effect-start/bun"
|
|
55
|
+
Effect Start comes with native Tailwind support that is lightweight and
|
|
56
|
+
works with minimal setup.
|
|
69
57
|
|
|
70
|
-
|
|
71
|
-
entrypoints: [
|
|
72
|
-
"./src/index.html",
|
|
73
|
-
],
|
|
74
|
-
plugins: [
|
|
75
|
-
BunTailwindPlugin.make(),
|
|
76
|
-
],
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
export default Start.make(
|
|
80
|
-
ClientBundle,
|
|
81
|
-
)
|
|
58
|
+
First, install Tailwind package:
|
|
82
59
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
60
|
+
```sh
|
|
61
|
+
bun add tailwindcss
|
|
86
62
|
```
|
|
87
63
|
|
|
88
|
-
Then
|
|
64
|
+
Then, register a plugin in `bunfig.toml`:
|
|
89
65
|
|
|
90
|
-
```
|
|
91
|
-
|
|
66
|
+
```toml
|
|
67
|
+
[serve.static]
|
|
68
|
+
plugins = ["effect-start/x/tailwind/plugin"]
|
|
92
69
|
```
|
|
93
70
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
Tunnel your local server to the Internet with `cloudflared`
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
import { Start } from "effect-start"
|
|
100
|
-
import { CloudflareTunnel } from "effect-start/x/cloudflare"
|
|
71
|
+
Finally, include it in your `src/app.css`:
|
|
101
72
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
if (import.meta.main) {
|
|
107
|
-
Start.serve(() => import("./server"))
|
|
108
|
-
}
|
|
73
|
+
```html
|
|
74
|
+
@import "tailwindcss";
|
|
109
75
|
```
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts",
|
|
8
|
-
"./
|
|
8
|
+
"./FileRouter": "./src/FileRouter.ts",
|
|
9
9
|
"./Route": "./src/Route.ts",
|
|
10
10
|
"./EncryptedCookies": "./src/EncryptedCookies.ts",
|
|
11
11
|
"./bun": "./src/bun/index.ts",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"./jsx-dev-runtime": "./src/jsx-runtime.ts",
|
|
17
17
|
"./hyper": "./src/hyper/index.ts",
|
|
18
18
|
"./x/*": "./src/x/*/index.ts",
|
|
19
|
+
"./x/tailwind/plugin": "./src/x/tailwind/plugin.ts",
|
|
19
20
|
"./middlewares/BasicAuthMiddleware": "./src/middlewares/BasicAuthMiddleware.ts",
|
|
20
21
|
"./assets.d.ts": "./src/assets.d.ts"
|
|
21
22
|
},
|
|
@@ -47,7 +48,6 @@
|
|
|
47
48
|
"@types/react-dom": "^19.2.3",
|
|
48
49
|
"dprint-cli": "^0.4.1",
|
|
49
50
|
"dprint-markup": "nounder/dprint-markup",
|
|
50
|
-
"effect": "^3.19.10",
|
|
51
51
|
"effect-memfs": "^0.8.0",
|
|
52
52
|
"ts-namespace-import": "nounder/ts-namespace-import#140c405"
|
|
53
53
|
},
|
package/src/Commander.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as t from "bun:test"
|
|
2
2
|
import * as Effect from "effect/Effect"
|
|
3
3
|
import * as Schema from "effect/Schema"
|
|
4
|
+
import * as assert from "node:assert"
|
|
4
5
|
import * as Commander from "./Commander.ts"
|
|
5
6
|
|
|
6
7
|
t.describe(`${Commander.make.name}`, () => {
|
|
@@ -1216,10 +1217,8 @@ t.describe("error handling", () => {
|
|
|
1216
1217
|
Effect.either(Commander.parse(cmd, ["--port", "invalid"])),
|
|
1217
1218
|
)
|
|
1218
1219
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
t.expect(result.left.message).toContain("Invalid value")
|
|
1222
|
-
}
|
|
1220
|
+
assert.strictEqual(result._tag, "Left")
|
|
1221
|
+
t.expect(result.left.message).toContain("Invalid value")
|
|
1223
1222
|
})
|
|
1224
1223
|
|
|
1225
1224
|
t.it("should fail on invalid choice", async () => {
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import * as Error from "@effect/platform/Error"
|
|
2
2
|
import * as FileSystem from "@effect/platform/FileSystem"
|
|
3
|
-
import * as HttpApp from "@effect/platform/HttpApp"
|
|
4
3
|
import * as HttpRouter from "@effect/platform/HttpRouter"
|
|
5
|
-
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
6
4
|
import * as t from "bun:test"
|
|
7
5
|
import * as Data from "effect/Data"
|
|
8
6
|
import * as Effect from "effect/Effect"
|
|
9
7
|
import * as FileHttpRouter from "./FileHttpRouter.ts"
|
|
8
|
+
import * as FileRouter from "./FileRouter.ts"
|
|
10
9
|
import * as Route from "./Route.ts"
|
|
11
|
-
import * as Router from "./Router.ts"
|
|
12
10
|
import * as TestHttpClient from "./TestHttpClient.ts"
|
|
13
11
|
import { effectFn } from "./testing.ts"
|
|
14
12
|
|
|
@@ -31,13 +29,7 @@ const SampleRoutes = [
|
|
|
31
29
|
},
|
|
32
30
|
] as const
|
|
33
31
|
|
|
34
|
-
const
|
|
35
|
-
routes: SampleRoutes,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const routerLayer = Router.layerPromise(async () => SampleRouteManifest)
|
|
39
|
-
|
|
40
|
-
const effect = effectFn(routerLayer)
|
|
32
|
+
const effect = effectFn()
|
|
41
33
|
|
|
42
34
|
t.it("HttpRouter Requirement and Error types infers", () =>
|
|
43
35
|
effect(function*() {
|
|
@@ -53,7 +45,7 @@ t.it("HttpRouter Requirement and Error types infers", () =>
|
|
|
53
45
|
|
|
54
46
|
t.it("HTTP methods", () =>
|
|
55
47
|
effect(function*() {
|
|
56
|
-
const allMethodsRoute:
|
|
48
|
+
const allMethodsRoute: FileRouter.LazyRoute = {
|
|
57
49
|
path: "/",
|
|
58
50
|
load: async () => ({
|
|
59
51
|
default: Route
|
|
@@ -111,40 +103,11 @@ t.it("router handles requests correctly", () =>
|
|
|
111
103
|
.toBe("User created")
|
|
112
104
|
}))
|
|
113
105
|
|
|
114
|
-
t.it("middleware falls back to original app on 404", () =>
|
|
115
|
-
effect(function*() {
|
|
116
|
-
const middleware = FileHttpRouter.middleware()
|
|
117
|
-
const fallbackApp = Effect.succeed(HttpServerResponse.text("fallback"))
|
|
118
|
-
const middlewareApp = middleware(fallbackApp)
|
|
119
|
-
|
|
120
|
-
const client = TestHttpClient.make(middlewareApp)
|
|
121
|
-
|
|
122
|
-
const existingRouteResponse = yield* client.get("/users")
|
|
123
|
-
|
|
124
|
-
t
|
|
125
|
-
.expect(existingRouteResponse.status)
|
|
126
|
-
.toBe(200)
|
|
127
|
-
|
|
128
|
-
t
|
|
129
|
-
.expect(yield* existingRouteResponse.text)
|
|
130
|
-
.toBe("Users list")
|
|
131
|
-
|
|
132
|
-
const notFoundResponse = yield* client.get("/nonexistent")
|
|
133
|
-
|
|
134
|
-
t
|
|
135
|
-
.expect(notFoundResponse.status)
|
|
136
|
-
.toBe(200)
|
|
137
|
-
|
|
138
|
-
t
|
|
139
|
-
.expect(yield* notFoundResponse.text)
|
|
140
|
-
.toBe("fallback")
|
|
141
|
-
}))
|
|
142
|
-
|
|
143
106
|
t.it(
|
|
144
107
|
"handles routes with special characters (tilde and hyphen)",
|
|
145
108
|
() =>
|
|
146
109
|
effect(function*() {
|
|
147
|
-
const specialCharRoutes:
|
|
110
|
+
const specialCharRoutes: FileRouter.LazyRoute[] = [
|
|
148
111
|
{
|
|
149
112
|
path: "/api-v1",
|
|
150
113
|
load: async () => ({
|
|
@@ -204,7 +167,7 @@ t.it(
|
|
|
204
167
|
"layer routes can wrap inner routes using next()",
|
|
205
168
|
() =>
|
|
206
169
|
effect(function*() {
|
|
207
|
-
const routeWithLayer:
|
|
170
|
+
const routeWithLayer: FileRouter.LazyRoute = {
|
|
208
171
|
path: "/page",
|
|
209
172
|
load: async () => ({
|
|
210
173
|
default: Route.html(Effect.succeed("<h1>Page Content</h1>")),
|
|
@@ -236,7 +199,7 @@ t.it(
|
|
|
236
199
|
|
|
237
200
|
t.it("nested layers compose correctly with next()", () =>
|
|
238
201
|
effect(function*() {
|
|
239
|
-
const routeWithNestedLayers:
|
|
202
|
+
const routeWithNestedLayers: FileRouter.LazyRoute = {
|
|
240
203
|
path: "/nested",
|
|
241
204
|
load: async () => ({
|
|
242
205
|
default: Route.html(Effect.succeed("content")),
|
package/src/FileHttpRouter.ts
CHANGED
|
@@ -192,18 +192,3 @@ export function make<
|
|
|
192
192
|
return router as HttpRouterFromServerRoutes<Routes>
|
|
193
193
|
})
|
|
194
194
|
}
|
|
195
|
-
|
|
196
|
-
export function middleware() {
|
|
197
|
-
return HttpMiddleware.make((app) =>
|
|
198
|
-
Effect.gen(function*() {
|
|
199
|
-
const routerContext = yield* Router.Router
|
|
200
|
-
const router = yield* make(
|
|
201
|
-
routerContext.routes as ReadonlyArray<Router.LazyRoute>,
|
|
202
|
-
)
|
|
203
|
-
const res = yield* router.pipe(
|
|
204
|
-
Effect.catchTag("RouteNotFound", () => app),
|
|
205
|
-
)
|
|
206
|
-
return res
|
|
207
|
-
})
|
|
208
|
-
)
|
|
209
|
-
}
|
package/src/FileRouter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PlatformError } from "@effect/platform/Error"
|
|
2
2
|
import * as FileSystem from "@effect/platform/FileSystem"
|
|
3
3
|
import * as Array from "effect/Array"
|
|
4
|
+
import * as Context from "effect/Context"
|
|
4
5
|
import * as Effect from "effect/Effect"
|
|
5
6
|
import * as Function from "effect/Function"
|
|
6
7
|
import * as Layer from "effect/Layer"
|
|
@@ -11,17 +12,33 @@ import * as NUrl from "node:url"
|
|
|
11
12
|
import * as FileRouterCodegen from "./FileRouterCodegen.ts"
|
|
12
13
|
import * as FileRouterPattern from "./FileRouterPattern.ts"
|
|
13
14
|
import * as FileSystemExtra from "./FileSystemExtra.ts"
|
|
15
|
+
import * as Route from "./Route.ts"
|
|
14
16
|
import * as Router from "./Router.ts"
|
|
15
17
|
|
|
18
|
+
export type RouteModule = {
|
|
19
|
+
default: Route.RouteSet.Default
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type LazyRoute = {
|
|
23
|
+
path: `/${string}`
|
|
24
|
+
load: () => Promise<RouteModule>
|
|
25
|
+
layers?: ReadonlyArray<() => Promise<unknown>>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Manifest = {
|
|
29
|
+
routes: readonly LazyRoute[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class FileRouter extends Context.Tag("effect-start/FileRouter")<
|
|
33
|
+
FileRouter,
|
|
34
|
+
Manifest
|
|
35
|
+
>() {}
|
|
36
|
+
|
|
16
37
|
export type GroupSegment<Name extends string = string> =
|
|
17
38
|
FileRouterPattern.GroupSegment<Name>
|
|
18
39
|
|
|
19
40
|
export type Segment = FileRouterPattern.Segment
|
|
20
41
|
|
|
21
|
-
export type RouteManifest = {
|
|
22
|
-
routes: readonly Router.LazyRoute[]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
42
|
export type RouteHandle = {
|
|
26
43
|
handle: "route" | "layer"
|
|
27
44
|
// eg. `about/route.tsx`, `users/[userId]/route.tsx`, `(admin)/users/route.tsx`
|
|
@@ -119,8 +136,8 @@ export function parseRoute(
|
|
|
119
136
|
/**
|
|
120
137
|
* Generates a manifest file that references all routes.
|
|
121
138
|
*/
|
|
122
|
-
export function
|
|
123
|
-
load: () => Promise<
|
|
139
|
+
export function layer(options: {
|
|
140
|
+
load: () => Promise<Manifest>
|
|
124
141
|
path: string
|
|
125
142
|
}) {
|
|
126
143
|
let manifestPath = options.path
|
|
@@ -135,44 +152,69 @@ export function layerManifest(options: {
|
|
|
135
152
|
const manifestFilename = NPath.basename(manifestPath)
|
|
136
153
|
const resolvedManifestPath = NPath.resolve(routesPath, manifestFilename)
|
|
137
154
|
|
|
138
|
-
return Layer.scopedDiscard(
|
|
139
|
-
Effect.gen(function*() {
|
|
140
|
-
yield* FileRouterCodegen.update(routesPath, manifestFilename)
|
|
141
|
-
|
|
142
|
-
const stream = Function.pipe(
|
|
143
|
-
FileSystemExtra.watchSource({
|
|
144
|
-
path: routesPath,
|
|
145
|
-
filter: (e) => !e.path.includes("node_modules"),
|
|
146
|
-
}),
|
|
147
|
-
Stream.onError((e) => Effect.logError(e)),
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
yield* Function.pipe(
|
|
151
|
-
stream,
|
|
152
|
-
// filter out edits to gen file
|
|
153
|
-
Stream.filter(e => e.path !== resolvedManifestPath),
|
|
154
|
-
Stream.runForEach(() =>
|
|
155
|
-
FileRouterCodegen.update(routesPath, manifestFilename)
|
|
156
|
-
),
|
|
157
|
-
Effect.fork,
|
|
158
|
-
)
|
|
159
|
-
}),
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function layer(options: {
|
|
164
|
-
load: () => Promise<Router.RouterManifest>
|
|
165
|
-
path: string
|
|
166
|
-
}) {
|
|
167
155
|
return Layer.provide(
|
|
168
156
|
Layer.effect(
|
|
169
|
-
|
|
157
|
+
FileRouter,
|
|
170
158
|
Effect.promise(() => options.load()),
|
|
171
159
|
),
|
|
172
|
-
|
|
160
|
+
Layer.scopedDiscard(
|
|
161
|
+
Effect.gen(function*() {
|
|
162
|
+
yield* FileRouterCodegen.update(routesPath, manifestFilename)
|
|
163
|
+
|
|
164
|
+
const stream = Function.pipe(
|
|
165
|
+
FileSystemExtra.watchSource({
|
|
166
|
+
path: routesPath,
|
|
167
|
+
filter: (e) => !e.path.includes("node_modules"),
|
|
168
|
+
}),
|
|
169
|
+
Stream.onError((e) => Effect.logError(e)),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
yield* Function.pipe(
|
|
173
|
+
stream,
|
|
174
|
+
// filter out edits to gen file
|
|
175
|
+
Stream.filter(e => e.path !== resolvedManifestPath),
|
|
176
|
+
Stream.runForEach(() =>
|
|
177
|
+
FileRouterCodegen.update(routesPath, manifestFilename)
|
|
178
|
+
),
|
|
179
|
+
Effect.fork,
|
|
180
|
+
)
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
173
183
|
)
|
|
174
184
|
}
|
|
175
185
|
|
|
186
|
+
export function fromManifest(
|
|
187
|
+
manifest: Manifest,
|
|
188
|
+
): Effect.Effect<Router.Router.Any> {
|
|
189
|
+
return Effect.gen(function*() {
|
|
190
|
+
const loadedEntries = yield* Effect.forEach(
|
|
191
|
+
manifest.routes,
|
|
192
|
+
(lazyRoute) =>
|
|
193
|
+
Effect.gen(function*() {
|
|
194
|
+
const routeModule = yield* Effect.promise(() => lazyRoute.load())
|
|
195
|
+
const layerModules = lazyRoute.layers
|
|
196
|
+
? yield* Effect.forEach(
|
|
197
|
+
lazyRoute.layers,
|
|
198
|
+
(loadLayer) => Effect.promise(() => loadLayer()),
|
|
199
|
+
)
|
|
200
|
+
: []
|
|
201
|
+
|
|
202
|
+
const layers = layerModules
|
|
203
|
+
.map((m: any) => m.default)
|
|
204
|
+
.filter(Route.isRouteLayer)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
path: lazyRoute.path,
|
|
208
|
+
route: routeModule.default,
|
|
209
|
+
layers,
|
|
210
|
+
}
|
|
211
|
+
}),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return Router.make(loadedEntries, [])
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
176
218
|
export function walkRoutesDirectory(
|
|
177
219
|
dir: string,
|
|
178
220
|
): Effect.Effect<
|