effect-start 0.9.0 → 0.11.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 +15 -14
- package/src/BundleHttp.test.ts +1 -1
- package/src/Commander.test.ts +15 -15
- package/src/Commander.ts +58 -88
- package/src/EncryptedCookies.test.ts +4 -4
- package/src/FileHttpRouter.test.ts +85 -16
- package/src/FileHttpRouter.ts +119 -32
- package/src/FileRouter.ts +62 -166
- package/src/FileRouterCodegen.test.ts +252 -66
- package/src/FileRouterCodegen.ts +13 -56
- package/src/FileRouterPattern.test.ts +116 -0
- package/src/FileRouterPattern.ts +59 -0
- package/src/FileRouter_path.test.ts +63 -102
- package/src/FileSystemExtra.test.ts +226 -0
- package/src/FileSystemExtra.ts +24 -60
- package/src/HttpAppExtra.test.ts +84 -0
- package/src/HttpAppExtra.ts +399 -47
- package/src/HttpUtils.test.ts +68 -0
- package/src/HttpUtils.ts +15 -0
- package/src/HyperHtml.ts +24 -5
- package/src/JsModule.test.ts +1 -1
- package/src/NodeFileSystem.ts +764 -0
- package/src/Random.ts +59 -0
- package/src/Route.test.ts +515 -18
- package/src/Route.ts +321 -166
- package/src/RouteRender.ts +40 -0
- package/src/Router.test.ts +416 -0
- package/src/Router.ts +288 -31
- package/src/RouterPattern.test.ts +655 -0
- package/src/RouterPattern.ts +416 -0
- package/src/Start.ts +14 -52
- package/src/TestHttpClient.test.ts +29 -0
- package/src/TestHttpClient.ts +122 -73
- package/src/assets.d.ts +39 -0
- package/src/bun/BunBundle.test.ts +0 -3
- package/src/bun/BunHttpServer.test.ts +74 -0
- package/src/bun/BunHttpServer.ts +259 -0
- package/src/bun/BunHttpServer_web.ts +384 -0
- package/src/bun/BunRoute.test.ts +514 -0
- package/src/bun/BunRoute.ts +427 -0
- package/src/bun/BunRoute_bundles.test.ts +218 -0
- package/src/bun/BunRuntime.ts +33 -0
- package/src/bun/BunTailwindPlugin.test.ts +1 -1
- package/src/bun/_empty.html +1 -0
- package/src/bun/index.ts +2 -1
- package/src/index.ts +14 -14
- package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
- package/src/middlewares/BasicAuthMiddleware.ts +36 -0
- package/src/testing.ts +12 -3
- package/src/Datastar.test.ts +0 -267
- package/src/Datastar.ts +0 -68
- package/src/bun/BunFullstackServer.ts +0 -45
- package/src/bun/BunFullstackServer_httpServer.ts +0 -541
- package/src/jsx-datastar.d.ts +0 -63
package/src/FileHttpRouter.ts
CHANGED
|
@@ -3,9 +3,14 @@ import * as HttpApp from "@effect/platform/HttpApp"
|
|
|
3
3
|
import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
|
|
4
4
|
import * as HttpRouter from "@effect/platform/HttpRouter"
|
|
5
5
|
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
6
|
+
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
6
7
|
import * as Effect from "effect/Effect"
|
|
7
8
|
import * as Function from "effect/Function"
|
|
9
|
+
import * as HttpUtils from "./HttpUtils.ts"
|
|
10
|
+
import * as Route from "./Route.ts"
|
|
8
11
|
import * as Router from "./Router.ts"
|
|
12
|
+
import * as RouteRender from "./RouteRender.ts"
|
|
13
|
+
import * as RouterPattern from "./RouterPattern.ts"
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* Combines Effect error channel from a record of effects.
|
|
@@ -54,52 +59,133 @@ export type HttpRouterFromServerRoutes<
|
|
|
54
59
|
>
|
|
55
60
|
|
|
56
61
|
/**
|
|
57
|
-
*
|
|
58
|
-
* Examples:
|
|
59
|
-
* /movies/[id] -> /movies/:id
|
|
60
|
-
* /docs/[[...slug]] -> /docs/*
|
|
61
|
-
* /api/[...path] -> /api/*
|
|
62
|
+
* Find layer routes that match a given route's method and media type.
|
|
62
63
|
*/
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
64
|
+
function findMatchingLayerRoutes(
|
|
65
|
+
route: Route.Route.Default,
|
|
66
|
+
layers: Route.RouteLayer[],
|
|
67
|
+
): Route.Route.Default[] {
|
|
68
|
+
const matchingRoutes: Route.Route.Default[] = []
|
|
69
|
+
|
|
70
|
+
for (const layer of layers) {
|
|
71
|
+
for (const layerRoute of layer.set) {
|
|
72
|
+
if (Route.matches(layerRoute, route)) {
|
|
73
|
+
matchingRoutes.push(layerRoute)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return matchingRoutes
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wrap an inner route with a layer route.
|
|
83
|
+
* Returns a new route that, when executed, provides next() to call the inner route.
|
|
84
|
+
*/
|
|
85
|
+
function wrapWithLayerRoute(
|
|
86
|
+
innerRoute: Route.Route.Default,
|
|
87
|
+
layerRoute: Route.Route.Default,
|
|
88
|
+
): Route.Route.Default {
|
|
89
|
+
const handler: Route.RouteHandler = (context) => {
|
|
90
|
+
const innerNext = () => innerRoute.handler(context)
|
|
91
|
+
|
|
92
|
+
const contextWithNext: Route.RouteContext = {
|
|
93
|
+
...context,
|
|
94
|
+
next: innerNext,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return layerRoute.handler(contextWithNext)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Route.make({
|
|
101
|
+
method: layerRoute.method,
|
|
102
|
+
media: layerRoute.media,
|
|
103
|
+
handler,
|
|
104
|
+
schemas: {},
|
|
105
|
+
})
|
|
71
106
|
}
|
|
72
107
|
|
|
73
108
|
/**
|
|
74
109
|
* Makes a HttpRouter from file-based routes.
|
|
75
110
|
*/
|
|
76
|
-
|
|
111
|
+
|
|
112
|
+
export function make<
|
|
113
|
+
Routes extends ReadonlyArray<Router.ServerRoute>,
|
|
114
|
+
>(
|
|
77
115
|
routes: Routes,
|
|
78
116
|
): Effect.Effect<HttpRouterFromServerRoutes<Routes>> {
|
|
79
117
|
return Effect.gen(function*() {
|
|
80
|
-
const
|
|
118
|
+
const routesWithModules = yield* Effect.forEach(
|
|
81
119
|
routes,
|
|
82
120
|
(route) =>
|
|
83
|
-
|
|
84
|
-
Effect.tryPromise(() => route.load())
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
121
|
+
Effect.gen(function*() {
|
|
122
|
+
const module = yield* Effect.tryPromise(() => route.load()).pipe(
|
|
123
|
+
Effect.orDie,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const layerModules = route.layers
|
|
127
|
+
? yield* Effect.forEach(
|
|
128
|
+
route.layers,
|
|
129
|
+
(layerLoad) =>
|
|
130
|
+
Effect.tryPromise(() => layerLoad()).pipe(Effect.orDie),
|
|
131
|
+
)
|
|
132
|
+
: []
|
|
133
|
+
|
|
134
|
+
const layers = layerModules
|
|
135
|
+
.map((mod: any) => mod.default)
|
|
136
|
+
.filter(Route.isRouteLayer)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
path: route.path,
|
|
140
|
+
routeSet: module.default,
|
|
141
|
+
layers,
|
|
142
|
+
}
|
|
143
|
+
}),
|
|
88
144
|
)
|
|
89
145
|
|
|
90
146
|
let router: HttpRouter.HttpRouter<any, any> = HttpRouter.empty
|
|
91
147
|
|
|
92
|
-
for (const { path,
|
|
93
|
-
const routeSet = module.default
|
|
94
|
-
const httpRouterPath = convertPathFormat(path)
|
|
95
|
-
|
|
148
|
+
for (const { path, routeSet, layers } of routesWithModules) {
|
|
96
149
|
for (const route of routeSet.set) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
150
|
+
const matchingLayerRoutes = findMatchingLayerRoutes(route, layers)
|
|
151
|
+
|
|
152
|
+
let wrappedRoute = route
|
|
153
|
+
// Reverse so first layer in array becomes outermost wrapper.
|
|
154
|
+
// Example: [outerLayer, innerLayer] wraps as outer(inner(route))
|
|
155
|
+
for (const layerRoute of matchingLayerRoutes.reverse()) {
|
|
156
|
+
wrappedRoute = wrapWithLayerRoute(wrappedRoute, layerRoute)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const wrappedHandler: HttpApp.Default = Effect.gen(function*() {
|
|
160
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
161
|
+
|
|
162
|
+
const context: Route.RouteContext = {
|
|
163
|
+
request,
|
|
164
|
+
get url() {
|
|
165
|
+
return HttpUtils.makeUrlFromRequest(request)
|
|
166
|
+
},
|
|
167
|
+
slots: {},
|
|
168
|
+
next: () => Effect.void,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return yield* RouteRender.render(wrappedRoute, context)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const allMiddleware = layers
|
|
175
|
+
.map((layer) => layer.httpMiddleware)
|
|
176
|
+
.filter((m): m is Route.HttpMiddlewareFunction => m !== undefined)
|
|
177
|
+
|
|
178
|
+
let finalHandler = wrappedHandler
|
|
179
|
+
for (const middleware of allMiddleware) {
|
|
180
|
+
finalHandler = middleware(finalHandler)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const pattern of RouterPattern.toEffect(path)) {
|
|
184
|
+
router = HttpRouter.route(route.method)(
|
|
185
|
+
pattern,
|
|
186
|
+
finalHandler as any,
|
|
187
|
+
)(router)
|
|
188
|
+
}
|
|
103
189
|
}
|
|
104
190
|
}
|
|
105
191
|
|
|
@@ -111,11 +197,12 @@ export function middleware() {
|
|
|
111
197
|
return HttpMiddleware.make((app) =>
|
|
112
198
|
Effect.gen(function*() {
|
|
113
199
|
const routerContext = yield* Router.Router
|
|
114
|
-
const router =
|
|
200
|
+
const router = yield* make(
|
|
201
|
+
routerContext.routes as ReadonlyArray<Router.LazyRoute>,
|
|
202
|
+
)
|
|
115
203
|
const res = yield* router.pipe(
|
|
116
204
|
Effect.catchTag("RouteNotFound", () => app),
|
|
117
205
|
)
|
|
118
|
-
|
|
119
206
|
return res
|
|
120
207
|
})
|
|
121
208
|
)
|
package/src/FileRouter.ts
CHANGED
|
@@ -9,64 +9,25 @@ import * as Stream from "effect/Stream"
|
|
|
9
9
|
import * as NPath from "node:path"
|
|
10
10
|
import * as NUrl from "node:url"
|
|
11
11
|
import * as FileRouterCodegen from "./FileRouterCodegen.ts"
|
|
12
|
+
import * as FileRouterPattern from "./FileRouterPattern.ts"
|
|
12
13
|
import * as FileSystemExtra from "./FileSystemExtra.ts"
|
|
13
|
-
import
|
|
14
|
+
import * as Router from "./Router.ts"
|
|
14
15
|
|
|
15
|
-
type
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
type GroupSegment = {
|
|
20
|
-
group: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type ParamSegment = {
|
|
24
|
-
param: string
|
|
25
|
-
optional?: true
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type RestSegment = {
|
|
29
|
-
rest: string
|
|
30
|
-
optional?: true
|
|
31
|
-
}
|
|
16
|
+
export type GroupSegment<Name extends string = string> =
|
|
17
|
+
FileRouterPattern.GroupSegment<Name>
|
|
32
18
|
|
|
33
|
-
type
|
|
34
|
-
handle: "route" | "layer"
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type Extension = "tsx" | "jsx" | "ts" | "js"
|
|
38
|
-
|
|
39
|
-
export type Segment =
|
|
40
|
-
| LiteralSegment
|
|
41
|
-
| GroupSegment
|
|
42
|
-
| ParamSegment
|
|
43
|
-
| RestSegment
|
|
44
|
-
| HandleSegment
|
|
45
|
-
|
|
46
|
-
export function isSegmentEqual(a: Segment, b: Segment): boolean {
|
|
47
|
-
if ("literal" in a && "literal" in b) return a.literal === b.literal
|
|
48
|
-
if ("group" in a && "group" in b) return a.group === b.group
|
|
49
|
-
if ("param" in a && "param" in b) return a.param === b.param
|
|
50
|
-
if ("rest" in a && "rest" in b) return a.rest === b.rest
|
|
51
|
-
if ("handle" in a && "handle" in b) return a.handle === b.handle
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type RouteModule = {
|
|
56
|
-
path: `/${string}`
|
|
57
|
-
segments: readonly Segment[]
|
|
58
|
-
load: () => Promise<ServerModule>
|
|
59
|
-
layers?: ReadonlyArray<() => Promise<unknown>>
|
|
60
|
-
}
|
|
19
|
+
export type Segment = FileRouterPattern.Segment
|
|
61
20
|
|
|
62
21
|
export type RouteManifest = {
|
|
63
|
-
|
|
22
|
+
routes: readonly Router.LazyRoute[]
|
|
64
23
|
}
|
|
65
24
|
|
|
66
25
|
export type RouteHandle = {
|
|
67
26
|
handle: "route" | "layer"
|
|
68
|
-
|
|
69
|
-
|
|
27
|
+
// eg. `about/route.tsx`, `users/[userId]/route.tsx`, `(admin)/users/route.tsx`
|
|
28
|
+
modulePath: string
|
|
29
|
+
// eg. `/about`, `/users/[userId]`, `/users` (groups stripped)
|
|
30
|
+
routePath: `/${string}`
|
|
70
31
|
segments: Segment[]
|
|
71
32
|
}
|
|
72
33
|
|
|
@@ -80,113 +41,31 @@ export type RouteHandle = {
|
|
|
80
41
|
*/
|
|
81
42
|
export type OrderedRouteHandles = RouteHandle[]
|
|
82
43
|
|
|
83
|
-
const ROUTE_PATH_REGEX = /^\/?(.*\/?)(
|
|
84
|
-
|
|
85
|
-
type RoutePathMatch = [
|
|
86
|
-
path: string,
|
|
87
|
-
kind: string,
|
|
88
|
-
kind: string,
|
|
89
|
-
ext: string,
|
|
90
|
-
]
|
|
91
|
-
|
|
92
|
-
export function segmentPath(path: string): Segment[] {
|
|
93
|
-
const trimmedPath = path.replace(/(^\/)|(\/$)/g, "") // trim leading/trailing slashes
|
|
94
|
-
|
|
95
|
-
if (trimmedPath === "") {
|
|
96
|
-
return [] // Handles "" and "/"
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const segmentStrings = trimmedPath
|
|
100
|
-
.split("/")
|
|
101
|
-
.filter(s => s !== "") // Remove empty segments from multiple slashes, e.g. "foo//bar"
|
|
102
|
-
|
|
103
|
-
if (segmentStrings.length === 0) {
|
|
104
|
-
return []
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const segments: (Segment | null)[] = segmentStrings.map(
|
|
108
|
-
(s): Segment | null => {
|
|
109
|
-
// Check if it's a handle (route.ts, layer.tsx, etc.)
|
|
110
|
-
const [, handle] = s.match(/^(route|layer)\.(tsx?|jsx?)$/)
|
|
111
|
-
?? []
|
|
112
|
-
|
|
113
|
-
if (handle) {
|
|
114
|
-
// @ts-expect-error regexp group ain't typed
|
|
115
|
-
return { handle }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// (group) - Groups
|
|
119
|
-
const groupMatch = s.match(/^\((\w+)\)$/)
|
|
120
|
-
if (groupMatch) {
|
|
121
|
-
return { group: groupMatch[1] }
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// [[...rest]] - Optional rest parameter
|
|
125
|
-
const optionalRestMatch = s.match(/^\[\[\.\.\.(\w+)\]\]$/)
|
|
126
|
-
if (optionalRestMatch) {
|
|
127
|
-
return {
|
|
128
|
-
rest: optionalRestMatch[1],
|
|
129
|
-
optional: true,
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// [...rest] - Required rest parameter
|
|
134
|
-
const requiredRestMatch = s.match(/^\[\.\.\.(\w+)\]$/)
|
|
135
|
-
if (requiredRestMatch) {
|
|
136
|
-
return { rest: requiredRestMatch[1] }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// [param] - Dynamic parameter
|
|
140
|
-
const paramMatch = s.match(/^\[(\w+)\]$/)
|
|
141
|
-
if (paramMatch) {
|
|
142
|
-
return { param: paramMatch[1] }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Literal segment
|
|
146
|
-
if (/^[A-Za-z0-9._~-]+$/.test(s)) {
|
|
147
|
-
return { literal: s }
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return null
|
|
151
|
-
},
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
if (segments.some((seg) => seg === null)) {
|
|
155
|
-
throw new Error(
|
|
156
|
-
`Invalid path segment in "${path}": contains invalid characters or format`,
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return segments as Segment[]
|
|
161
|
-
}
|
|
44
|
+
const ROUTE_PATH_REGEX = /^\/?(.*\/?)(?:route|layer)\.(jsx?|tsx?)$/
|
|
162
45
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if ("param" in seg) return `[${seg.param}]`
|
|
167
|
-
if ("rest" in seg) {
|
|
168
|
-
return seg.optional ? `[[...${seg.rest}]]` : `[...${seg.rest}]`
|
|
169
|
-
}
|
|
170
|
-
if ("handle" in seg) return seg.handle
|
|
171
|
-
return ""
|
|
172
|
-
}
|
|
46
|
+
export const parse = FileRouterPattern.parse
|
|
47
|
+
export const formatSegment = FileRouterPattern.formatSegment
|
|
48
|
+
export const format = FileRouterPattern.format
|
|
173
49
|
|
|
174
50
|
export function parseRoute(
|
|
175
51
|
path: string,
|
|
176
52
|
): RouteHandle {
|
|
177
|
-
const segs =
|
|
53
|
+
const segs = parse(path)
|
|
178
54
|
|
|
179
|
-
const
|
|
55
|
+
const lastSeg = segs.at(-1)
|
|
56
|
+
const handleMatch = lastSeg?._tag === "LiteralSegment"
|
|
57
|
+
&& lastSeg.value.match(/^(route|layer)\.(tsx?|jsx?)$/)
|
|
58
|
+
const handle = handleMatch ? handleMatch[1] as "route" | "layer" : null
|
|
180
59
|
|
|
181
|
-
if (!handle
|
|
60
|
+
if (!handle) {
|
|
182
61
|
throw new Error(
|
|
183
62
|
`Invalid route path "${path}": must end with a valid handle (route or layer)`,
|
|
184
63
|
)
|
|
185
64
|
}
|
|
186
65
|
|
|
187
|
-
//
|
|
66
|
+
// rest segments must be the last segment before the handle
|
|
188
67
|
const pathSegments = segs.slice(0, -1) // All segments except the handle
|
|
189
|
-
const restIndex = pathSegments.findIndex(seg => "
|
|
68
|
+
const restIndex = pathSegments.findIndex(seg => seg._tag === "RestSegment")
|
|
190
69
|
|
|
191
70
|
if (restIndex !== -1) {
|
|
192
71
|
// If there's a rest, it must be the last path segment
|
|
@@ -196,19 +75,27 @@ export function parseRoute(
|
|
|
196
75
|
)
|
|
197
76
|
}
|
|
198
77
|
|
|
199
|
-
//
|
|
78
|
+
// all segments before the rest must be literal, param, or group
|
|
200
79
|
for (let i = 0; i < restIndex; i++) {
|
|
201
80
|
const seg = pathSegments[i]
|
|
202
|
-
if (
|
|
81
|
+
if (
|
|
82
|
+
seg._tag !== "LiteralSegment"
|
|
83
|
+
&& seg._tag !== "ParamSegment"
|
|
84
|
+
&& seg._tag !== "GroupSegment"
|
|
85
|
+
) {
|
|
203
86
|
throw new Error(
|
|
204
87
|
`Invalid route path "${path}": segments before rest must be literal, param, or group segments`,
|
|
205
88
|
)
|
|
206
89
|
}
|
|
207
90
|
}
|
|
208
91
|
} else {
|
|
209
|
-
// No rest:
|
|
92
|
+
// No rest: all path segments are literal, param, or group
|
|
210
93
|
for (const seg of pathSegments) {
|
|
211
|
-
if (
|
|
94
|
+
if (
|
|
95
|
+
seg._tag !== "LiteralSegment"
|
|
96
|
+
&& seg._tag !== "ParamSegment"
|
|
97
|
+
&& seg._tag !== "GroupSegment"
|
|
98
|
+
) {
|
|
212
99
|
throw new Error(
|
|
213
100
|
`Invalid route path "${path}": path segments must be literal, param, or group segments`,
|
|
214
101
|
)
|
|
@@ -216,37 +103,33 @@ export function parseRoute(
|
|
|
216
103
|
}
|
|
217
104
|
}
|
|
218
105
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.map(segmentToText)
|
|
224
|
-
|
|
225
|
-
const routePath = (routePathSegments.length > 0
|
|
226
|
-
? `/${routePathSegments.join("/")}`
|
|
227
|
-
: "/") as `/${string}`
|
|
106
|
+
const routePathSegments = pathSegments.filter(
|
|
107
|
+
seg => seg._tag !== "GroupSegment",
|
|
108
|
+
)
|
|
109
|
+
const routePath = FileRouterPattern.format(routePathSegments)
|
|
228
110
|
|
|
229
111
|
return {
|
|
230
|
-
handle
|
|
112
|
+
handle,
|
|
231
113
|
modulePath: path,
|
|
232
114
|
routePath,
|
|
233
|
-
segments:
|
|
115
|
+
segments: pathSegments,
|
|
234
116
|
}
|
|
235
117
|
}
|
|
236
118
|
|
|
237
119
|
/**
|
|
238
|
-
* Generates a file that references all routes.
|
|
120
|
+
* Generates a manifest file that references all routes.
|
|
239
121
|
*/
|
|
240
|
-
export function
|
|
122
|
+
export function layerManifest(options: {
|
|
241
123
|
load: () => Promise<unknown>
|
|
242
124
|
path: string
|
|
243
125
|
}) {
|
|
244
126
|
let manifestPath = options.path
|
|
245
|
-
|
|
246
|
-
// handle use of import.meta.resolve
|
|
247
127
|
if (manifestPath.startsWith("file://")) {
|
|
248
128
|
manifestPath = NUrl.fileURLToPath(manifestPath)
|
|
249
129
|
}
|
|
130
|
+
if (NPath.extname(manifestPath) === "") {
|
|
131
|
+
manifestPath = NPath.join(manifestPath, "index.ts")
|
|
132
|
+
}
|
|
250
133
|
|
|
251
134
|
const routesPath = NPath.dirname(manifestPath)
|
|
252
135
|
const manifestFilename = NPath.basename(manifestPath)
|
|
@@ -277,6 +160,19 @@ export function layer(options: {
|
|
|
277
160
|
)
|
|
278
161
|
}
|
|
279
162
|
|
|
163
|
+
export function layer(options: {
|
|
164
|
+
load: () => Promise<Router.RouterManifest>
|
|
165
|
+
path: string
|
|
166
|
+
}) {
|
|
167
|
+
return Layer.provide(
|
|
168
|
+
Layer.effect(
|
|
169
|
+
Router.Router,
|
|
170
|
+
Effect.promise(() => options.load()),
|
|
171
|
+
),
|
|
172
|
+
layerManifest(options),
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
280
176
|
export function walkRoutesDirectory(
|
|
281
177
|
dir: string,
|
|
282
178
|
): Effect.Effect<
|
|
@@ -299,10 +195,10 @@ export function getRouteHandlesFromPaths(
|
|
|
299
195
|
paths: string[],
|
|
300
196
|
): OrderedRouteHandles {
|
|
301
197
|
const handles = paths
|
|
302
|
-
.map(f => f.match(ROUTE_PATH_REGEX)
|
|
198
|
+
.map(f => f.match(ROUTE_PATH_REGEX))
|
|
303
199
|
.filter(Boolean)
|
|
304
200
|
.map(v => {
|
|
305
|
-
const path = v[0]
|
|
201
|
+
const path = v![0]
|
|
306
202
|
try {
|
|
307
203
|
return parseRoute(path)
|
|
308
204
|
} catch {
|
|
@@ -313,8 +209,8 @@ export function getRouteHandlesFromPaths(
|
|
|
313
209
|
.toSorted((a, b) => {
|
|
314
210
|
const aDepth = a.segments.length
|
|
315
211
|
const bDepth = b.segments.length
|
|
316
|
-
const aHasRest = a.segments.some(seg => "
|
|
317
|
-
const bHasRest = b.segments.some(seg => "
|
|
212
|
+
const aHasRest = a.segments.some(seg => seg._tag === "RestSegment")
|
|
213
|
+
const bHasRest = b.segments.some(seg => seg._tag === "RestSegment")
|
|
318
214
|
|
|
319
215
|
return (
|
|
320
216
|
// rest is a dominant factor (routes with rest come last)
|