effect-start 0.30.0 → 0.31.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/FileRouter.d.ts.map +1 -1
- package/dist/FileRouter.js +1 -0
- package/dist/FileRouter.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/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 +2 -2
- package/dist/RouteSse.d.ts.map +1 -1
- package/dist/RouteSse.js +1 -1
- 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/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/routes/errors/route.d.ts +1 -1
- package/dist/studio/routes/errors/route.d.ts.map +1 -1
- package/dist/studio/routes/errors/route.js.map +1 -1
- package/dist/studio/routes/fibers/route.d.ts +1 -1
- package/dist/studio/routes/logs/route.d.ts +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/traces/route.d.ts +1 -1
- package/dist/studio/routes/traces/route.d.ts.map +1 -1
- package/dist/studio/routes/traces/route.js.map +1 -1
- package/dist/studio/routes/tree.d.ts +6 -6
- package/package.json +2 -2
- package/src/FileRouter.ts +1 -0
- package/src/GlobalLayer.ts +1 -3
- package/src/Html.ts +3 -3
- package/src/Route.ts +10 -12
- package/src/RouteBody.ts +36 -14
- package/src/RouteHttp.ts +14 -3
- package/src/RouteSse.ts +6 -6
- 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/bun/BunRoute.ts +10 -0
- package/src/bun/BunServer.ts +33 -7
- package/src/studio/routes/errors/route.tsx +1 -4
- package/src/studio/routes/traces/route.tsx +1 -4
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/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 = ""
|
package/src/bun/BunServer.ts
CHANGED
|
@@ -50,6 +50,7 @@ export type BunServer = {
|
|
|
50
50
|
readonly server: Bun.Server<WebSocketContext>
|
|
51
51
|
readonly pushHandler: (fetch: FetchHandler) => void
|
|
52
52
|
readonly popHandler: () => void
|
|
53
|
+
readonly setRoutes: (tree: RouteTree.RouteTree) => Effect.Effect<void>
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export const BunServer = Context.GenericTag<BunServer>("effect-start/BunServer")
|
|
@@ -77,6 +78,9 @@ export const make = (
|
|
|
77
78
|
},
|
|
78
79
|
]
|
|
79
80
|
|
|
81
|
+
const setRoutesDeferred =
|
|
82
|
+
yield* Deferred.make<(tree: RouteTree.RouteTree) => Effect.Effect<void>>()
|
|
83
|
+
|
|
80
84
|
const service = BunServer.of({
|
|
81
85
|
// During the construction we need to create a service imlpementation
|
|
82
86
|
// first so we can provide it in the runtime that will be used in web
|
|
@@ -93,6 +97,11 @@ export const make = (
|
|
|
93
97
|
handlerStack.pop()
|
|
94
98
|
reload()
|
|
95
99
|
},
|
|
100
|
+
setRoutes(tree) {
|
|
101
|
+
return Deferred.await(setRoutesDeferred).pipe(
|
|
102
|
+
Effect.flatMap((applyRoutes) => applyRoutes(tree)),
|
|
103
|
+
)
|
|
104
|
+
},
|
|
96
105
|
})
|
|
97
106
|
|
|
98
107
|
const runtime = yield* Effect.runtime().pipe(
|
|
@@ -150,6 +159,18 @@ export const make = (
|
|
|
150
159
|
})
|
|
151
160
|
}
|
|
152
161
|
|
|
162
|
+
yield* Deferred.succeed(setRoutesDeferred, (tree) =>
|
|
163
|
+
walkBunRoutes(runtime, tree).pipe(
|
|
164
|
+
Effect.tap((bunRoutes) =>
|
|
165
|
+
Effect.sync(() => {
|
|
166
|
+
currentRoutes = bunRoutes
|
|
167
|
+
reload()
|
|
168
|
+
}),
|
|
169
|
+
),
|
|
170
|
+
Effect.asVoid,
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
153
174
|
const bunServer = BunServer.of({
|
|
154
175
|
server,
|
|
155
176
|
pushHandler(fetch) {
|
|
@@ -160,6 +181,11 @@ export const make = (
|
|
|
160
181
|
handlerStack.pop()
|
|
161
182
|
reload()
|
|
162
183
|
},
|
|
184
|
+
setRoutes(tree) {
|
|
185
|
+
return Deferred.await(setRoutesDeferred).pipe(
|
|
186
|
+
Effect.flatMap((applyRoutes) => applyRoutes(tree)),
|
|
187
|
+
)
|
|
188
|
+
},
|
|
163
189
|
})
|
|
164
190
|
|
|
165
191
|
return bunServer
|
|
@@ -185,6 +211,7 @@ export const layerRoutes = (
|
|
|
185
211
|
/**
|
|
186
212
|
* Resolves the Bun server in one place for Start.serve so routes are available:
|
|
187
213
|
* 1) Reuse a user-provided BunServer when one already exists in context.
|
|
214
|
+
* If Route.Routes are available, upgrade the existing server with them.
|
|
188
215
|
* 2) Otherwise create the server from Route.Routes when routes are available.
|
|
189
216
|
* 3) Otherwise create a fallback server with the default 404 handler.
|
|
190
217
|
*/
|
|
@@ -195,18 +222,17 @@ export const layerStart = (
|
|
|
195
222
|
BunServer,
|
|
196
223
|
Effect.gen(function* () {
|
|
197
224
|
const app = yield* StartApp.StartApp
|
|
225
|
+
const routes = yield* Effect.serviceOption(Route.Routes)
|
|
226
|
+
const routeTree = Option.getOrNull(routes)
|
|
198
227
|
const existing = yield* Effect.serviceOption(BunServer)
|
|
199
228
|
if (Option.isSome(existing)) {
|
|
229
|
+
if (routeTree !== null) {
|
|
230
|
+
yield* existing.value.setRoutes(routeTree)
|
|
231
|
+
}
|
|
200
232
|
yield* Deferred.succeed(app.server, existing.value)
|
|
201
233
|
return existing.value
|
|
202
234
|
}
|
|
203
|
-
const
|
|
204
|
-
if (Option.isSome(routes)) {
|
|
205
|
-
const server = yield* make(options ?? {}, routes.value)
|
|
206
|
-
yield* Deferred.succeed(app.server, server)
|
|
207
|
-
return server
|
|
208
|
-
}
|
|
209
|
-
const server = yield* make(options ?? {})
|
|
235
|
+
const server = yield* make(options ?? {}, routeTree ?? undefined)
|
|
210
236
|
yield* Deferred.succeed(app.server, server)
|
|
211
237
|
return server
|
|
212
238
|
}),
|
|
@@ -75,10 +75,7 @@ export default Route.get(
|
|
|
75
75
|
Stream.fromPubSub(StudioStore.store.events).pipe(
|
|
76
76
|
Stream.filter((e) => e._tag === "Error"),
|
|
77
77
|
Stream.map((e) => {
|
|
78
|
-
const html = Html.renderToString(<Errors.ErrorLine error={e.error} />).replace(
|
|
79
|
-
/\n/g,
|
|
80
|
-
"",
|
|
81
|
-
)
|
|
78
|
+
const html = Html.renderToString(<Errors.ErrorLine error={e.error} />).replace(/\n/g, "")
|
|
82
79
|
return {
|
|
83
80
|
event: "datastar-patch-elements",
|
|
84
81
|
data: `selector #errors-list\nmode prepend\nelements ${html}`,
|
|
@@ -56,10 +56,7 @@ export default Route.get(
|
|
|
56
56
|
Stream.mapEffect(() =>
|
|
57
57
|
Effect.gen(function* () {
|
|
58
58
|
const spans = yield* StudioStore.allSpans(StudioStore.store.sql)
|
|
59
|
-
const html = Html.renderToString(<Traces.TraceGroups spans={spans} />).replace(
|
|
60
|
-
/\n/g,
|
|
61
|
-
"",
|
|
62
|
-
)
|
|
59
|
+
const html = Html.renderToString(<Traces.TraceGroups spans={spans} />).replace(/\n/g, "")
|
|
63
60
|
return {
|
|
64
61
|
event: "datastar-patch-elements",
|
|
65
62
|
data: `selector #traces-container\nmode inner\nelements ${html}`,
|