effect-start 0.15.0 → 0.17.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/package.json +2 -1
- package/src/ContentNegotiation.test.ts +103 -0
- package/src/ContentNegotiation.ts +10 -3
- package/src/Development.test.ts +119 -0
- package/src/Development.ts +137 -0
- package/src/Entity.test.ts +592 -0
- package/src/Entity.ts +359 -0
- package/src/FileRouter.ts +2 -2
- package/src/Http.test.ts +315 -20
- package/src/Http.ts +153 -11
- package/src/PathPattern.ts +3 -1
- package/src/Route.ts +26 -10
- package/src/RouteBody.test.ts +98 -66
- package/src/RouteBody.ts +125 -35
- package/src/RouteHook.ts +15 -14
- package/src/RouteHttp.test.ts +2549 -83
- package/src/RouteHttp.ts +337 -113
- package/src/RouteHttpTracer.ts +92 -0
- package/src/RouteMount.test.ts +23 -10
- package/src/RouteMount.ts +161 -4
- package/src/RouteSchema.test.ts +346 -0
- package/src/RouteSchema.ts +386 -7
- package/src/RouteSse.test.ts +249 -0
- package/src/RouteSse.ts +195 -0
- package/src/RouteTree.test.ts +233 -85
- package/src/RouteTree.ts +98 -44
- package/src/StreamExtra.ts +21 -1
- package/src/Values.test.ts +263 -0
- package/src/Values.ts +68 -6
- package/src/bun/BunBundle.ts +0 -73
- package/src/bun/BunHttpServer.ts +23 -7
- package/src/bun/BunRoute.test.ts +162 -0
- package/src/bun/BunRoute.ts +144 -105
- package/src/hyper/HyperHtml.test.ts +119 -0
- package/src/hyper/HyperHtml.ts +10 -2
- package/src/hyper/HyperNode.ts +2 -0
- package/src/hyper/HyperRoute.test.tsx +197 -0
- package/src/hyper/HyperRoute.ts +61 -0
- package/src/hyper/index.ts +4 -0
- package/src/hyper/jsx.d.ts +15 -0
- package/src/index.ts +2 -0
- package/src/node/FileSystem.ts +8 -0
- package/src/testing/TestLogger.test.ts +0 -3
- package/src/testing/TestLogger.ts +15 -9
- package/src/FileSystemExtra.test.ts +0 -242
- package/src/FileSystemExtra.ts +0 -66
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"exports": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"./middlewares": "./src/middlewares/index.ts",
|
|
15
15
|
"./experimental": "./src/experimental/index.ts",
|
|
16
16
|
"./client/assets.d.ts": "./src/assets.d.ts",
|
|
17
|
+
"./hyper": "./src/hyper/index.ts",
|
|
17
18
|
"./jsx-runtime": "./src/hyper/jsx-runtime.ts",
|
|
18
19
|
"./jsx-dev-runtime": "./src/hyper/jsx-runtime.ts",
|
|
19
20
|
"./package.json": "./package.json"
|
|
@@ -148,6 +148,109 @@ test.describe("ContentNegotiation.media", () => {
|
|
|
148
148
|
.expect(result)
|
|
149
149
|
.toEqual(["text/html", "application/json"])
|
|
150
150
|
})
|
|
151
|
+
|
|
152
|
+
test.describe("wildcard in available types", () => {
|
|
153
|
+
test.it("text/* matches text/event-stream", () => {
|
|
154
|
+
const result = ContentNegotiation.media(
|
|
155
|
+
"text/event-stream",
|
|
156
|
+
["text/*", "application/json"],
|
|
157
|
+
)
|
|
158
|
+
test
|
|
159
|
+
.expect(result)
|
|
160
|
+
.toEqual(["text/*"])
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test.it("text/* matches text/markdown", () => {
|
|
164
|
+
const result = ContentNegotiation.media(
|
|
165
|
+
"text/markdown",
|
|
166
|
+
["text/*"],
|
|
167
|
+
)
|
|
168
|
+
test
|
|
169
|
+
.expect(result)
|
|
170
|
+
.toEqual(["text/*"])
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test.it("text/* matches text/plain", () => {
|
|
174
|
+
const result = ContentNegotiation.media(
|
|
175
|
+
"text/plain",
|
|
176
|
+
["text/*"],
|
|
177
|
+
)
|
|
178
|
+
test
|
|
179
|
+
.expect(result)
|
|
180
|
+
.toEqual(["text/*"])
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test.it("text/* does not match application/json", () => {
|
|
184
|
+
const result = ContentNegotiation.media(
|
|
185
|
+
"application/json",
|
|
186
|
+
["text/*"],
|
|
187
|
+
)
|
|
188
|
+
test
|
|
189
|
+
.expect(result)
|
|
190
|
+
.toEqual([])
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test.it("prefers exact match over wildcard available type", () => {
|
|
194
|
+
const result = ContentNegotiation.media(
|
|
195
|
+
"text/html",
|
|
196
|
+
["text/*", "text/html"],
|
|
197
|
+
)
|
|
198
|
+
test
|
|
199
|
+
.expect(result)
|
|
200
|
+
.toEqual(["text/html", "text/*"])
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test.it("text/* matches multiple text types in Accept", () => {
|
|
204
|
+
const result = ContentNegotiation.media(
|
|
205
|
+
"text/html, text/plain",
|
|
206
|
+
["text/*"],
|
|
207
|
+
)
|
|
208
|
+
test
|
|
209
|
+
.expect(result)
|
|
210
|
+
.toEqual(["text/*"])
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test.it("application/* matches application/xml", () => {
|
|
214
|
+
const result = ContentNegotiation.media(
|
|
215
|
+
"application/xml",
|
|
216
|
+
["application/*", "text/html"],
|
|
217
|
+
)
|
|
218
|
+
test
|
|
219
|
+
.expect(result)
|
|
220
|
+
.toEqual(["application/*"])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test.it("*/* in available matches any type", () => {
|
|
224
|
+
const result = ContentNegotiation.media(
|
|
225
|
+
"image/png",
|
|
226
|
+
["*/*"],
|
|
227
|
+
)
|
|
228
|
+
test
|
|
229
|
+
.expect(result)
|
|
230
|
+
.toEqual(["*/*"])
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test.it("combines client and server wildcards", () => {
|
|
234
|
+
// Client wants text/*, server offers text/*
|
|
235
|
+
const result = ContentNegotiation.media(
|
|
236
|
+
"text/*",
|
|
237
|
+
["text/*"],
|
|
238
|
+
)
|
|
239
|
+
test
|
|
240
|
+
.expect(result)
|
|
241
|
+
.toEqual(["text/*"])
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test.it("quality values still apply with wildcard available", () => {
|
|
245
|
+
const result = ContentNegotiation.media(
|
|
246
|
+
"text/html;q=0.5, text/event-stream;q=0.9",
|
|
247
|
+
["text/*"],
|
|
248
|
+
)
|
|
249
|
+
test
|
|
250
|
+
.expect(result)
|
|
251
|
+
.toEqual(["text/*"])
|
|
252
|
+
})
|
|
253
|
+
})
|
|
151
254
|
})
|
|
152
255
|
|
|
153
256
|
test.describe("ContentNegotiation.language", () => {
|
|
@@ -113,17 +113,24 @@ function specifyMediaType(
|
|
|
113
113
|
let s = 0
|
|
114
114
|
|
|
115
115
|
if (spec.type === type) {
|
|
116
|
-
s |= 4
|
|
116
|
+
s |= 4 // exact match: highest specificity
|
|
117
|
+
} else if (type === "*") {
|
|
118
|
+
s |= 0 // server offers wildcard (e.g. */*): matches any client type
|
|
117
119
|
} else if (spec.type !== "*") {
|
|
120
|
+
// client is NOT requesting wildcard: no match
|
|
118
121
|
return null
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
// client requests wildcard (e.g. Accept: */*)
|
|
121
125
|
if (spec.subtype === subtype) {
|
|
122
|
-
s |= 2
|
|
126
|
+
s |= 2 // // exact match: highest specificity
|
|
127
|
+
} else if (subtype === "*") {
|
|
128
|
+
s |= 1 // server offers wildcard (e.g. text/*)
|
|
123
129
|
} else if (spec.subtype !== "*") {
|
|
124
|
-
return null
|
|
130
|
+
return null // client is NOT requesting wildcard
|
|
125
131
|
}
|
|
126
132
|
|
|
133
|
+
// client requests wildcard (e.g. Accept: text/*): matches any server subtype
|
|
127
134
|
const specParams = Object.keys(spec.params)
|
|
128
135
|
if (specParams.length > 0) {
|
|
129
136
|
if (
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as FileSystem from "@effect/platform/FileSystem"
|
|
2
|
+
import * as test from "bun:test"
|
|
3
|
+
import { MemoryFileSystem } from "effect-memfs"
|
|
4
|
+
import * as Chunk from "effect/Chunk"
|
|
5
|
+
import * as Effect from "effect/Effect"
|
|
6
|
+
import * as Fiber from "effect/Fiber"
|
|
7
|
+
import * as Layer from "effect/Layer"
|
|
8
|
+
import * as Stream from "effect/Stream"
|
|
9
|
+
import * as Development from "./Development.ts"
|
|
10
|
+
|
|
11
|
+
test.beforeEach(() => {
|
|
12
|
+
Development._resetForTesting()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test.describe("watch", () => {
|
|
16
|
+
test.it("creates pubsub and publishes file events", () =>
|
|
17
|
+
Effect
|
|
18
|
+
.gen(function*() {
|
|
19
|
+
const fs = yield* FileSystem.FileSystem
|
|
20
|
+
const watchDir = "/dev-watch"
|
|
21
|
+
|
|
22
|
+
const dev = yield* Development.watch({ path: watchDir })
|
|
23
|
+
|
|
24
|
+
const subFiber = yield* Effect.fork(
|
|
25
|
+
Stream.runCollect(
|
|
26
|
+
Stream.take(Stream.fromPubSub(dev.events), 1),
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
yield* Effect.sleep(1)
|
|
31
|
+
yield* fs.writeFileString(`${watchDir}/test.ts`, "const x = 1")
|
|
32
|
+
|
|
33
|
+
const events = yield* Fiber.join(subFiber)
|
|
34
|
+
|
|
35
|
+
test
|
|
36
|
+
.expect(Chunk.size(events))
|
|
37
|
+
.toBe(1)
|
|
38
|
+
const first = Chunk.unsafeGet(events, 0)
|
|
39
|
+
test
|
|
40
|
+
.expect("path" in first && first.path)
|
|
41
|
+
.toContain("test.ts")
|
|
42
|
+
})
|
|
43
|
+
.pipe(
|
|
44
|
+
Effect.scoped,
|
|
45
|
+
Effect.provide(
|
|
46
|
+
MemoryFileSystem.layerWith({ "/dev-watch/.gitkeep": "" }),
|
|
47
|
+
),
|
|
48
|
+
Effect.runPromise,
|
|
49
|
+
))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test.describe("layerWatch", () => {
|
|
53
|
+
test.it("provides Development service", () =>
|
|
54
|
+
Effect
|
|
55
|
+
.gen(function*() {
|
|
56
|
+
const dev = yield* Development.Development
|
|
57
|
+
|
|
58
|
+
test
|
|
59
|
+
.expect(dev.events)
|
|
60
|
+
.toBeDefined()
|
|
61
|
+
})
|
|
62
|
+
.pipe(
|
|
63
|
+
Effect.scoped,
|
|
64
|
+
Effect.provide(Development.layerWatch({ path: "/layer-test" })),
|
|
65
|
+
Effect.provide(
|
|
66
|
+
MemoryFileSystem.layerWith({ "/layer-test/.gitkeep": "" }),
|
|
67
|
+
),
|
|
68
|
+
Effect.runPromise,
|
|
69
|
+
))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test.describe("stream", () => {
|
|
73
|
+
test.it("returns stream from pubsub when Development is available", () =>
|
|
74
|
+
Effect
|
|
75
|
+
.gen(function*() {
|
|
76
|
+
const fs = yield* FileSystem.FileSystem
|
|
77
|
+
const watchDir = "/events-test"
|
|
78
|
+
|
|
79
|
+
const collectFiber = yield* Effect.fork(
|
|
80
|
+
Stream.runCollect(Stream.take(Development.stream(), 1)),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
yield* Effect.sleep(1)
|
|
84
|
+
yield* fs.writeFileString(`${watchDir}/file.ts`, "content")
|
|
85
|
+
|
|
86
|
+
const collected = yield* Fiber.join(collectFiber)
|
|
87
|
+
|
|
88
|
+
test
|
|
89
|
+
.expect(Chunk.size(collected))
|
|
90
|
+
.toBe(1)
|
|
91
|
+
const first = Chunk.unsafeGet(collected, 0)
|
|
92
|
+
test
|
|
93
|
+
.expect("path" in first && first.path)
|
|
94
|
+
.toContain("file.ts")
|
|
95
|
+
})
|
|
96
|
+
.pipe(
|
|
97
|
+
Effect.scoped,
|
|
98
|
+
Effect.provide(Development.layerWatch({ path: "/events-test" })),
|
|
99
|
+
Effect.provide(
|
|
100
|
+
MemoryFileSystem.layerWith({ "/events-test/.gitkeep": "" }),
|
|
101
|
+
),
|
|
102
|
+
Effect.runPromise,
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
test.it("returns empty stream when Development is not available", () =>
|
|
106
|
+
Effect
|
|
107
|
+
.gen(function*() {
|
|
108
|
+
const collected = yield* Stream.runCollect(Development.stream())
|
|
109
|
+
|
|
110
|
+
test
|
|
111
|
+
.expect(Chunk.size(collected))
|
|
112
|
+
.toBe(0)
|
|
113
|
+
})
|
|
114
|
+
.pipe(
|
|
115
|
+
Effect.scoped,
|
|
116
|
+
Effect.provide(Layer.empty),
|
|
117
|
+
Effect.runPromise,
|
|
118
|
+
))
|
|
119
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Context,
|
|
3
|
+
Effect,
|
|
4
|
+
Function,
|
|
5
|
+
Layer,
|
|
6
|
+
Option,
|
|
7
|
+
pipe,
|
|
8
|
+
PubSub,
|
|
9
|
+
Stream,
|
|
10
|
+
} from "effect"
|
|
11
|
+
import { globalValue } from "effect/GlobalValue"
|
|
12
|
+
import {
|
|
13
|
+
Error,
|
|
14
|
+
FileSystem,
|
|
15
|
+
} from "./node/FileSystem.ts"
|
|
16
|
+
|
|
17
|
+
export type DevelopmentEvent =
|
|
18
|
+
| FileSystem.WatchEvent
|
|
19
|
+
| {
|
|
20
|
+
readonly _tag: "Reload"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const devState = globalValue(
|
|
24
|
+
Symbol.for("effect-start/Development"),
|
|
25
|
+
() => ({
|
|
26
|
+
count: 0,
|
|
27
|
+
pubsub: null as PubSub.PubSub<DevelopmentEvent> | null,
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
/** @internal */
|
|
32
|
+
export const _resetForTesting = () => {
|
|
33
|
+
devState.count = 0
|
|
34
|
+
devState.pubsub = null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type DevelopmentService = {
|
|
38
|
+
events: PubSub.PubSub<DevelopmentEvent>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Development extends Context.Tag("effect-start/Development")<
|
|
42
|
+
Development,
|
|
43
|
+
DevelopmentService
|
|
44
|
+
>() {}
|
|
45
|
+
|
|
46
|
+
const SOURCE_FILENAME = /\.(tsx?|jsx?|html?|css|json)$/
|
|
47
|
+
|
|
48
|
+
export const filterSourceFiles = (event: FileSystem.WatchEvent): boolean => {
|
|
49
|
+
return SOURCE_FILENAME.test(event.path)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const filterDirectory = (event: FileSystem.WatchEvent): boolean => {
|
|
53
|
+
return event.path.endsWith("/")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const watchSource = (
|
|
57
|
+
opts?: {
|
|
58
|
+
path?: string
|
|
59
|
+
recursive?: boolean
|
|
60
|
+
filter?: (event: FileSystem.WatchEvent) => boolean
|
|
61
|
+
},
|
|
62
|
+
): Stream.Stream<
|
|
63
|
+
FileSystem.WatchEvent,
|
|
64
|
+
Error.PlatformError,
|
|
65
|
+
FileSystem.FileSystem
|
|
66
|
+
> => {
|
|
67
|
+
const baseDir = opts?.path ?? process.cwd()
|
|
68
|
+
const customFilter = opts?.filter
|
|
69
|
+
|
|
70
|
+
return Function.pipe(
|
|
71
|
+
Stream.unwrap(
|
|
72
|
+
Effect.map(
|
|
73
|
+
FileSystem.FileSystem,
|
|
74
|
+
fs => fs.watch(baseDir, { recursive: opts?.recursive ?? true }),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
customFilter ? Stream.filter(customFilter) : Function.identity,
|
|
78
|
+
Stream.rechunk(1),
|
|
79
|
+
Stream.throttle({
|
|
80
|
+
units: 1,
|
|
81
|
+
cost: () => 1,
|
|
82
|
+
duration: "400 millis",
|
|
83
|
+
strategy: "enforce",
|
|
84
|
+
}),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const watch = (
|
|
89
|
+
opts?: {
|
|
90
|
+
path?: string
|
|
91
|
+
recursive?: boolean
|
|
92
|
+
filter?: (event: FileSystem.WatchEvent) => boolean
|
|
93
|
+
},
|
|
94
|
+
) =>
|
|
95
|
+
Effect.gen(function*() {
|
|
96
|
+
devState.count++
|
|
97
|
+
|
|
98
|
+
if (devState.count === 1) {
|
|
99
|
+
const pubsub = yield* PubSub.unbounded<DevelopmentEvent>()
|
|
100
|
+
devState.pubsub = pubsub
|
|
101
|
+
|
|
102
|
+
yield* pipe(
|
|
103
|
+
watchSource({
|
|
104
|
+
path: opts?.path,
|
|
105
|
+
recursive: opts?.recursive,
|
|
106
|
+
filter: opts?.filter ?? filterSourceFiles,
|
|
107
|
+
}),
|
|
108
|
+
Stream.runForEach((event) => PubSub.publish(pubsub, event)),
|
|
109
|
+
Effect.fork,
|
|
110
|
+
)
|
|
111
|
+
} else {
|
|
112
|
+
yield* PubSub.publish(devState.pubsub!, { _tag: "Reload" })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { events: devState.pubsub! } satisfies DevelopmentService
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
export const layerWatch = (
|
|
119
|
+
opts?: {
|
|
120
|
+
path?: string
|
|
121
|
+
recursive?: boolean
|
|
122
|
+
filter?: (event: FileSystem.WatchEvent) => boolean
|
|
123
|
+
},
|
|
124
|
+
) => Layer.scoped(Development, watch(opts))
|
|
125
|
+
|
|
126
|
+
export const stream = (): Stream.Stream<DevelopmentEvent> =>
|
|
127
|
+
Stream.unwrap(
|
|
128
|
+
pipe(
|
|
129
|
+
Effect.serviceOption(Development),
|
|
130
|
+
Effect.map(
|
|
131
|
+
Option.match({
|
|
132
|
+
onNone: () => Stream.empty,
|
|
133
|
+
onSome: (dev) => Stream.fromPubSub(dev.events),
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
)
|