effect-start 0.30.1 → 0.32.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/dist/Fetch.d.ts +1 -1
- package/dist/Fetch.d.ts.map +1 -1
- package/dist/Fetch.js +1 -1
- package/dist/Fetch.js.map +1 -1
- package/dist/GlobalLayer.d.ts.map +1 -1
- package/dist/GlobalLayer.js.map +1 -1
- package/dist/Html.js +3 -3
- package/dist/Html.js.map +1 -1
- package/dist/Password.d.ts +26 -0
- package/dist/Password.d.ts.map +1 -0
- package/dist/Password.js +83 -0
- package/dist/Password.js.map +1 -0
- package/dist/Route.d.ts.map +1 -1
- package/dist/Route.js +1 -1
- package/dist/Route.js.map +1 -1
- package/dist/RouteBody.d.ts +10 -5
- package/dist/RouteBody.d.ts.map +1 -1
- package/dist/RouteBody.js +6 -2
- package/dist/RouteBody.js.map +1 -1
- package/dist/RouteHttp.d.ts.map +1 -1
- package/dist/RouteHttp.js +10 -3
- package/dist/RouteHttp.js.map +1 -1
- package/dist/RouteSse.d.ts +3 -4
- package/dist/RouteSse.d.ts.map +1 -1
- package/dist/RouteSse.js +3 -3
- package/dist/RouteSse.js.map +1 -1
- package/dist/RouteTree.d.ts +20 -5
- package/dist/RouteTree.d.ts.map +1 -1
- package/dist/RouteTree.js +172 -40
- package/dist/RouteTree.js.map +1 -1
- package/dist/StaticFiles.d.ts +23 -0
- package/dist/StaticFiles.d.ts.map +1 -0
- package/dist/StaticFiles.js +74 -0
- package/dist/StaticFiles.js.map +1 -0
- package/dist/_Mime.d.ts +2 -0
- package/dist/_Mime.d.ts.map +1 -0
- package/dist/_Mime.js +33 -0
- package/dist/_Mime.js.map +1 -0
- package/dist/_PathPattern.js +4 -4
- package/dist/_PathPattern.js.map +1 -1
- package/dist/_StreamExtra.d.ts.map +1 -1
- package/dist/_StreamExtra.js +0 -1
- package/dist/_StreamExtra.js.map +1 -1
- package/dist/bun/BunRoute.d.ts.map +1 -1
- package/dist/bun/BunRoute.js +6 -0
- package/dist/bun/BunRoute.js.map +1 -1
- package/dist/bun/BunServer.d.ts +2 -0
- package/dist/bun/BunServer.d.ts.map +1 -1
- package/dist/bun/BunServer.js +18 -7
- package/dist/bun/BunServer.js.map +1 -1
- package/dist/studio/Studio.d.ts +1 -1
- package/dist/studio/Studio.d.ts.map +1 -1
- package/dist/studio/Studio.js +5 -4
- package/dist/studio/Studio.js.map +1 -1
- package/dist/studio/StudioErrors.d.ts.map +1 -1
- package/dist/studio/StudioErrors.js +2 -2
- package/dist/studio/StudioErrors.js.map +1 -1
- package/dist/studio/StudioLogger.d.ts.map +1 -1
- package/dist/studio/StudioLogger.js +1 -3
- package/dist/studio/StudioLogger.js.map +1 -1
- package/dist/studio/StudioStore.d.ts +23 -17
- package/dist/studio/StudioStore.d.ts.map +1 -1
- package/dist/studio/StudioStore.js +48 -44
- package/dist/studio/StudioStore.js.map +1 -1
- package/dist/studio/StudioTracer.d.ts.map +1 -1
- package/dist/studio/StudioTracer.js +10 -4
- package/dist/studio/StudioTracer.js.map +1 -1
- package/dist/studio/routes/errors/route.d.ts +2 -2
- package/dist/studio/routes/errors/route.d.ts.map +1 -1
- package/dist/studio/routes/errors/route.js +2 -2
- package/dist/studio/routes/errors/route.js.map +1 -1
- package/dist/studio/routes/fiberDetail.d.ts +1 -1
- package/dist/studio/routes/fiberDetail.js +4 -4
- package/dist/studio/routes/fiberDetail.js.map +1 -1
- package/dist/studio/routes/fibers/route.d.ts +3 -3
- package/dist/studio/routes/fibers/route.js +4 -4
- package/dist/studio/routes/fibers/route.js.map +1 -1
- package/dist/studio/routes/logs/route.d.ts +2 -2
- package/dist/studio/routes/logs/route.d.ts.map +1 -1
- package/dist/studio/routes/logs/route.js +2 -2
- package/dist/studio/routes/logs/route.js.map +1 -1
- package/dist/studio/routes/metrics/route.d.ts +1 -1
- package/dist/studio/routes/system/route.d.ts +1 -1
- package/dist/studio/routes/traceDetail.d.ts +1 -1
- package/dist/studio/routes/traceDetail.js +1 -1
- package/dist/studio/routes/traceDetail.js.map +1 -1
- package/dist/studio/routes/traces/route.d.ts +3 -3
- package/dist/studio/routes/traces/route.js +5 -5
- package/dist/studio/routes/traces/route.js.map +1 -1
- package/dist/studio/routes/tree.d.ts +14 -14
- package/dist/studio/ui/Traces.d.ts +1 -0
- package/dist/studio/ui/Traces.d.ts.map +1 -1
- package/dist/studio/ui/Traces.js +32 -11
- package/dist/studio/ui/Traces.js.map +1 -1
- package/package.json +2 -2
- package/src/Fetch.ts +2 -2
- package/src/GlobalLayer.ts +1 -3
- package/src/Html.ts +3 -3
- package/src/Password.ts +130 -0
- package/src/Route.ts +10 -12
- package/src/RouteBody.ts +36 -14
- package/src/RouteHttp.ts +14 -3
- package/src/RouteSse.ts +10 -10
- package/src/RouteTree.ts +252 -61
- package/src/StaticFiles.ts +112 -0
- package/src/_Mime.ts +33 -0
- package/src/_PathPattern.ts +4 -4
- package/src/_StreamExtra.ts +0 -1
- package/src/bun/BunRoute.ts +10 -0
- package/src/bun/BunServer.ts +33 -7
- package/src/studio/Studio.ts +6 -5
- package/src/studio/StudioErrors.ts +2 -3
- package/src/studio/StudioLogger.ts +2 -3
- package/src/studio/StudioStore.ts +159 -115
- package/src/studio/StudioTracer.ts +11 -5
- package/src/studio/routes/errors/route.tsx +4 -5
- package/src/studio/routes/fiberDetail.tsx +4 -4
- package/src/studio/routes/fibers/route.tsx +4 -4
- package/src/studio/routes/logs/route.tsx +3 -1
- package/src/studio/routes/traceDetail.tsx +1 -1
- package/src/studio/routes/traces/route.tsx +8 -8
- package/src/studio/ui/Traces.tsx +41 -13
package/src/RouteSse.ts
CHANGED
|
@@ -12,10 +12,9 @@ const HEARTBEAT_INTERVAL = Duration.seconds(5)
|
|
|
12
12
|
const HEARTBEAT = ": <3\n\n"
|
|
13
13
|
|
|
14
14
|
interface SseEvent {
|
|
15
|
-
readonly [key: string]: unknown
|
|
16
15
|
readonly _tag?: string
|
|
17
16
|
readonly data?: string | undefined
|
|
18
|
-
readonly
|
|
17
|
+
readonly event?: string
|
|
19
18
|
readonly retry?: number
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -26,8 +25,8 @@ function formatSseEvent(event: SseEvent): string {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
let result = ""
|
|
29
|
-
if (event.
|
|
30
|
-
result += `event: ${event.
|
|
28
|
+
if (event.event) {
|
|
29
|
+
result += `event: ${event.event}\n`
|
|
31
30
|
}
|
|
32
31
|
if (typeof event.data === "string") {
|
|
33
32
|
for (const line of event.data.split("\n")) {
|
|
@@ -48,6 +47,7 @@ type SseHandlerInput<B, E, R> =
|
|
|
48
47
|
| Effect.Effect<Stream.Stream<SseEvent, E, R>, E, R>
|
|
49
48
|
| ((
|
|
50
49
|
context: Values.Simplify<B>,
|
|
50
|
+
// TODO: typing is super loose here
|
|
51
51
|
next: (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<string>,
|
|
52
52
|
) =>
|
|
53
53
|
| Stream.Stream<SseEvent, E, R>
|
|
@@ -64,10 +64,10 @@ export function sse<
|
|
|
64
64
|
I extends Route.Route.Tuple,
|
|
65
65
|
E = never,
|
|
66
66
|
R = never,
|
|
67
|
-
>(handler: SseHandlerInput<NoInfer<D & B & Route.ExtractBindings<I> & { format: "
|
|
67
|
+
>(handler: SseHandlerInput<NoInfer<D & B & Route.ExtractBindings<I> & { format: "sse" }>, E, R>) {
|
|
68
68
|
return function (self: Route.RouteSet.RouteSet<D, B, I>) {
|
|
69
69
|
const sseHandler: Route.Route.Handler<
|
|
70
|
-
D & B & Route.ExtractBindings<I> & { format: "
|
|
70
|
+
D & B & Route.ExtractBindings<I> & { format: "sse" },
|
|
71
71
|
Stream.Stream<string, E, R>,
|
|
72
72
|
E,
|
|
73
73
|
R
|
|
@@ -113,20 +113,20 @@ export function sse<
|
|
|
113
113
|
})
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
const route = Route.make<{ format: "
|
|
116
|
+
const route = Route.make<{ format: "sse" }, {}, Stream.Stream<string, E, R>, E, R>(
|
|
117
117
|
sseHandler as any,
|
|
118
|
-
{ format: "
|
|
118
|
+
{ format: "sse" },
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
const items: [
|
|
122
122
|
...I,
|
|
123
|
-
Route.Route.Route<{ format: "
|
|
123
|
+
Route.Route.Route<{ format: "sse" }, {}, Stream.Stream<string, E, R>, E, R>,
|
|
124
124
|
] = [...Route.items(self), route]
|
|
125
125
|
|
|
126
126
|
return Route.set<
|
|
127
127
|
D,
|
|
128
128
|
B,
|
|
129
|
-
[...I, Route.Route.Route<{ format: "
|
|
129
|
+
[...I, Route.Route.Route<{ format: "sse" }, {}, Stream.Stream<string, E, R>, E, R>]
|
|
130
130
|
>(items, Route.descriptor(self))
|
|
131
131
|
}
|
|
132
132
|
}
|
package/src/RouteTree.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as Predicate from "effect/Predicate"
|
|
2
|
-
import * as PathPattern from "./_PathPattern.ts"
|
|
2
|
+
import type * as PathPattern from "./_PathPattern.ts"
|
|
3
3
|
import * as Route from "./Route.ts"
|
|
4
4
|
import type * as RouteMount from "./RouteMount.ts"
|
|
5
5
|
|
|
6
6
|
const TypeId = "~effect-start/RouteTree" as const
|
|
7
7
|
const RouteTreeRoutes: unique symbol = Symbol()
|
|
8
|
+
const CompiledRoutesKey: unique symbol = Symbol()
|
|
8
9
|
|
|
9
10
|
type MethodRoute = Route.Route.With<{ method: string }>
|
|
10
11
|
|
|
@@ -27,40 +28,25 @@ export type RouteMap = {
|
|
|
27
28
|
|
|
28
29
|
export type Routes<T extends RouteTree> = T[typeof RouteTreeRoutes]
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
interface CompiledMethod {
|
|
32
|
+
regex: RegExp
|
|
33
|
+
table: Array<{
|
|
34
|
+
path: PathPattern.PathPattern
|
|
35
|
+
routes: Array<RouteMount.MountedRoute>
|
|
36
|
+
paramNames: Array<string>
|
|
37
|
+
paramGroupIndices: Array<number>
|
|
38
|
+
sentinelIndex: number
|
|
39
|
+
}>
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
interface CompiledRoutes {
|
|
43
|
+
methods: Record<string, CompiledMethod>
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const maxPriority = Math.max(
|
|
44
|
-
...segments.map((s) =>
|
|
45
|
-
!s.startsWith(":") ? 0 : s.endsWith("*") ? 4 : s.endsWith("+") ? 3 : s.endsWith("?") ? 2 : 1,
|
|
46
|
-
),
|
|
47
|
-
0,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
return greedyIdx === -1
|
|
51
|
-
? // non-greedy: sort by depth, then by max segment priority
|
|
52
|
-
(segments.length << 16) + (maxPriority << 8)
|
|
53
|
-
: // greedy: sort after non-greedy, by greedy position (later = first), then priority
|
|
54
|
-
(1 << 24) + ((16 - greedyIdx) << 16) + (maxPriority << 8)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function sortRoutes(input: RouteMap): RouteMap {
|
|
58
|
-
const keys = Object.keys(input).sort((a, b) => sortScore(a) - sortScore(b) || a.localeCompare(b))
|
|
59
|
-
const sorted: RouteMap = {}
|
|
60
|
-
for (const key of keys) {
|
|
61
|
-
sorted[key as PathPattern.PathPattern] = input[key as PathPattern.PathPattern]
|
|
62
|
-
}
|
|
63
|
-
return sorted
|
|
46
|
+
export interface RouteTree<Routes extends RouteMap = RouteMap> {
|
|
47
|
+
[TypeId]: typeof TypeId
|
|
48
|
+
[RouteTreeRoutes]: Routes
|
|
49
|
+
[CompiledRoutesKey]?: CompiledRoutes
|
|
64
50
|
}
|
|
65
51
|
|
|
66
52
|
type PrefixKeys<T, Prefix extends string> = {
|
|
@@ -92,6 +78,16 @@ type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (
|
|
|
92
78
|
? I
|
|
93
79
|
: never
|
|
94
80
|
|
|
81
|
+
export type WalkDescriptor = {
|
|
82
|
+
path: PathPattern.PathPattern
|
|
83
|
+
method: string
|
|
84
|
+
} & Route.RouteDescriptor.Any
|
|
85
|
+
|
|
86
|
+
export interface LookupResult {
|
|
87
|
+
route: RouteMount.MountedRoute
|
|
88
|
+
params: Record<string, string>
|
|
89
|
+
}
|
|
90
|
+
|
|
95
91
|
export function make<const Routes extends InputRouteMap>(
|
|
96
92
|
input: Routes,
|
|
97
93
|
): RouteTree<FlattenRouteMap<Routes>> {
|
|
@@ -115,30 +111,13 @@ export function make<const Routes extends InputRouteMap>(
|
|
|
115
111
|
|
|
116
112
|
flatten(input, "", layerRoutes)
|
|
117
113
|
|
|
114
|
+
const sorted = sortRoutes(merged)
|
|
118
115
|
return {
|
|
119
116
|
[TypeId]: TypeId,
|
|
120
|
-
[RouteTreeRoutes]:
|
|
117
|
+
[RouteTreeRoutes]: sorted,
|
|
121
118
|
} as RouteTree<FlattenRouteMap<Routes>>
|
|
122
119
|
}
|
|
123
120
|
|
|
124
|
-
export type WalkDescriptor = {
|
|
125
|
-
path: PathPattern.PathPattern
|
|
126
|
-
method: string
|
|
127
|
-
} & Route.RouteDescriptor.Any
|
|
128
|
-
|
|
129
|
-
function* flattenRoutes(
|
|
130
|
-
path: PathPattern.PathPattern,
|
|
131
|
-
routes: Iterable<MethodRoute>,
|
|
132
|
-
): Generator<RouteMount.MountedRoute> {
|
|
133
|
-
for (const route of routes) {
|
|
134
|
-
const descriptor = {
|
|
135
|
-
...route[Route.RouteDescriptor],
|
|
136
|
-
path,
|
|
137
|
-
}
|
|
138
|
-
yield Route.make(route.handler as any, descriptor) as RouteMount.MountedRoute
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
121
|
export function* walk(tree: RouteTree): Generator<RouteMount.MountedRoute> {
|
|
143
122
|
const _routes = routes(tree) as RouteMap
|
|
144
123
|
|
|
@@ -153,9 +132,10 @@ export function merge(a: RouteTree, b: RouteTree): RouteTree {
|
|
|
153
132
|
const key = path as PathPattern.PathPattern
|
|
154
133
|
combined[key] = combined[key] ? [...combined[key], ...items] : items
|
|
155
134
|
}
|
|
135
|
+
const sorted = sortRoutes(combined)
|
|
156
136
|
return {
|
|
157
137
|
[TypeId]: TypeId,
|
|
158
|
-
[RouteTreeRoutes]:
|
|
138
|
+
[RouteTreeRoutes]: sorted,
|
|
159
139
|
} as RouteTree
|
|
160
140
|
}
|
|
161
141
|
|
|
@@ -163,21 +143,232 @@ export function isRouteTree(input: unknown): input is RouteTree {
|
|
|
163
143
|
return Predicate.hasProperty(input, TypeId)
|
|
164
144
|
}
|
|
165
145
|
|
|
166
|
-
export
|
|
167
|
-
|
|
168
|
-
|
|
146
|
+
export function lookup(tree: RouteTree, method: string, path: string): LookupResult | null {
|
|
147
|
+
tree[CompiledRoutesKey] ??= compileRoutes(routes(tree))
|
|
148
|
+
const { methods } = tree[CompiledRoutesKey]
|
|
149
|
+
|
|
150
|
+
const wildcard = methods["*"]
|
|
151
|
+
if (wildcard) {
|
|
152
|
+
const result = execCompiled(wildcard, path)
|
|
153
|
+
if (result) return result
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const specific = methods[method]
|
|
157
|
+
if (specific) {
|
|
158
|
+
const result = execCompiled(specific, path)
|
|
159
|
+
if (result) return result
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null
|
|
169
163
|
}
|
|
170
164
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
165
|
+
function routes<Routes extends RouteMap>(tree: RouteTree<Routes>): Routes {
|
|
166
|
+
return tree[RouteTreeRoutes]
|
|
167
|
+
}
|
|
174
168
|
|
|
175
|
-
|
|
169
|
+
// segment priority: static (0) < :param (1) < :param? (2) < :param+ (3) < :param* (4)
|
|
170
|
+
function sortScore(path: string): number {
|
|
171
|
+
const segments = path.split("/")
|
|
172
|
+
const greedyIdx = segments.findIndex((s) => s.endsWith("*") || s.endsWith("+"))
|
|
173
|
+
const maxPriority = Math.max(
|
|
174
|
+
...segments.map((s) =>
|
|
175
|
+
!s.startsWith(":") ? 0 : s.endsWith("*") ? 4 : s.endsWith("+") ? 3 : s.endsWith("?") ? 2 : 1,
|
|
176
|
+
),
|
|
177
|
+
0,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return greedyIdx === -1
|
|
181
|
+
? // non-greedy: sort by depth, then by max segment priority
|
|
182
|
+
(segments.length << 16) + (maxPriority << 8)
|
|
183
|
+
: // greedy: sort after non-greedy, by greedy position (later = first), then priority
|
|
184
|
+
(1 << 24) + ((16 - greedyIdx) << 16) + (maxPriority << 8)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function sortRoutes(input: RouteMap): RouteMap {
|
|
188
|
+
const keys = Object.keys(input).sort((a, b) => sortScore(a) - sortScore(b) || a.localeCompare(b))
|
|
189
|
+
const sorted: RouteMap = {}
|
|
190
|
+
for (const key of keys) {
|
|
191
|
+
sorted[key as PathPattern.PathPattern] = input[key as PathPattern.PathPattern]
|
|
192
|
+
}
|
|
193
|
+
return sorted
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function* flattenRoutes(
|
|
197
|
+
path: PathPattern.PathPattern,
|
|
198
|
+
routes: Iterable<MethodRoute>,
|
|
199
|
+
): Generator<RouteMount.MountedRoute> {
|
|
200
|
+
for (const route of routes) {
|
|
201
|
+
const descriptor = {
|
|
202
|
+
...route[Route.RouteDescriptor],
|
|
203
|
+
path,
|
|
204
|
+
}
|
|
205
|
+
yield Route.make(route.handler as any, descriptor) as RouteMount.MountedRoute
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function escapeRegex(s: string): string {
|
|
210
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function patternToRegex(pattern: string): {
|
|
214
|
+
fragment: string
|
|
215
|
+
paramNames: Array<string>
|
|
216
|
+
groupCount: number
|
|
217
|
+
} {
|
|
218
|
+
const segments = pattern.split("/").filter(Boolean)
|
|
219
|
+
const paramNames: Array<string> = []
|
|
220
|
+
let fragment = ""
|
|
221
|
+
let groupCount = 0
|
|
222
|
+
|
|
223
|
+
for (let i = 0; i < segments.length; i++) {
|
|
224
|
+
const seg = segments[i]
|
|
225
|
+
|
|
226
|
+
if (seg.startsWith(":")) {
|
|
227
|
+
const last = seg[seg.length - 1]
|
|
228
|
+
if (last === "+") {
|
|
229
|
+
const name = seg.slice(1, -1)
|
|
230
|
+
paramNames.push(name)
|
|
231
|
+
fragment += "\\/(.+)"
|
|
232
|
+
groupCount++
|
|
233
|
+
} else if (last === "*") {
|
|
234
|
+
const name = seg.slice(1, -1)
|
|
235
|
+
paramNames.push(name)
|
|
236
|
+
fragment += "(?:\\/(.+))?"
|
|
237
|
+
groupCount++
|
|
238
|
+
} else if (last === "?") {
|
|
239
|
+
const name = seg.slice(1, -1)
|
|
240
|
+
paramNames.push(name)
|
|
241
|
+
fragment += "(?:\\/([^\\/]+))?"
|
|
242
|
+
groupCount++
|
|
243
|
+
} else {
|
|
244
|
+
const name = seg.slice(1)
|
|
245
|
+
paramNames.push(name)
|
|
246
|
+
fragment += "\\/([^\\/]+)"
|
|
247
|
+
groupCount++
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
fragment += "\\/" + escapeRegex(seg)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { fragment, paramNames, groupCount }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function compileRoutes(sortedRoutes: RouteMap): CompiledRoutes {
|
|
258
|
+
const methodGroups: Record<
|
|
259
|
+
string,
|
|
260
|
+
Array<{
|
|
261
|
+
path: PathPattern.PathPattern
|
|
262
|
+
route: RouteMount.MountedRoute
|
|
263
|
+
}>
|
|
264
|
+
> = {}
|
|
265
|
+
|
|
266
|
+
for (const path of Object.keys(sortedRoutes) as Array<PathPattern.PathPattern>) {
|
|
267
|
+
for (const routeData of sortedRoutes[path]) {
|
|
268
|
+
const descriptor = {
|
|
269
|
+
...routeData[Route.RouteDescriptor],
|
|
270
|
+
path,
|
|
271
|
+
}
|
|
272
|
+
const mounted = Route.make(routeData.handler as any, descriptor) as RouteMount.MountedRoute
|
|
273
|
+
const method = (descriptor as { method: string }).method
|
|
274
|
+
|
|
275
|
+
if (!methodGroups[method]) methodGroups[method] = []
|
|
276
|
+
methodGroups[method].push({ path, route: mounted })
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const pathRoutesByMethod: Record<
|
|
281
|
+
string,
|
|
282
|
+
Map<
|
|
283
|
+
string,
|
|
284
|
+
{
|
|
285
|
+
path: PathPattern.PathPattern
|
|
286
|
+
routes: Array<RouteMount.MountedRoute>
|
|
287
|
+
}
|
|
288
|
+
>
|
|
289
|
+
> = {}
|
|
290
|
+
|
|
291
|
+
for (const method of Object.keys(methodGroups)) {
|
|
292
|
+
const map = new Map<
|
|
293
|
+
string,
|
|
294
|
+
{
|
|
295
|
+
path: PathPattern.PathPattern
|
|
296
|
+
routes: Array<RouteMount.MountedRoute>
|
|
297
|
+
}
|
|
298
|
+
>()
|
|
299
|
+
for (const { path, route } of methodGroups[method]) {
|
|
300
|
+
let entry = map.get(path)
|
|
301
|
+
if (!entry) {
|
|
302
|
+
entry = { path, routes: [] }
|
|
303
|
+
map.set(path, entry)
|
|
304
|
+
}
|
|
305
|
+
entry.routes.push(route)
|
|
306
|
+
}
|
|
307
|
+
pathRoutesByMethod[method] = map
|
|
308
|
+
}
|
|
176
309
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
310
|
+
const methods: Record<string, CompiledMethod> = {}
|
|
311
|
+
|
|
312
|
+
for (const method of Object.keys(pathRoutesByMethod)) {
|
|
313
|
+
const pathMap = pathRoutesByMethod[method]
|
|
314
|
+
const sortedPaths = Object.keys(sortedRoutes) as Array<PathPattern.PathPattern>
|
|
315
|
+
const orderedPaths = sortedPaths.filter((p) => pathMap.has(p))
|
|
316
|
+
|
|
317
|
+
const branches: Array<string> = []
|
|
318
|
+
const table: CompiledMethod["table"] = []
|
|
319
|
+
let groupOffset = 1
|
|
320
|
+
|
|
321
|
+
for (const path of orderedPaths) {
|
|
322
|
+
const entry = pathMap.get(path)!
|
|
323
|
+
const { fragment, paramNames, groupCount } = patternToRegex(path)
|
|
324
|
+
|
|
325
|
+
const paramGroupIndices: Array<number> = []
|
|
326
|
+
for (let i = 0; i < groupCount; i++) {
|
|
327
|
+
paramGroupIndices.push(groupOffset + i)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const sentinelIndex = groupOffset + groupCount
|
|
331
|
+
branches.push(fragment + "()")
|
|
332
|
+
groupOffset += groupCount + 1
|
|
333
|
+
|
|
334
|
+
table.push({
|
|
335
|
+
path: entry.path,
|
|
336
|
+
routes: entry.routes,
|
|
337
|
+
paramNames,
|
|
338
|
+
paramGroupIndices,
|
|
339
|
+
sentinelIndex,
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (branches.length === 0) continue
|
|
344
|
+
|
|
345
|
+
const pattern = "^(?:" + branches.join("|") + ")\\/*$"
|
|
346
|
+
methods[method] = {
|
|
347
|
+
regex: new RegExp(pattern),
|
|
348
|
+
table,
|
|
180
349
|
}
|
|
181
350
|
}
|
|
351
|
+
|
|
352
|
+
return { methods }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function execCompiled(compiled: CompiledMethod, path: string): LookupResult | null {
|
|
356
|
+
const match = compiled.regex.exec(path)
|
|
357
|
+
if (!match) return null
|
|
358
|
+
|
|
359
|
+
for (const entry of compiled.table) {
|
|
360
|
+
if (match[entry.sentinelIndex] === undefined) continue
|
|
361
|
+
|
|
362
|
+
const params: Record<string, string> = {}
|
|
363
|
+
for (let i = 0; i < entry.paramNames.length; i++) {
|
|
364
|
+
const val = match[entry.paramGroupIndices[i]]
|
|
365
|
+
if (val !== undefined) {
|
|
366
|
+
params[entry.paramNames[i]] = val
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return { route: entry.routes[0], params }
|
|
371
|
+
}
|
|
372
|
+
|
|
182
373
|
return null
|
|
183
374
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Layer from "effect/Layer"
|
|
3
|
+
import * as Option from "effect/Option"
|
|
4
|
+
import * as Schema from "effect/Schema"
|
|
5
|
+
import * as Entity from "./Entity.ts"
|
|
6
|
+
import * as FileSystem from "./FileSystem.ts"
|
|
7
|
+
import * as PathPattern from "./_PathPattern.ts"
|
|
8
|
+
import * as Mime from "./_Mime.ts"
|
|
9
|
+
import * as Route from "./Route.ts"
|
|
10
|
+
import * as RouteSchema from "./RouteSchema.ts"
|
|
11
|
+
import * as RouteTree from "./RouteTree.ts"
|
|
12
|
+
import * as System from "./System.ts"
|
|
13
|
+
|
|
14
|
+
const defaultMountPath = "/assets"
|
|
15
|
+
const PathParamsSchema = Schema.Struct({
|
|
16
|
+
path: Schema.optional(Schema.String),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const emptyNotFound = Entity.make(new Uint8Array(0), { status: 404 })
|
|
20
|
+
|
|
21
|
+
export const make = (directory: string) =>
|
|
22
|
+
Route.get(
|
|
23
|
+
RouteSchema.schemaPathParams(PathParamsSchema),
|
|
24
|
+
Route.render(function* (ctx) {
|
|
25
|
+
const fs = yield* FileSystem.FileSystem
|
|
26
|
+
const relativePath =
|
|
27
|
+
typeof ctx.pathParams.path === "string" ? normalizeRelativePath(ctx.pathParams.path) : null
|
|
28
|
+
|
|
29
|
+
if (!relativePath) {
|
|
30
|
+
return emptyNotFound
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const absolutePath = `${directory.replace(/[\\/]+$/, "")}/${relativePath}`
|
|
34
|
+
const info = yield* fs.stat(absolutePath).pipe(
|
|
35
|
+
Effect.catchAll((error) =>
|
|
36
|
+
isNotFound(error) ? Effect.succeed(null) : Effect.fail(error),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if (info === null || info.type !== "File") {
|
|
41
|
+
return emptyNotFound
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const headers: Entity.Headers = {
|
|
45
|
+
"content-length": String(info.size),
|
|
46
|
+
"content-type": Mime.fromPath(relativePath),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Option.isSome(info.mtime)) {
|
|
50
|
+
headers["last-modified"] = info.mtime.value.toUTCString()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const bytes = yield* fs.readFile(absolutePath)
|
|
54
|
+
return Entity.make(bytes, { headers })
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
export const layer = (options: {
|
|
59
|
+
directory: string
|
|
60
|
+
path?: string
|
|
61
|
+
}) =>
|
|
62
|
+
Layer.effect(
|
|
63
|
+
Route.Routes,
|
|
64
|
+
Effect.gen(function* () {
|
|
65
|
+
const existing = yield* Effect.serviceOption(Route.Routes).pipe(
|
|
66
|
+
Effect.andThen(Option.getOrUndefined),
|
|
67
|
+
)
|
|
68
|
+
const trimmedMountPath = (options.path ?? defaultMountPath).trim().replace(/^\/+|\/+$/g, "")
|
|
69
|
+
const mountPattern = (
|
|
70
|
+
trimmedMountPath.length > 0 ? `/${trimmedMountPath}/:path+` : "/:path+"
|
|
71
|
+
) as PathPattern.PathPattern
|
|
72
|
+
const staticFilesTree = Route.tree({
|
|
73
|
+
[mountPattern]: make(options.directory),
|
|
74
|
+
})
|
|
75
|
+
if (!existing) {
|
|
76
|
+
return staticFilesTree
|
|
77
|
+
}
|
|
78
|
+
return RouteTree.merge(existing, staticFilesTree)
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
function normalizeRelativePath(path: string): string | null {
|
|
83
|
+
if (path.length === 0 || path.startsWith("/") || path.startsWith("\\")) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const segments = path.split(/[\\/]+/)
|
|
88
|
+
const normalized: Array<string> = []
|
|
89
|
+
|
|
90
|
+
for (const segment of segments) {
|
|
91
|
+
if (segment === "" || segment === ".") {
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if (segment === ".." || segment.includes("\0")) {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
normalized.push(segment)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return normalized.length > 0 ? normalized.join("/") : null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isNotFound(error: unknown): boolean {
|
|
104
|
+
return (
|
|
105
|
+
typeof error === "object" &&
|
|
106
|
+
error !== null &&
|
|
107
|
+
"_tag" in error &&
|
|
108
|
+
error._tag === "SystemError" &&
|
|
109
|
+
"reason" in error &&
|
|
110
|
+
error.reason === "NotFound"
|
|
111
|
+
)
|
|
112
|
+
}
|
package/src/_Mime.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const mediaTypes: Record<string, string> = {
|
|
2
|
+
".avif": "image/avif",
|
|
3
|
+
".bmp": "image/bmp",
|
|
4
|
+
".css": "text/css",
|
|
5
|
+
".csv": "text/csv",
|
|
6
|
+
".gif": "image/gif",
|
|
7
|
+
".html": "text/html",
|
|
8
|
+
".ico": "image/x-icon",
|
|
9
|
+
".js": "text/javascript",
|
|
10
|
+
".jpeg": "image/jpeg",
|
|
11
|
+
".jpg": "image/jpeg",
|
|
12
|
+
".json": "application/json",
|
|
13
|
+
".map": "application/json",
|
|
14
|
+
".md": "text/markdown",
|
|
15
|
+
".mjs": "text/javascript",
|
|
16
|
+
".pdf": "application/pdf",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".svg": "image/svg+xml",
|
|
19
|
+
".txt": "text/plain",
|
|
20
|
+
".wasm": "application/wasm",
|
|
21
|
+
".webm": "video/webm",
|
|
22
|
+
".webp": "image/webp",
|
|
23
|
+
".woff": "font/woff",
|
|
24
|
+
".woff2": "font/woff2",
|
|
25
|
+
".xml": "application/xml",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fromPath(path: string): string {
|
|
29
|
+
const dotIndex = path.lastIndexOf(".")
|
|
30
|
+
const extension = dotIndex === -1 ? "" : path.slice(dotIndex).toLowerCase()
|
|
31
|
+
const mediaType = mediaTypes[extension] ?? "application/octet-stream"
|
|
32
|
+
return mediaType.startsWith("text/") ? `${mediaType}; charset=utf-8` : mediaType
|
|
33
|
+
}
|
package/src/_PathPattern.ts
CHANGED
|
@@ -175,7 +175,7 @@ export function match(pattern: string, path: string): Record<string, string> | n
|
|
|
175
175
|
if (remaining.length === 0) {
|
|
176
176
|
return null
|
|
177
177
|
}
|
|
178
|
-
params[name] = remaining.join("/")
|
|
178
|
+
params[name] = remaining.map(decodeURIComponent).join("/")
|
|
179
179
|
return params
|
|
180
180
|
}
|
|
181
181
|
|
|
@@ -183,7 +183,7 @@ export function match(pattern: string, path: string): Record<string, string> | n
|
|
|
183
183
|
const name = rest.slice(0, -1)
|
|
184
184
|
const remaining = pathSegments.slice(pathIndex)
|
|
185
185
|
if (remaining.length > 0) {
|
|
186
|
-
params[name] = remaining.join("/")
|
|
186
|
+
params[name] = remaining.map(decodeURIComponent).join("/")
|
|
187
187
|
}
|
|
188
188
|
return params
|
|
189
189
|
}
|
|
@@ -191,7 +191,7 @@ export function match(pattern: string, path: string): Record<string, string> | n
|
|
|
191
191
|
if (rest.endsWith("?")) {
|
|
192
192
|
const name = rest.slice(0, -1)
|
|
193
193
|
if (pathIndex < pathSegments.length) {
|
|
194
|
-
params[name] = pathSegments[pathIndex]
|
|
194
|
+
params[name] = decodeURIComponent(pathSegments[pathIndex])
|
|
195
195
|
pathIndex++
|
|
196
196
|
}
|
|
197
197
|
patternIndex++
|
|
@@ -202,7 +202,7 @@ export function match(pattern: string, path: string): Record<string, string> | n
|
|
|
202
202
|
return null
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
params[rest] = pathSegments[pathIndex]
|
|
205
|
+
params[rest] = decodeURIComponent(pathSegments[pathIndex])
|
|
206
206
|
pathIndex++
|
|
207
207
|
patternIndex++
|
|
208
208
|
continue
|
package/src/_StreamExtra.ts
CHANGED
package/src/bun/BunRoute.ts
CHANGED
|
@@ -139,6 +139,16 @@ export function htmlBundle(load: () => HTMLBundleModule | Promise<HTMLBundleModu
|
|
|
139
139
|
const childEntity = yield* next(context).pipe(
|
|
140
140
|
Effect.locally(bundleDepthRef, bundleDepth + 1),
|
|
141
141
|
)
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
Entity.isEntity(childEntity) &&
|
|
145
|
+
childEntity.status &&
|
|
146
|
+
childEntity.status >= 300 &&
|
|
147
|
+
childEntity.status < 400
|
|
148
|
+
) {
|
|
149
|
+
return childEntity
|
|
150
|
+
}
|
|
151
|
+
|
|
142
152
|
const children = childEntity?.body ?? childEntity
|
|
143
153
|
|
|
144
154
|
let childrenHtml = ""
|