effect-start 0.9.0 → 0.10.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 +12 -13
- 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 +81 -12
- package/src/FileHttpRouter.ts +115 -26
- package/src/FileRouter.ts +60 -162
- package/src/FileRouterCodegen.test.ts +250 -64
- 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/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 +471 -0
- package/src/Route.ts +298 -153
- package/src/RouteRender.ts +38 -0
- package/src/Router.ts +11 -33
- package/src/RouterPattern.test.ts +629 -0
- package/src/RouterPattern.ts +391 -0
- package/src/Start.ts +14 -52
- package/src/bun/BunBundle.test.ts +0 -3
- package/src/bun/BunHttpServer.ts +246 -0
- package/src/bun/BunHttpServer_web.ts +384 -0
- package/src/bun/BunRoute.test.ts +341 -0
- package/src/bun/BunRoute.ts +326 -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/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,49 +59,132 @@ 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
|
|
94
|
-
const httpRouterPath = convertPathFormat(path)
|
|
148
|
+
for (const { path, routeSet, layers } of routesWithModules) {
|
|
149
|
+
const httpRouterPath = RouterPattern.toHttpPath(path)
|
|
95
150
|
|
|
96
151
|
for (const route of routeSet.set) {
|
|
152
|
+
const matchingLayerRoutes = findMatchingLayerRoutes(route, layers)
|
|
153
|
+
|
|
154
|
+
let wrappedRoute = route
|
|
155
|
+
// Reverse so first layer in array becomes outermost wrapper.
|
|
156
|
+
// Example: [outerLayer, innerLayer] wraps as outer(inner(route))
|
|
157
|
+
for (const layerRoute of matchingLayerRoutes.reverse()) {
|
|
158
|
+
wrappedRoute = wrapWithLayerRoute(wrappedRoute, layerRoute)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const wrappedHandler: HttpApp.Default = Effect.gen(function*() {
|
|
162
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
163
|
+
|
|
164
|
+
const context: Route.RouteContext = {
|
|
165
|
+
request,
|
|
166
|
+
get url() {
|
|
167
|
+
return HttpUtils.makeUrlFromRequest(request)
|
|
168
|
+
},
|
|
169
|
+
slots: {},
|
|
170
|
+
next: () => Effect.void,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return yield* RouteRender.render(wrappedRoute, context)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const allMiddleware = layers
|
|
177
|
+
.map((layer) => layer.httpMiddleware)
|
|
178
|
+
.filter((m): m is Route.HttpMiddlewareFunction => m !== undefined)
|
|
179
|
+
|
|
180
|
+
let finalHandler = wrappedHandler
|
|
181
|
+
for (const middleware of allMiddleware) {
|
|
182
|
+
finalHandler = middleware(finalHandler)
|
|
183
|
+
}
|
|
184
|
+
|
|
97
185
|
router = HttpRouter.route(route.method)(
|
|
98
186
|
httpRouterPath,
|
|
99
|
-
|
|
187
|
+
finalHandler as any,
|
|
100
188
|
)(
|
|
101
189
|
router,
|
|
102
190
|
)
|
|
@@ -111,11 +199,12 @@ export function middleware() {
|
|
|
111
199
|
return HttpMiddleware.make((app) =>
|
|
112
200
|
Effect.gen(function*() {
|
|
113
201
|
const routerContext = yield* Router.Router
|
|
114
|
-
const router =
|
|
202
|
+
const router = yield* make(
|
|
203
|
+
routerContext.routes as ReadonlyArray<Router.LazyRoute>,
|
|
204
|
+
)
|
|
115
205
|
const res = yield* router.pipe(
|
|
116
206
|
Effect.catchTag("RouteNotFound", () => app),
|
|
117
207
|
)
|
|
118
|
-
|
|
119
208
|
return res
|
|
120
209
|
})
|
|
121
210
|
)
|
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,105 +41,23 @@ 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
|
)
|
|
@@ -186,7 +65,7 @@ export function parseRoute(
|
|
|
186
65
|
|
|
187
66
|
// Validate Route constraints: 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
|
|
@@ -199,7 +78,11 @@ export function parseRoute(
|
|
|
199
78
|
// Validate that all segments before the rest are 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
|
)
|
|
@@ -208,7 +91,11 @@ export function parseRoute(
|
|
|
208
91
|
} else {
|
|
209
92
|
// No rest: validate that 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,35 @@ export function parseRoute(
|
|
|
216
103
|
}
|
|
217
104
|
}
|
|
218
105
|
|
|
219
|
-
// Construct routePath from path segments (excluding
|
|
106
|
+
// Construct routePath from path segments (excluding groups)
|
|
220
107
|
// Groups like (admin) are stripped from the URL path
|
|
221
|
-
const routePathSegments = pathSegments
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const routePath = (routePathSegments.length > 0
|
|
226
|
-
? `/${routePathSegments.join("/")}`
|
|
227
|
-
: "/") as `/${string}`
|
|
108
|
+
const routePathSegments = pathSegments.filter(
|
|
109
|
+
seg => seg._tag !== "GroupSegment",
|
|
110
|
+
)
|
|
111
|
+
const routePath = FileRouterPattern.format(routePathSegments)
|
|
228
112
|
|
|
229
113
|
return {
|
|
230
|
-
handle
|
|
114
|
+
handle,
|
|
231
115
|
modulePath: path,
|
|
232
116
|
routePath,
|
|
233
|
-
segments:
|
|
117
|
+
segments: pathSegments,
|
|
234
118
|
}
|
|
235
119
|
}
|
|
236
120
|
|
|
237
121
|
/**
|
|
238
|
-
* Generates a file that references all routes.
|
|
122
|
+
* Generates a manifest file that references all routes.
|
|
239
123
|
*/
|
|
240
|
-
export function
|
|
124
|
+
export function layerManifest(options: {
|
|
241
125
|
load: () => Promise<unknown>
|
|
242
126
|
path: string
|
|
243
127
|
}) {
|
|
244
128
|
let manifestPath = options.path
|
|
245
|
-
|
|
246
|
-
// handle use of import.meta.resolve
|
|
247
129
|
if (manifestPath.startsWith("file://")) {
|
|
248
130
|
manifestPath = NUrl.fileURLToPath(manifestPath)
|
|
249
131
|
}
|
|
132
|
+
if (NPath.extname(manifestPath) === "") {
|
|
133
|
+
manifestPath = NPath.join(manifestPath, "index.ts")
|
|
134
|
+
}
|
|
250
135
|
|
|
251
136
|
const routesPath = NPath.dirname(manifestPath)
|
|
252
137
|
const manifestFilename = NPath.basename(manifestPath)
|
|
@@ -277,6 +162,19 @@ export function layer(options: {
|
|
|
277
162
|
)
|
|
278
163
|
}
|
|
279
164
|
|
|
165
|
+
export function layer(options: {
|
|
166
|
+
load: () => Promise<Router.RouterManifest>
|
|
167
|
+
path: string
|
|
168
|
+
}) {
|
|
169
|
+
return Layer.mergeAll(
|
|
170
|
+
Layer.effect(
|
|
171
|
+
Router.Router,
|
|
172
|
+
Effect.promise(() => options.load()),
|
|
173
|
+
),
|
|
174
|
+
layerManifest(options),
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
280
178
|
export function walkRoutesDirectory(
|
|
281
179
|
dir: string,
|
|
282
180
|
): Effect.Effect<
|
|
@@ -299,10 +197,10 @@ export function getRouteHandlesFromPaths(
|
|
|
299
197
|
paths: string[],
|
|
300
198
|
): OrderedRouteHandles {
|
|
301
199
|
const handles = paths
|
|
302
|
-
.map(f => f.match(ROUTE_PATH_REGEX)
|
|
200
|
+
.map(f => f.match(ROUTE_PATH_REGEX))
|
|
303
201
|
.filter(Boolean)
|
|
304
202
|
.map(v => {
|
|
305
|
-
const path = v[0]
|
|
203
|
+
const path = v![0]
|
|
306
204
|
try {
|
|
307
205
|
return parseRoute(path)
|
|
308
206
|
} catch {
|
|
@@ -313,8 +211,8 @@ export function getRouteHandlesFromPaths(
|
|
|
313
211
|
.toSorted((a, b) => {
|
|
314
212
|
const aDepth = a.segments.length
|
|
315
213
|
const bDepth = b.segments.length
|
|
316
|
-
const aHasRest = a.segments.some(seg => "
|
|
317
|
-
const bHasRest = b.segments.some(seg => "
|
|
214
|
+
const aHasRest = a.segments.some(seg => seg._tag === "RestSegment")
|
|
215
|
+
const bHasRest = b.segments.some(seg => seg._tag === "RestSegment")
|
|
318
216
|
|
|
319
217
|
return (
|
|
320
218
|
// rest is a dominant factor (routes with rest come last)
|