effect-start 0.19.0 → 0.20.1
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/README.md +3 -3
- package/dist/Development.d.ts +3 -3
- package/dist/Development.js +3 -2
- package/dist/Effectify.d.ts +212 -0
- package/dist/Effectify.js +19 -0
- package/dist/FilePathPattern.d.ts +29 -0
- package/dist/FilePathPattern.js +86 -0
- package/dist/FileRouter.d.ts +39 -41
- package/dist/FileRouter.js +104 -158
- package/dist/FileRouterCodegen.d.ts +7 -8
- package/dist/FileRouterCodegen.js +97 -66
- package/dist/PlatformError.d.ts +46 -0
- package/dist/PlatformError.js +43 -0
- package/dist/PlatformRuntime.d.ts +23 -0
- package/dist/PlatformRuntime.js +42 -0
- package/dist/RouteBody.d.ts +1 -1
- package/dist/Start.d.ts +34 -3
- package/dist/Start.js +31 -6
- package/dist/bun/BunPlatformHttpServer.d.ts +10 -0
- package/dist/bun/BunPlatformHttpServer.js +53 -0
- package/dist/bun/BunRoute.d.ts +3 -5
- package/dist/bun/BunRoute.js +9 -17
- package/dist/bun/BunRuntime.d.ts +2 -1
- package/dist/bun/BunRuntime.js +10 -5
- package/dist/bun/BunServer.d.ts +33 -0
- package/dist/bun/BunServer.js +133 -0
- package/dist/bun/BunServerRequest.d.ts +60 -0
- package/dist/bun/BunServerRequest.js +252 -0
- package/dist/bun/index.d.ts +1 -1
- package/dist/bun/index.js +1 -1
- package/dist/datastar/actions/fetch.d.ts +30 -0
- package/dist/datastar/actions/fetch.js +411 -0
- package/dist/datastar/actions/peek.d.ts +1 -0
- package/dist/datastar/actions/peek.js +14 -0
- package/dist/datastar/actions/setAll.d.ts +1 -0
- package/dist/datastar/actions/setAll.js +13 -0
- package/dist/datastar/actions/toggleAll.d.ts +1 -0
- package/dist/datastar/actions/toggleAll.js +13 -0
- package/dist/datastar/attributes/attr.d.ts +1 -0
- package/dist/datastar/attributes/attr.js +49 -0
- package/dist/datastar/attributes/bind.d.ts +1 -0
- package/dist/datastar/attributes/bind.js +183 -0
- package/dist/datastar/attributes/class.d.ts +1 -0
- package/dist/datastar/attributes/class.js +50 -0
- package/dist/datastar/attributes/computed.d.ts +1 -0
- package/dist/datastar/attributes/computed.js +27 -0
- package/dist/datastar/attributes/effect.d.ts +1 -0
- package/dist/datastar/attributes/effect.js +10 -0
- package/dist/datastar/attributes/indicator.d.ts +1 -0
- package/dist/datastar/attributes/indicator.js +32 -0
- package/dist/datastar/attributes/init.d.ts +1 -0
- package/dist/datastar/attributes/init.js +27 -0
- package/dist/datastar/attributes/jsonSignals.d.ts +1 -0
- package/dist/datastar/attributes/jsonSignals.js +31 -0
- package/dist/datastar/attributes/on.d.ts +1 -0
- package/dist/datastar/attributes/on.js +59 -0
- package/dist/datastar/attributes/onIntersect.d.ts +1 -0
- package/dist/datastar/attributes/onIntersect.js +54 -0
- package/dist/datastar/attributes/onInterval.d.ts +1 -0
- package/dist/datastar/attributes/onInterval.js +31 -0
- package/dist/datastar/attributes/onSignalPatch.d.ts +1 -0
- package/dist/datastar/attributes/onSignalPatch.js +44 -0
- package/dist/datastar/attributes/ref.d.ts +1 -0
- package/dist/datastar/attributes/ref.js +11 -0
- package/dist/datastar/attributes/show.d.ts +1 -0
- package/dist/datastar/attributes/show.js +32 -0
- package/dist/datastar/attributes/signals.d.ts +1 -0
- package/dist/datastar/attributes/signals.js +18 -0
- package/dist/datastar/attributes/style.d.ts +1 -0
- package/dist/datastar/attributes/style.js +56 -0
- package/dist/datastar/attributes/text.d.ts +1 -0
- package/dist/datastar/attributes/text.js +27 -0
- package/dist/datastar/engine.d.ts +156 -0
- package/dist/datastar/engine.js +971 -0
- package/dist/datastar/index.d.ts +24 -0
- package/dist/datastar/index.js +24 -0
- package/dist/datastar/load.d.ts +24 -0
- package/dist/datastar/load.js +24 -0
- package/dist/datastar/utils.d.ts +51 -0
- package/dist/datastar/utils.js +205 -0
- package/dist/datastar/watchers/patchElements.d.ts +1 -0
- package/dist/datastar/watchers/patchElements.js +420 -0
- package/dist/datastar/watchers/patchSignals.d.ts +1 -0
- package/dist/datastar/watchers/patchSignals.js +15 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node/NodeFileSystem.d.ts +7 -0
- package/dist/node/NodeFileSystem.js +420 -0
- package/dist/node/NodeUtils.d.ts +2 -0
- package/dist/node/NodeUtils.js +20 -0
- package/dist/x/tailwind/plugin.js +1 -1
- package/package.json +11 -7
- package/src/Development.ts +26 -25
- package/src/{node/Effectify.ts → Effectify.ts} +10 -3
- package/src/FilePathPattern.ts +115 -0
- package/src/FileRouter.ts +178 -255
- package/src/FileRouterCodegen.ts +135 -92
- package/src/{node/PlatformError.ts → PlatformError.ts} +34 -19
- package/src/PlatformRuntime.ts +97 -0
- package/src/RouteBody.ts +1 -1
- package/src/RouteHttp.ts +3 -1
- package/src/Start.ts +61 -14
- package/src/bun/BunPlatformHttpServer.ts +88 -0
- package/src/bun/BunRoute.ts +12 -22
- package/src/bun/BunRuntime.ts +21 -5
- package/src/bun/BunServer.ts +228 -0
- package/src/bun/index.ts +1 -1
- package/src/datastar/README.md +18 -0
- package/src/datastar/actions/fetch.ts +609 -0
- package/src/datastar/actions/peek.ts +17 -0
- package/src/datastar/actions/setAll.ts +20 -0
- package/src/datastar/actions/toggleAll.ts +20 -0
- package/src/datastar/attributes/attr.ts +50 -0
- package/src/datastar/attributes/bind.ts +220 -0
- package/src/datastar/attributes/class.ts +57 -0
- package/src/datastar/attributes/computed.ts +33 -0
- package/src/datastar/attributes/effect.ts +11 -0
- package/src/datastar/attributes/indicator.ts +39 -0
- package/src/datastar/attributes/init.ts +35 -0
- package/src/datastar/attributes/jsonSignals.ts +38 -0
- package/src/datastar/attributes/on.ts +71 -0
- package/src/datastar/attributes/onIntersect.ts +65 -0
- package/src/datastar/attributes/onInterval.ts +39 -0
- package/src/datastar/attributes/onSignalPatch.ts +63 -0
- package/src/datastar/attributes/ref.ts +12 -0
- package/src/datastar/attributes/show.ts +33 -0
- package/src/datastar/attributes/signals.ts +22 -0
- package/src/datastar/attributes/style.ts +63 -0
- package/src/datastar/attributes/text.ts +30 -0
- package/src/datastar/engine.ts +1341 -0
- package/src/datastar/index.ts +25 -0
- package/src/datastar/utils.ts +286 -0
- package/src/datastar/watchers/patchElements.ts +554 -0
- package/src/datastar/watchers/patchSignals.ts +15 -0
- package/src/index.ts +1 -1
- package/src/node/{FileSystem.ts → NodeFileSystem.ts} +2 -2
- package/src/node/{Utils.ts → NodeUtils.ts} +2 -0
- package/src/x/tailwind/plugin.ts +1 -1
- package/src/FileRouterCodegen.todo.ts +0 -1133
- package/src/FileRouterPattern.ts +0 -59
- package/src/RouterPattern.ts +0 -416
- package/src/StartApp.ts +0 -47
- package/src/bun/BunHttpServer.ts +0 -303
- /package/src/bun/{BunHttpServer_web.ts → BunServerRequest.ts} +0 -0
package/dist/FileRouter.js
CHANGED
|
@@ -1,203 +1,149 @@
|
|
|
1
1
|
import * as FileSystem from "@effect/platform/FileSystem";
|
|
2
|
-
import * as
|
|
3
|
-
import * as Context from "effect/Context";
|
|
2
|
+
import * as Data from "effect/Data";
|
|
4
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Either from "effect/Either";
|
|
5
5
|
import * as Function from "effect/Function";
|
|
6
6
|
import * as Layer from "effect/Layer";
|
|
7
|
-
import * as Record from "effect/Record";
|
|
8
7
|
import * as Stream from "effect/Stream";
|
|
9
8
|
import * as NPath from "node:path";
|
|
10
9
|
import * as NUrl from "node:url";
|
|
11
10
|
import * as Development from "./Development.js";
|
|
11
|
+
import * as FilePathPattern from "./FilePathPattern.js";
|
|
12
12
|
import * as FileRouterCodegen from "./FileRouterCodegen.js";
|
|
13
|
-
import * as
|
|
14
|
-
|
|
13
|
+
import * as NodeUtils from "./node/NodeUtils.js";
|
|
14
|
+
import * as Route from "./Route.js";
|
|
15
|
+
import * as RouteTree from "./RouteTree.js";
|
|
16
|
+
export class FileRouterError extends Data.TaggedError("FileRouterError") {
|
|
15
17
|
}
|
|
16
18
|
const ROUTE_PATH_REGEX = /^\/?(.*\/?)(?:route|layer)\.(jsx?|tsx?)$/;
|
|
17
|
-
export const parse = FileRouterPattern.parse;
|
|
18
|
-
export const formatSegment = FileRouterPattern.formatSegment;
|
|
19
|
-
export const format = FileRouterPattern.format;
|
|
20
19
|
export function parseRoute(path) {
|
|
21
|
-
const segs =
|
|
20
|
+
const segs = FilePathPattern.segments(path);
|
|
22
21
|
const lastSeg = segs.at(-1);
|
|
23
22
|
const handleMatch = lastSeg?._tag === "LiteralSegment"
|
|
24
23
|
&& lastSeg.value.match(/^(route|layer)\.(tsx?|jsx?)$/);
|
|
25
24
|
const handle = handleMatch ? handleMatch[1] : null;
|
|
26
25
|
if (!handle) {
|
|
27
|
-
|
|
26
|
+
return null;
|
|
28
27
|
}
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// If there's a rest, it must be the last path segment
|
|
34
|
-
if (restIndex !== pathSegments.length - 1) {
|
|
35
|
-
throw new Error(`Invalid route path "${path}": rest segment ([...rest] or [[...rest]]) must be the last path segment before the handle`);
|
|
36
|
-
}
|
|
37
|
-
// all segments before the rest must be literal, param, or group
|
|
38
|
-
for (let i = 0; i < restIndex; i++) {
|
|
39
|
-
const seg = pathSegments[i];
|
|
40
|
-
if (seg._tag !== "LiteralSegment"
|
|
41
|
-
&& seg._tag !== "ParamSegment"
|
|
42
|
-
&& seg._tag !== "GroupSegment") {
|
|
43
|
-
throw new Error(`Invalid route path "${path}": segments before rest must be literal, param, or group segments`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
28
|
+
const pathSegments = segs.slice(0, -1);
|
|
29
|
+
const validated = FilePathPattern.validate(FilePathPattern.format(pathSegments));
|
|
30
|
+
if (Either.isLeft(validated)) {
|
|
31
|
+
return null;
|
|
46
32
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (seg._tag !== "LiteralSegment"
|
|
51
|
-
&& seg._tag !== "ParamSegment"
|
|
52
|
-
&& seg._tag !== "GroupSegment") {
|
|
53
|
-
throw new Error(`Invalid route path "${path}": path segments must be literal, param, or group segments`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
33
|
+
const restIndex = pathSegments.findIndex((seg) => seg._tag === "RestSegment");
|
|
34
|
+
if (restIndex !== -1 && restIndex !== pathSegments.length - 1) {
|
|
35
|
+
return null;
|
|
56
36
|
}
|
|
57
|
-
const routePathSegments = pathSegments.filter(seg => seg._tag !== "GroupSegment");
|
|
58
|
-
const routePath =
|
|
37
|
+
const routePathSegments = pathSegments.filter((seg) => seg._tag !== "GroupSegment");
|
|
38
|
+
const routePath = FilePathPattern.format(routePathSegments);
|
|
59
39
|
return {
|
|
60
40
|
handle,
|
|
61
|
-
modulePath: path
|
|
41
|
+
modulePath: `/${path}`,
|
|
62
42
|
routePath,
|
|
63
43
|
segments: pathSegments,
|
|
64
44
|
};
|
|
65
45
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
46
|
+
function importModule(load) {
|
|
47
|
+
return Effect.tryPromise({
|
|
48
|
+
try: () => load(),
|
|
49
|
+
catch: (cause) => new FileRouterError({ reason: "Import", cause }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function layer(loadOrOptions) {
|
|
53
|
+
const options = typeof loadOrOptions === "function"
|
|
54
|
+
? {
|
|
55
|
+
load: loadOrOptions,
|
|
56
|
+
path: NPath.join(NodeUtils.getEntrypoint(), "routes"),
|
|
57
|
+
}
|
|
58
|
+
: loadOrOptions;
|
|
59
|
+
let treePath = options.path;
|
|
60
|
+
if (treePath.startsWith("file://")) {
|
|
61
|
+
treePath = NUrl.fileURLToPath(treePath);
|
|
73
62
|
}
|
|
74
|
-
if (NPath.extname(
|
|
75
|
-
|
|
63
|
+
if (NPath.extname(treePath) === "") {
|
|
64
|
+
treePath = NPath.join(treePath, "server.gen.ts");
|
|
76
65
|
}
|
|
77
|
-
const routesPath = NPath.dirname(
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
return Layer.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}))
|
|
66
|
+
const routesPath = NPath.dirname(treePath);
|
|
67
|
+
const treeFilename = NPath.basename(treePath);
|
|
68
|
+
const relativeRoutesPath = NPath.relative(process.cwd(), routesPath);
|
|
69
|
+
return Layer.scoped(Route.Routes, Effect.gen(function* () {
|
|
70
|
+
// Generate routes file before loading
|
|
71
|
+
yield* FileRouterCodegen.update(routesPath, treeFilename);
|
|
72
|
+
// Load and build route tree
|
|
73
|
+
const m = yield* importModule(options.load);
|
|
74
|
+
const routeTree = yield* fromFileRoutes(m.default);
|
|
75
|
+
// Watch for changes (only when Development service is available)
|
|
76
|
+
yield* Function.pipe(Development.stream(), Stream.filter(e => e._tag !== "Reload" && e.path.startsWith(relativeRoutesPath)), Stream.runForEach(() => FileRouterCodegen.update(routesPath, treeFilename)), Effect.fork);
|
|
77
|
+
return routeTree;
|
|
78
|
+
}));
|
|
90
79
|
}
|
|
91
|
-
export function
|
|
80
|
+
export function fromFileRoutes(fileRoutes) {
|
|
92
81
|
return Effect.gen(function* () {
|
|
93
82
|
const mounts = {};
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const layerRouteSet = m.default;
|
|
104
|
-
if (RouteSet.isRouteSet(layerRouteSet)) {
|
|
105
|
-
mergedRouteSet = Route.merge(layerRouteSet, mergedRouteSet);
|
|
83
|
+
for (const [path, loaders] of Object.entries(fileRoutes)) {
|
|
84
|
+
const modules = yield* Effect.forEach(loaders, (loader) => Effect.promise(() => loader()));
|
|
85
|
+
const allRoutes = [];
|
|
86
|
+
for (const m of modules) {
|
|
87
|
+
if (Route.isRouteSet(m.default)) {
|
|
88
|
+
for (const route of m.default) {
|
|
89
|
+
;
|
|
90
|
+
allRoutes.push(route);
|
|
91
|
+
}
|
|
106
92
|
}
|
|
107
93
|
}
|
|
108
|
-
mounts[
|
|
109
|
-
}
|
|
110
|
-
return
|
|
94
|
+
mounts[path] = allRoutes;
|
|
95
|
+
}
|
|
96
|
+
return RouteTree.make(mounts);
|
|
111
97
|
});
|
|
112
98
|
}
|
|
113
99
|
export function walkRoutesDirectory(dir) {
|
|
114
100
|
return Effect.gen(function* () {
|
|
115
101
|
const fs = yield* FileSystem.FileSystem;
|
|
116
102
|
const files = yield* fs.readDirectory(dir, { recursive: true });
|
|
117
|
-
return
|
|
103
|
+
return yield* getFileRoutes(files);
|
|
118
104
|
});
|
|
119
105
|
}
|
|
120
|
-
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
106
|
+
export function getFileRoutes(paths) {
|
|
107
|
+
return Effect.gen(function* () {
|
|
108
|
+
const routes = paths
|
|
109
|
+
.map(f => f.match(ROUTE_PATH_REGEX))
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.map(v => {
|
|
112
|
+
const path = v[0];
|
|
113
|
+
try {
|
|
114
|
+
return parseRoute(path);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.filter((route) => route !== null)
|
|
121
|
+
.toSorted((a, b) => {
|
|
122
|
+
const aDepth = a.segments.length;
|
|
123
|
+
const bDepth = b.segments.length;
|
|
124
|
+
const aHasRest = a.segments.some((seg) => seg._tag === "RestSegment");
|
|
125
|
+
const bHasRest = b.segments.some((seg) => seg._tag === "RestSegment");
|
|
126
|
+
return (
|
|
127
|
+
// rest is a dominant factor (routes with rest come last)
|
|
128
|
+
(+aHasRest - +bHasRest) * 1000
|
|
129
|
+
// depth is reversed for rest
|
|
130
|
+
+ (aDepth - bDepth) * (1 - 2 * +aHasRest)
|
|
131
|
+
// lexicographic comparison as tiebreaker
|
|
132
|
+
+ a.modulePath.localeCompare(b.modulePath) * 0.001);
|
|
133
|
+
});
|
|
134
|
+
// Detect conflicting routes at the same path
|
|
135
|
+
const routesByPath = new Map();
|
|
136
|
+
for (const route of routes) {
|
|
137
|
+
const existing = routesByPath.get(route.routePath) || [];
|
|
138
|
+
existing.push(route);
|
|
139
|
+
routesByPath.set(route.routePath, existing);
|
|
131
140
|
}
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
for (const [path, pathRoutes] of routesByPath) {
|
|
142
|
+
const routeHandles = pathRoutes.filter(h => h.handle === "route");
|
|
143
|
+
if (routeHandles.length > 1) {
|
|
144
|
+
yield* new FileRouterError({ reason: "Conflict", path });
|
|
145
|
+
}
|
|
134
146
|
}
|
|
135
|
-
|
|
136
|
-
.filter((route) => route !== null)
|
|
137
|
-
.toSorted((a, b) => {
|
|
138
|
-
const aDepth = a.segments.length;
|
|
139
|
-
const bDepth = b.segments.length;
|
|
140
|
-
const aHasRest = a.segments.some(seg => seg._tag === "RestSegment");
|
|
141
|
-
const bHasRest = b.segments.some(seg => seg._tag === "RestSegment");
|
|
142
|
-
return (
|
|
143
|
-
// rest is a dominant factor (routes with rest come last)
|
|
144
|
-
(+aHasRest - +bHasRest) * 1000
|
|
145
|
-
// depth is reversed for rest
|
|
146
|
-
+ (aDepth - bDepth) * (1 - 2 * +aHasRest)
|
|
147
|
-
// lexicographic comparison as tiebreaker
|
|
148
|
-
+ a.modulePath.localeCompare(b.modulePath) * 0.001);
|
|
147
|
+
return routes;
|
|
149
148
|
});
|
|
150
|
-
// Detect conflicting routes at the same path
|
|
151
|
-
const routesByPath = new Map();
|
|
152
|
-
for (const handle of handles) {
|
|
153
|
-
const existing = routesByPath.get(handle.routePath) || [];
|
|
154
|
-
existing.push(handle);
|
|
155
|
-
routesByPath.set(handle.routePath, existing);
|
|
156
|
-
}
|
|
157
|
-
for (const [path, pathHandles] of routesByPath) {
|
|
158
|
-
const routeHandles = pathHandles.filter(h => h.handle === "route");
|
|
159
|
-
if (routeHandles.length > 1) {
|
|
160
|
-
const modulePaths = routeHandles.map(h => h.modulePath).join(", ");
|
|
161
|
-
throw new Error(`Conflicting routes detected at path ${path}: ${modulePaths}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return handles;
|
|
165
|
-
}
|
|
166
|
-
export function treeFromRouteHandles(handles) {
|
|
167
|
-
const handlesByPath = Array.groupBy(handles, handle => handle.routePath);
|
|
168
|
-
const paths = Record.keys(handlesByPath);
|
|
169
|
-
const root = {
|
|
170
|
-
path: "/",
|
|
171
|
-
handles: handlesByPath["/"] || [],
|
|
172
|
-
};
|
|
173
|
-
const nodeMap = new Map([["/", root]]);
|
|
174
|
-
for (const absolutePath of paths) {
|
|
175
|
-
if (absolutePath === "/")
|
|
176
|
-
continue;
|
|
177
|
-
// Find parent path
|
|
178
|
-
const segments = absolutePath.split("/").filter(Boolean);
|
|
179
|
-
const parentPath = segments.length === 1
|
|
180
|
-
? "/"
|
|
181
|
-
: "/" + segments.slice(0, -1).join("/");
|
|
182
|
-
const parent = nodeMap.get(parentPath);
|
|
183
|
-
if (!parent) {
|
|
184
|
-
continue; // Skip orphaned paths
|
|
185
|
-
}
|
|
186
|
-
// Create node with relative path
|
|
187
|
-
const relativePath = parent.path === "/"
|
|
188
|
-
? absolutePath
|
|
189
|
-
: absolutePath.slice(parentPath.length);
|
|
190
|
-
const node = {
|
|
191
|
-
path: relativePath,
|
|
192
|
-
handles: handlesByPath[absolutePath],
|
|
193
|
-
};
|
|
194
|
-
// Add to parent
|
|
195
|
-
if (!parent.children) {
|
|
196
|
-
parent.children = [];
|
|
197
|
-
}
|
|
198
|
-
parent.children.push(node);
|
|
199
|
-
// Store for future children
|
|
200
|
-
nodeMap.set(absolutePath, node);
|
|
201
|
-
}
|
|
202
|
-
return root;
|
|
203
149
|
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
import type { PlatformError } from "@effect/platform/Error";
|
|
2
1
|
import * as FileSystem from "@effect/platform/FileSystem";
|
|
3
2
|
import * as Effect from "effect/Effect";
|
|
4
3
|
import * as Schema from "effect/Schema";
|
|
4
|
+
import * as FilePathPattern from "./FilePathPattern.ts";
|
|
5
5
|
import * as FileRouter from "./FileRouter.ts";
|
|
6
|
-
import * as FileRouterPattern from "./FileRouterPattern.ts";
|
|
7
6
|
export declare function validateRouteModule(module: unknown): module is FileRouter.RouteModule;
|
|
8
|
-
export declare function generatePathParamsSchema(segments: ReadonlyArray<
|
|
7
|
+
export declare function generatePathParamsSchema(segments: ReadonlyArray<FilePathPattern.Segment>): Schema.Struct<any> | null;
|
|
9
8
|
/**
|
|
10
9
|
* Validates all route modules in the given route handles.
|
|
11
10
|
*/
|
|
12
|
-
export declare function validateRouteModules(
|
|
13
|
-
export declare function generateCode(
|
|
11
|
+
export declare function validateRouteModules(path: string, routes: FileRouter.OrderedFileRoutes): Effect.Effect<void, FileRouter.FileRouterError, FileSystem.FileSystem>;
|
|
12
|
+
export declare function generateCode(fileRoutes: FileRouter.OrderedFileRoutes): string | null;
|
|
14
13
|
/**
|
|
15
|
-
* Updates the
|
|
14
|
+
* Updates the tree file only if the generated content differs from the existing file.
|
|
16
15
|
* This prevents infinite loops when watching for file changes.
|
|
17
16
|
*/
|
|
18
|
-
export declare function update(routesPath: string,
|
|
19
|
-
export declare function dump(routesPath: string,
|
|
17
|
+
export declare function update(routesPath: string, treePath?: string): Effect.Effect<void, FileRouter.FileRouterError, FileSystem.FileSystem>;
|
|
18
|
+
export declare function dump(routesPath: string, treePath?: string): Effect.Effect<void, FileRouter.FileRouterError, FileSystem.FileSystem>;
|
|
@@ -8,9 +8,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import * as FileSystem from "@effect/platform/FileSystem";
|
|
10
10
|
import * as Effect from "effect/Effect";
|
|
11
|
-
import * as
|
|
11
|
+
import * as Either from "effect/Either";
|
|
12
12
|
import * as Schema from "effect/Schema";
|
|
13
13
|
import * as NPath from "node:path";
|
|
14
|
+
import * as FilePathPattern from "./FilePathPattern.js";
|
|
14
15
|
import * as FileRouter from "./FileRouter.js";
|
|
15
16
|
import * as SchemaExtra from "./SchemaExtra.js";
|
|
16
17
|
export function validateRouteModule(module) {
|
|
@@ -26,11 +27,11 @@ export function validateRouteModule(module) {
|
|
|
26
27
|
export function generatePathParamsSchema(segments) {
|
|
27
28
|
const fields = {};
|
|
28
29
|
for (const segment of segments) {
|
|
29
|
-
if (segment._tag === "ParamSegment"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
if (segment._tag === "ParamSegment") {
|
|
31
|
+
fields[segment.name] = Schema.String;
|
|
32
|
+
}
|
|
33
|
+
else if (segment._tag === "RestSegment") {
|
|
34
|
+
fields[segment.name] = Schema.optional(Schema.String);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
if (Object.keys(fields).length === 0) {
|
|
@@ -41,18 +42,25 @@ export function generatePathParamsSchema(segments) {
|
|
|
41
42
|
/**
|
|
42
43
|
* Validates all route modules in the given route handles.
|
|
43
44
|
*/
|
|
44
|
-
export function validateRouteModules(
|
|
45
|
+
export function validateRouteModules(path, routes) {
|
|
45
46
|
return Effect.gen(function* () {
|
|
46
47
|
const fs = yield* FileSystem.FileSystem;
|
|
47
|
-
const routeHandles =
|
|
48
|
+
const routeHandles = routes.filter(h => h.handle === "route");
|
|
48
49
|
for (const handle of routeHandles) {
|
|
49
|
-
const routeModulePath = NPath.resolve(
|
|
50
|
+
const routeModulePath = NPath.resolve(path, handle.modulePath);
|
|
50
51
|
const expectedSchema = generatePathParamsSchema(handle.segments);
|
|
51
|
-
const fileExists = yield* fs.exists(routeModulePath);
|
|
52
|
+
const fileExists = yield* fs.exists(routeModulePath).pipe(Effect.catchAll(() => Effect.succeed(false)));
|
|
52
53
|
if (!fileExists) {
|
|
53
54
|
continue;
|
|
54
55
|
}
|
|
55
|
-
const module = yield* Effect.
|
|
56
|
+
const module = yield* Effect.tryPromise({
|
|
57
|
+
try: () => import(__rewriteRelativeImportExtension(routeModulePath)),
|
|
58
|
+
catch: (cause) => new FileRouter.FileRouterError({
|
|
59
|
+
reason: "Import",
|
|
60
|
+
cause,
|
|
61
|
+
path: routeModulePath,
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
56
64
|
if (!validateRouteModule(module)) {
|
|
57
65
|
yield* Effect.logWarning(`Route module ${routeModulePath} should export default Route`);
|
|
58
66
|
continue;
|
|
@@ -69,36 +77,31 @@ export function validateRouteModules(routesPath, handles) {
|
|
|
69
77
|
}
|
|
70
78
|
});
|
|
71
79
|
}
|
|
72
|
-
export function generateCode(
|
|
73
|
-
const routerModuleId = "effect-start";
|
|
80
|
+
export function generateCode(fileRoutes) {
|
|
74
81
|
// Group routes by path to find layers
|
|
75
82
|
const routesByPath = new Map();
|
|
76
|
-
for (const
|
|
77
|
-
const existing = routesByPath.get(
|
|
78
|
-
if (
|
|
79
|
-
existing.route =
|
|
83
|
+
for (const fileRoute of fileRoutes) {
|
|
84
|
+
const existing = routesByPath.get(fileRoute.routePath) || { layers: [] };
|
|
85
|
+
if (fileRoute.handle === "route") {
|
|
86
|
+
existing.route = fileRoute;
|
|
80
87
|
}
|
|
81
|
-
else if (
|
|
82
|
-
existing.layers.push(
|
|
88
|
+
else if (fileRoute.handle === "layer") {
|
|
89
|
+
existing.layers.push(fileRoute);
|
|
83
90
|
}
|
|
84
|
-
routesByPath.set(
|
|
91
|
+
routesByPath.set(fileRoute.routePath, existing);
|
|
85
92
|
}
|
|
86
|
-
// Generate route definitions
|
|
87
|
-
const routes = [];
|
|
88
93
|
// Helper to check if layer's path is an ancestor of route's path
|
|
89
94
|
const layerMatchesRoute = (layer, route) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Layer at root (empty layerDir) applies to all routes
|
|
93
|
-
if (layerDir === "")
|
|
95
|
+
const layerDir = layer.modulePath.replace(/\/(layer)\.(tsx?|jsx?)$/, "");
|
|
96
|
+
if (layerDir === "/")
|
|
94
97
|
return true;
|
|
95
|
-
// Route's modulePath must start with the layer's directory
|
|
96
98
|
return route.modulePath.startsWith(layerDir + "/");
|
|
97
99
|
};
|
|
98
|
-
//
|
|
100
|
+
// Build entries for each route path
|
|
101
|
+
const entries = [];
|
|
99
102
|
for (const [path, { route }] of routesByPath) {
|
|
100
103
|
if (!route)
|
|
101
|
-
continue;
|
|
104
|
+
continue;
|
|
102
105
|
// Collect all parent layers that match the route's groups
|
|
103
106
|
const allLayers = [];
|
|
104
107
|
let currentPath = path;
|
|
@@ -110,67 +113,95 @@ export function generateCode(handles) {
|
|
|
110
113
|
}
|
|
111
114
|
if (currentPath === "/")
|
|
112
115
|
break;
|
|
113
|
-
// Move to parent path
|
|
114
116
|
const parentPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
|
|
115
117
|
currentPath = parentPath || "/";
|
|
116
118
|
}
|
|
117
|
-
//
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
// Convert file-style path to colon-style PathPattern
|
|
120
|
+
const pathPatternResult = FilePathPattern.toPathPattern(path);
|
|
121
|
+
if (Either.isLeft(pathPatternResult)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const pathPattern = pathPatternResult.right;
|
|
125
|
+
// Order: route first, then layers from innermost to outermost
|
|
126
|
+
const loaders = [
|
|
127
|
+
`() => import(".${route.modulePath}")`,
|
|
128
|
+
...allLayers.reverse().map(layer => `() => import(".${layer.modulePath}")`),
|
|
129
|
+
];
|
|
130
|
+
entries.push({ path: pathPattern, loaders });
|
|
131
|
+
}
|
|
132
|
+
// No routes found - don't create file
|
|
133
|
+
if (entries.length === 0) {
|
|
134
|
+
return null;
|
|
126
135
|
}
|
|
127
|
-
const
|
|
136
|
+
const routeEntries = entries
|
|
137
|
+
.map(({ path, loaders }) => {
|
|
138
|
+
const loadersCode = loaders.join(",\n ");
|
|
139
|
+
return ` "${path}": [\n ${loadersCode},\n ]`;
|
|
140
|
+
})
|
|
141
|
+
.join(",\n");
|
|
142
|
+
return `/**
|
|
128
143
|
* Auto-generated by effect-start.
|
|
129
|
-
|
|
130
|
-
const routesArray = routes.length > 0
|
|
131
|
-
? `[\n${routes.join("\n")}\n]`
|
|
132
|
-
: "[]";
|
|
133
|
-
return `${header}
|
|
144
|
+
*/
|
|
134
145
|
|
|
135
|
-
export
|
|
146
|
+
export default {
|
|
147
|
+
${routeEntries},
|
|
148
|
+
} satisfies import("effect-start/FileRouter").FileRoutes
|
|
136
149
|
`;
|
|
137
150
|
}
|
|
138
151
|
/**
|
|
139
|
-
* Updates the
|
|
152
|
+
* Updates the tree file only if the generated content differs from the existing file.
|
|
140
153
|
* This prevents infinite loops when watching for file changes.
|
|
141
154
|
*/
|
|
142
|
-
export function update(routesPath,
|
|
155
|
+
export function update(routesPath, treePath = "server.gen.ts") {
|
|
143
156
|
return Effect.gen(function* () {
|
|
144
|
-
|
|
157
|
+
treePath = NPath.resolve(routesPath, treePath);
|
|
145
158
|
const fs = yield* FileSystem.FileSystem;
|
|
146
|
-
const files = yield* fs.readDirectory(routesPath, { recursive: true });
|
|
147
|
-
const
|
|
159
|
+
const files = yield* fs.readDirectory(routesPath, { recursive: true }).pipe(Effect.mapError((cause) => new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: routesPath })));
|
|
160
|
+
const fileRoutes = yield* FileRouter.getFileRoutes(files);
|
|
148
161
|
// Validate route modules
|
|
149
|
-
yield* validateRouteModules(routesPath,
|
|
150
|
-
const newCode = generateCode(
|
|
151
|
-
// Check if file exists
|
|
162
|
+
yield* validateRouteModules(routesPath, fileRoutes);
|
|
163
|
+
const newCode = generateCode(fileRoutes);
|
|
164
|
+
// Check if file exists (ok to fail - means file doesn't exist)
|
|
152
165
|
const existingCode = yield* fs
|
|
153
|
-
.readFileString(
|
|
166
|
+
.readFileString(treePath)
|
|
154
167
|
.pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
168
|
+
// No routes found
|
|
169
|
+
if (newCode === null) {
|
|
170
|
+
// If gen file exists, write empty export
|
|
171
|
+
if (existingCode !== null) {
|
|
172
|
+
const emptyCode = "export default {}\n";
|
|
173
|
+
if (existingCode !== emptyCode) {
|
|
174
|
+
yield* Effect.logDebug(`Clearing file routes tree: ${treePath}`);
|
|
175
|
+
yield* fs.writeFileString(treePath, emptyCode).pipe(Effect.mapError((cause) => new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: treePath })));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Write if content differs
|
|
155
181
|
if (existingCode !== newCode) {
|
|
156
|
-
yield* Effect.logDebug(`Updating file routes
|
|
157
|
-
yield* fs.writeFileString(
|
|
182
|
+
yield* Effect.logDebug(`Updating file routes tree: ${treePath}`);
|
|
183
|
+
yield* fs.writeFileString(treePath, newCode).pipe(Effect.mapError((cause) => new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: treePath })));
|
|
158
184
|
}
|
|
159
185
|
else {
|
|
160
|
-
yield* Effect.logDebug(`File routes
|
|
186
|
+
yield* Effect.logDebug(`File routes tree unchanged: ${treePath}`);
|
|
161
187
|
}
|
|
162
188
|
});
|
|
163
189
|
}
|
|
164
|
-
export function dump(routesPath,
|
|
190
|
+
export function dump(routesPath, treePath = "server.gen.ts") {
|
|
165
191
|
return Effect.gen(function* () {
|
|
166
|
-
|
|
192
|
+
treePath = NPath.resolve(routesPath, treePath);
|
|
167
193
|
const fs = yield* FileSystem.FileSystem;
|
|
168
|
-
const files = yield* fs.readDirectory(routesPath, { recursive: true });
|
|
169
|
-
const
|
|
194
|
+
const files = yield* fs.readDirectory(routesPath, { recursive: true }).pipe(Effect.mapError((cause) => new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: routesPath })));
|
|
195
|
+
const fileRoutes = yield* FileRouter.getFileRoutes(files);
|
|
170
196
|
// Validate route modules
|
|
171
|
-
yield* validateRouteModules(routesPath,
|
|
172
|
-
const code = generateCode(
|
|
173
|
-
|
|
174
|
-
|
|
197
|
+
yield* validateRouteModules(routesPath, fileRoutes);
|
|
198
|
+
const code = generateCode(fileRoutes);
|
|
199
|
+
// No routes found - don't create file
|
|
200
|
+
if (code === null) {
|
|
201
|
+
yield* Effect.logDebug(`No routes found, skipping: ${treePath}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
yield* Effect.logDebug(`Generating file routes tree: ${treePath}`);
|
|
205
|
+
yield* fs.writeFileString(treePath, code).pipe(Effect.mapError((cause) => new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: treePath })));
|
|
175
206
|
});
|
|
176
207
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type * as Cause from "effect/Cause";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import type * as Types from "effect/Types";
|
|
4
|
+
import { TypeId as TypeId_ } from "@effect/platform/Error";
|
|
5
|
+
export declare const TypeId: typeof TypeId_;
|
|
6
|
+
export type TypeId = typeof TypeId;
|
|
7
|
+
export declare const isPlatformError: (u: unknown) => u is PlatformError;
|
|
8
|
+
export declare const TypeIdError: <const TypeId extends symbol, const Tag extends string>(typeId: TypeId, tag: Tag) => new <A extends Record<string, any>>(args: Types.Simplify<A>) => Cause.YieldableError & Record<TypeId, TypeId> & {
|
|
9
|
+
readonly _tag: Tag;
|
|
10
|
+
} & Readonly<A>;
|
|
11
|
+
export declare const Module: Schema.Literal<["Clipboard", "Command", "FileSystem", "KeyValueStore", "Path", "Stream", "Terminal"]>;
|
|
12
|
+
declare const BadArgument_base: Schema.TaggedErrorClass<BadArgument, "BadArgument", {
|
|
13
|
+
readonly _tag: Schema.tag<"BadArgument">;
|
|
14
|
+
} & {
|
|
15
|
+
module: Schema.Literal<["Clipboard", "Command", "FileSystem", "KeyValueStore", "Path", "Stream", "Terminal"]>;
|
|
16
|
+
method: typeof Schema.String;
|
|
17
|
+
description: Schema.optional<typeof Schema.String>;
|
|
18
|
+
cause: Schema.optional<typeof Schema.Defect>;
|
|
19
|
+
}>;
|
|
20
|
+
export declare class BadArgument extends BadArgument_base {
|
|
21
|
+
readonly [TypeId]: typeof TypeId;
|
|
22
|
+
get message(): string;
|
|
23
|
+
}
|
|
24
|
+
export declare const SystemErrorReason: Schema.Literal<["AlreadyExists", "BadResource", "Busy", "InvalidData", "NotFound", "PermissionDenied", "TimedOut", "UnexpectedEof", "Unknown", "WouldBlock", "WriteZero"]>;
|
|
25
|
+
export type SystemErrorReason = typeof SystemErrorReason.Type;
|
|
26
|
+
declare const SystemError_base: Schema.TaggedErrorClass<SystemError, "SystemError", {
|
|
27
|
+
readonly _tag: Schema.tag<"SystemError">;
|
|
28
|
+
} & {
|
|
29
|
+
reason: Schema.Literal<["AlreadyExists", "BadResource", "Busy", "InvalidData", "NotFound", "PermissionDenied", "TimedOut", "UnexpectedEof", "Unknown", "WouldBlock", "WriteZero"]>;
|
|
30
|
+
module: Schema.Literal<["Clipboard", "Command", "FileSystem", "KeyValueStore", "Path", "Stream", "Terminal"]>;
|
|
31
|
+
method: typeof Schema.String;
|
|
32
|
+
description: Schema.optional<typeof Schema.String>;
|
|
33
|
+
syscall: Schema.optional<typeof Schema.String>;
|
|
34
|
+
pathOrDescriptor: Schema.optional<Schema.Union<[typeof Schema.String, typeof Schema.Number]>>;
|
|
35
|
+
cause: Schema.optional<typeof Schema.Defect>;
|
|
36
|
+
}>;
|
|
37
|
+
export declare class SystemError extends SystemError_base {
|
|
38
|
+
readonly [TypeId]: typeof TypeId;
|
|
39
|
+
get message(): string;
|
|
40
|
+
}
|
|
41
|
+
export type PlatformError = BadArgument | SystemError;
|
|
42
|
+
export declare const PlatformError: Schema.Union<[
|
|
43
|
+
typeof BadArgument,
|
|
44
|
+
typeof SystemError
|
|
45
|
+
]>;
|
|
46
|
+
export {};
|