effect-start 0.17.0 → 0.17.2
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/Commander.d.ts +103 -0
- package/dist/Commander.js +333 -0
- package/dist/ContentNegotiation.d.ts +13 -0
- package/dist/ContentNegotiation.js +364 -0
- package/dist/Development.d.ts +34 -0
- package/dist/Development.js +52 -0
- package/dist/Entity.d.ts +47 -0
- package/dist/Entity.js +224 -0
- package/dist/FileRouter.d.ts +61 -0
- package/dist/FileRouter.js +203 -0
- package/dist/FileRouterCodegen.d.ts +19 -0
- package/dist/FileRouterCodegen.js +176 -0
- package/dist/FileRouterPattern.d.ts +9 -0
- package/dist/FileRouterPattern.js +35 -0
- package/dist/Http.d.ts +37 -0
- package/dist/Http.js +92 -0
- package/dist/HttpAppExtra.d.ts +7 -0
- package/dist/HttpAppExtra.js +320 -0
- package/dist/HttpUtils.d.ts +3 -0
- package/dist/HttpUtils.js +11 -0
- package/dist/PathPattern.d.ts +134 -0
- package/dist/PathPattern.js +415 -0
- package/dist/Random.d.ts +5 -0
- package/dist/Random.js +49 -0
- package/dist/Route.d.ts +98 -0
- package/dist/Route.js +81 -0
- package/dist/RouteBody.d.ts +53 -0
- package/dist/RouteBody.js +67 -0
- package/dist/RouteHook.d.ts +12 -0
- package/dist/RouteHook.js +45 -0
- package/dist/RouteHttp.d.ts +21 -0
- package/dist/RouteHttp.js +260 -0
- package/dist/RouteHttpTracer.d.ts +10 -0
- package/dist/RouteHttpTracer.js +62 -0
- package/dist/RouteMount.d.ts +119 -0
- package/dist/RouteMount.js +77 -0
- package/dist/RouteSchema.d.ts +65 -0
- package/dist/RouteSchema.js +155 -0
- package/dist/RouteSse.d.ts +21 -0
- package/dist/RouteSse.js +85 -0
- package/dist/RouteTree.d.ts +56 -0
- package/dist/RouteTree.js +91 -0
- package/dist/RouteTrie.d.ts +20 -0
- package/dist/RouteTrie.js +157 -0
- package/dist/RouterPattern.d.ts +118 -0
- package/dist/RouterPattern.js +269 -0
- package/dist/SchemaExtra.d.ts +7 -0
- package/dist/SchemaExtra.js +74 -0
- package/dist/Start.d.ts +19 -0
- package/dist/Start.js +23 -0
- package/dist/StartApp.d.ts +19 -0
- package/dist/StartApp.js +21 -0
- package/dist/StreamExtra.d.ts +28 -0
- package/dist/StreamExtra.js +100 -0
- package/dist/TuplePathPattern.d.ts +9 -0
- package/dist/TuplePathPattern.js +63 -0
- package/dist/Values.d.ts +26 -0
- package/dist/Values.js +30 -0
- package/dist/bun/BunBundle.d.ts +12 -0
- package/dist/bun/BunBundle.js +145 -0
- package/dist/bun/BunHttpServer.d.ts +44 -0
- package/dist/bun/BunHttpServer.js +187 -0
- package/dist/bun/BunHttpServer_web.d.ts +60 -0
- package/dist/bun/BunHttpServer_web.js +252 -0
- package/dist/bun/BunImportTrackerPlugin.d.ts +13 -0
- package/dist/bun/BunImportTrackerPlugin.js +71 -0
- package/dist/bun/BunRoute.d.ts +49 -0
- package/dist/bun/BunRoute.js +131 -0
- package/dist/bun/BunRuntime.d.ts +1 -0
- package/dist/bun/BunRuntime.js +26 -0
- package/dist/bun/BunVirtualFilesPlugin.d.ts +4 -0
- package/dist/bun/BunVirtualFilesPlugin.js +40 -0
- package/dist/bun/_BunEnhancedResolve.d.ts +45 -0
- package/dist/bun/_BunEnhancedResolve.js +104 -0
- package/dist/bun/index.d.ts +4 -0
- package/dist/bun/index.js +4 -0
- package/dist/bundler/Bundle.d.ts +60 -0
- package/dist/bundler/Bundle.js +48 -0
- package/dist/bundler/BundleFiles.d.ts +13 -0
- package/dist/bundler/BundleFiles.js +94 -0
- package/dist/bundler/BundleHttp.d.ts +45 -0
- package/dist/bundler/BundleHttp.js +176 -0
- package/dist/client/Overlay.d.ts +2 -0
- package/dist/client/Overlay.js +32 -0
- package/dist/client/ScrollState.d.ts +6 -0
- package/dist/client/ScrollState.js +98 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +81 -0
- package/dist/experimental/EncryptedCookies.d.ts +51 -0
- package/dist/experimental/EncryptedCookies.js +243 -0
- package/dist/experimental/SseHttpResponse.d.ts +7 -0
- package/dist/experimental/SseHttpResponse.js +28 -0
- package/dist/experimental/index.d.ts +2 -0
- package/dist/experimental/index.js +2 -0
- package/dist/hyper/Hyper.d.ts +32 -0
- package/dist/hyper/Hyper.js +34 -0
- package/dist/hyper/HyperHtml.d.ts +23 -0
- package/dist/hyper/HyperHtml.js +144 -0
- package/dist/hyper/HyperNode.d.ts +14 -0
- package/dist/hyper/HyperNode.js +11 -0
- package/dist/hyper/HyperRoute.d.ts +8 -0
- package/dist/hyper/HyperRoute.js +32 -0
- package/dist/hyper/HyperRoute.test.d.ts +1 -0
- package/dist/hyper/HyperRoute.test.js +72 -0
- package/dist/hyper/index.d.ts +4 -0
- package/dist/hyper/index.js +4 -0
- package/dist/hyper/jsx-runtime.d.ts +7 -0
- package/dist/hyper/jsx-runtime.js +8 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/inference_check.d.ts +1 -0
- package/dist/inference_check.js +15 -0
- package/dist/middlewares/BasicAuthMiddleware.d.ts +8 -0
- package/dist/middlewares/BasicAuthMiddleware.js +22 -0
- package/dist/middlewares/index.d.ts +1 -0
- package/dist/middlewares/index.js +1 -0
- package/dist/node/FileSystem.d.ts +9 -0
- package/dist/node/FileSystem.js +440 -0
- package/dist/node/Utils.d.ts +1 -0
- package/dist/node/Utils.js +19 -0
- package/dist/repro_fail.d.ts +1 -0
- package/dist/repro_fail.js +14 -0
- package/dist/testing/TestHttpClient.d.ts +13 -0
- package/dist/testing/TestHttpClient.js +68 -0
- package/dist/testing/TestLogger.d.ts +13 -0
- package/dist/testing/TestLogger.js +29 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/utils.d.ts +9 -0
- package/dist/testing/utils.js +39 -0
- package/dist/x/cloudflare/CloudflareTunnel.d.ts +13 -0
- package/dist/x/cloudflare/CloudflareTunnel.js +43 -0
- package/dist/x/cloudflare/index.d.ts +1 -0
- package/dist/x/cloudflare/index.js +1 -0
- package/dist/x/datastar/Datastar.d.ts +6 -0
- package/dist/x/datastar/Datastar.js +46 -0
- package/dist/x/datastar/index.d.ts +2 -0
- package/dist/x/datastar/index.js +2 -0
- package/dist/x/tailwind/TailwindPlugin.d.ts +23 -0
- package/dist/x/tailwind/TailwindPlugin.js +219 -0
- package/dist/x/tailwind/compile.d.ts +19 -0
- package/dist/x/tailwind/compile.js +156 -0
- package/dist/x/tailwind/plugin.d.ts +2 -0
- package/dist/x/tailwind/plugin.js +15 -0
- package/package.json +68 -16
- package/src/RouteBody.test.ts +18 -0
- package/src/RouteBody.ts +126 -2
- package/src/x/tailwind/compile.ts +8 -2
package/dist/Entity.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as ParseResult from "effect/ParseResult";
|
|
3
|
+
import * as Pipeable from "effect/Pipeable";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import * as StreamExtra from "./StreamExtra.js";
|
|
8
|
+
import * as Values from "./Values.js";
|
|
9
|
+
export const TypeId = Symbol.for("effect-start/Entity");
|
|
10
|
+
const textDecoder = new TextDecoder();
|
|
11
|
+
const textEncoder = new TextEncoder();
|
|
12
|
+
function isBinary(v) {
|
|
13
|
+
return v instanceof Uint8Array || v instanceof ArrayBuffer;
|
|
14
|
+
}
|
|
15
|
+
function parseJson(s) {
|
|
16
|
+
try {
|
|
17
|
+
return Effect.succeed(JSON.parse(s));
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
return Effect.fail(new ParseResult.ParseError({
|
|
21
|
+
issue: new ParseResult.Type(Schema.Unknown.ast, s, e instanceof Error ? e.message : "Failed to parse JSON"),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function getText(self) {
|
|
26
|
+
const v = self.body;
|
|
27
|
+
if (StreamExtra.isStream(v)) {
|
|
28
|
+
return Stream.mkString(Stream.decodeText(v));
|
|
29
|
+
}
|
|
30
|
+
if (Effect.isEffect(v)) {
|
|
31
|
+
return Effect.flatMap(v, (inner) => {
|
|
32
|
+
if (isEntity(inner)) {
|
|
33
|
+
return inner.text;
|
|
34
|
+
}
|
|
35
|
+
if (typeof inner === "string") {
|
|
36
|
+
return Effect.succeed(inner);
|
|
37
|
+
}
|
|
38
|
+
if (isBinary(inner)) {
|
|
39
|
+
return Effect.succeed(textDecoder.decode(inner));
|
|
40
|
+
}
|
|
41
|
+
return Effect.fail(mismatch(Schema.String, inner));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (typeof v === "string") {
|
|
45
|
+
return Effect.succeed(v);
|
|
46
|
+
}
|
|
47
|
+
if (isBinary(v)) {
|
|
48
|
+
return Effect.succeed(textDecoder.decode(v));
|
|
49
|
+
}
|
|
50
|
+
return Effect.fail(mismatch(Schema.String, v));
|
|
51
|
+
}
|
|
52
|
+
function getJson(self) {
|
|
53
|
+
const v = self.body;
|
|
54
|
+
if (StreamExtra.isStream(v)) {
|
|
55
|
+
return Effect.flatMap(getText(self), parseJson);
|
|
56
|
+
}
|
|
57
|
+
if (Effect.isEffect(v)) {
|
|
58
|
+
return Effect.flatMap(v, (inner) => {
|
|
59
|
+
if (isEntity(inner)) {
|
|
60
|
+
return inner.json;
|
|
61
|
+
}
|
|
62
|
+
if (typeof inner === "object" && inner !== null && !isBinary(inner)) {
|
|
63
|
+
return Effect.succeed(inner);
|
|
64
|
+
}
|
|
65
|
+
if (typeof inner === "string") {
|
|
66
|
+
return parseJson(inner);
|
|
67
|
+
}
|
|
68
|
+
if (isBinary(inner)) {
|
|
69
|
+
return parseJson(textDecoder.decode(inner));
|
|
70
|
+
}
|
|
71
|
+
return Effect.fail(mismatch(Schema.Unknown, inner));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (typeof v === "object" && v !== null && !isBinary(v)) {
|
|
75
|
+
return Effect.succeed(v);
|
|
76
|
+
}
|
|
77
|
+
if (typeof v === "string") {
|
|
78
|
+
return parseJson(v);
|
|
79
|
+
}
|
|
80
|
+
if (isBinary(v)) {
|
|
81
|
+
return parseJson(textDecoder.decode(v));
|
|
82
|
+
}
|
|
83
|
+
return Effect.fail(mismatch(Schema.Unknown, v));
|
|
84
|
+
}
|
|
85
|
+
function getBytes(self) {
|
|
86
|
+
const v = self.body;
|
|
87
|
+
if (StreamExtra.isStream(v)) {
|
|
88
|
+
return Stream.runFold(v, new Uint8Array(0), Values.concatBytes);
|
|
89
|
+
}
|
|
90
|
+
if (Effect.isEffect(v)) {
|
|
91
|
+
return Effect.flatMap(v, (inner) => {
|
|
92
|
+
if (isEntity(inner)) {
|
|
93
|
+
return inner.bytes;
|
|
94
|
+
}
|
|
95
|
+
if (inner instanceof Uint8Array) {
|
|
96
|
+
return Effect.succeed(inner);
|
|
97
|
+
}
|
|
98
|
+
if (inner instanceof ArrayBuffer) {
|
|
99
|
+
return Effect.succeed(new Uint8Array(inner));
|
|
100
|
+
}
|
|
101
|
+
if (typeof inner === "string") {
|
|
102
|
+
return Effect.succeed(textEncoder.encode(inner));
|
|
103
|
+
}
|
|
104
|
+
return Effect.fail(mismatch(Schema.Uint8ArrayFromSelf, inner));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (v instanceof Uint8Array) {
|
|
108
|
+
return Effect.succeed(v);
|
|
109
|
+
}
|
|
110
|
+
if (v instanceof ArrayBuffer) {
|
|
111
|
+
return Effect.succeed(new Uint8Array(v));
|
|
112
|
+
}
|
|
113
|
+
if (typeof v === "string") {
|
|
114
|
+
return Effect.succeed(textEncoder.encode(v));
|
|
115
|
+
}
|
|
116
|
+
// Allows entity.stream to work when body is a JSON object
|
|
117
|
+
if (typeof v === "object" && v !== null && !isBinary(v)) {
|
|
118
|
+
return Effect.succeed(textEncoder.encode(JSON.stringify(v)));
|
|
119
|
+
}
|
|
120
|
+
return Effect.fail(mismatch(Schema.Uint8ArrayFromSelf, v));
|
|
121
|
+
}
|
|
122
|
+
function getStream(self) {
|
|
123
|
+
const v = self.body;
|
|
124
|
+
if (StreamExtra.isStream(v)) {
|
|
125
|
+
return v;
|
|
126
|
+
}
|
|
127
|
+
if (Effect.isEffect(v)) {
|
|
128
|
+
return Stream.unwrap(Effect.map(v, (inner) => {
|
|
129
|
+
if (isEntity(inner)) {
|
|
130
|
+
return inner.stream;
|
|
131
|
+
}
|
|
132
|
+
return Stream.fromEffect(getBytes(make(inner)));
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
return Stream.fromEffect(getBytes(self));
|
|
136
|
+
}
|
|
137
|
+
const Proto = Object.defineProperties(Object.create(null), {
|
|
138
|
+
[TypeId]: { value: TypeId },
|
|
139
|
+
pipe: {
|
|
140
|
+
value: function () {
|
|
141
|
+
return Pipeable.pipeArguments(this, arguments);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
text: {
|
|
145
|
+
get() {
|
|
146
|
+
return getText(this);
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
json: {
|
|
150
|
+
get() {
|
|
151
|
+
return getJson(this);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
bytes: {
|
|
155
|
+
get() {
|
|
156
|
+
return getBytes(this);
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
stream: {
|
|
160
|
+
get() {
|
|
161
|
+
return getStream(this);
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
export function isEntity(input) {
|
|
166
|
+
return Predicate.hasProperty(input, TypeId);
|
|
167
|
+
}
|
|
168
|
+
export function make(body, options) {
|
|
169
|
+
return Object.assign(Object.create(Proto), {
|
|
170
|
+
body,
|
|
171
|
+
headers: options?.headers ?? {},
|
|
172
|
+
url: options?.url,
|
|
173
|
+
status: options?.status,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
export function effect(body) {
|
|
177
|
+
return make(body);
|
|
178
|
+
}
|
|
179
|
+
export function resolve(entity) {
|
|
180
|
+
const body = entity.body;
|
|
181
|
+
if (Effect.isEffect(body)) {
|
|
182
|
+
return Effect.map(body, (inner) => isEntity(inner)
|
|
183
|
+
? inner
|
|
184
|
+
: make(inner, {
|
|
185
|
+
status: entity.status,
|
|
186
|
+
headers: entity.headers,
|
|
187
|
+
url: entity.url,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
return Effect.succeed(entity);
|
|
191
|
+
}
|
|
192
|
+
export function type(self) {
|
|
193
|
+
const h = self.headers;
|
|
194
|
+
if (h["content-type"]) {
|
|
195
|
+
return h["content-type"];
|
|
196
|
+
}
|
|
197
|
+
const v = self.body;
|
|
198
|
+
if (typeof v === "string") {
|
|
199
|
+
return "text/plain";
|
|
200
|
+
}
|
|
201
|
+
if (typeof v === "object" && v !== null && !isBinary(v)) {
|
|
202
|
+
return "application/json";
|
|
203
|
+
}
|
|
204
|
+
return "application/octet-stream";
|
|
205
|
+
}
|
|
206
|
+
export function length(self) {
|
|
207
|
+
const h = self.headers;
|
|
208
|
+
if (h["content-length"]) {
|
|
209
|
+
return parseInt(h["content-length"], 10);
|
|
210
|
+
}
|
|
211
|
+
const v = self.body;
|
|
212
|
+
if (typeof v === "string") {
|
|
213
|
+
return textEncoder.encode(v).byteLength;
|
|
214
|
+
}
|
|
215
|
+
if (isBinary(v)) {
|
|
216
|
+
return v.byteLength;
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
function mismatch(expected, actual) {
|
|
221
|
+
return new ParseResult.ParseError({
|
|
222
|
+
issue: new ParseResult.Type(expected.ast, actual),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { PlatformError } from "@effect/platform/Error";
|
|
2
|
+
import * as FileSystem from "@effect/platform/FileSystem";
|
|
3
|
+
import * as Context from "effect/Context";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as FileRouterPattern from "./FileRouterPattern.ts";
|
|
7
|
+
export type RouteModule = {
|
|
8
|
+
default: RouteSet.RouteSet.Default;
|
|
9
|
+
};
|
|
10
|
+
export type LazyRoute = {
|
|
11
|
+
path: `/${string}`;
|
|
12
|
+
load: () => Promise<RouteModule>;
|
|
13
|
+
layers?: ReadonlyArray<() => Promise<unknown>>;
|
|
14
|
+
};
|
|
15
|
+
type Manifest = {
|
|
16
|
+
routes: readonly LazyRoute[];
|
|
17
|
+
};
|
|
18
|
+
declare const FileRouter_base: Context.TagClass<FileRouter, "effect-start/FileRouter", Manifest>;
|
|
19
|
+
export declare class FileRouter extends FileRouter_base {
|
|
20
|
+
}
|
|
21
|
+
export type GroupSegment<Name extends string = string> = FileRouterPattern.GroupSegment<Name>;
|
|
22
|
+
export type Segment = FileRouterPattern.Segment;
|
|
23
|
+
export type RouteHandle = {
|
|
24
|
+
handle: "route" | "layer";
|
|
25
|
+
modulePath: string;
|
|
26
|
+
routePath: `/${string}`;
|
|
27
|
+
segments: Segment[];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Routes are sorted by depth, layers are first,
|
|
31
|
+
* rest parameters are put at the end for each segment.
|
|
32
|
+
* - layer.tsx
|
|
33
|
+
* - users/route.tsx
|
|
34
|
+
* - users/[userId]/route.tsx
|
|
35
|
+
* - [[...rest]]/route.tsx
|
|
36
|
+
*/
|
|
37
|
+
export type OrderedRouteHandles = RouteHandle[];
|
|
38
|
+
export declare const parse: typeof FileRouterPattern.parse;
|
|
39
|
+
export declare const formatSegment: typeof FileRouterPattern.formatSegment;
|
|
40
|
+
export declare const format: typeof FileRouterPattern.format;
|
|
41
|
+
export declare function parseRoute(path: string): RouteHandle;
|
|
42
|
+
/**
|
|
43
|
+
* Generates a manifest file that references all routes.
|
|
44
|
+
*/
|
|
45
|
+
export declare function layer(options: {
|
|
46
|
+
load: () => Promise<Manifest>;
|
|
47
|
+
path: string;
|
|
48
|
+
}): Layer.Layer<FileRouter, PlatformError, FileSystem.FileSystem>;
|
|
49
|
+
export declare function fromManifest(manifest: Manifest): Effect.Effect<Router.Router.Any>;
|
|
50
|
+
export declare function walkRoutesDirectory(dir: string): Effect.Effect<OrderedRouteHandles, PlatformError, FileSystem.FileSystem>;
|
|
51
|
+
/**
|
|
52
|
+
* Given a list of paths, return a list of route handles.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getRouteHandlesFromPaths(paths: string[]): OrderedRouteHandles;
|
|
55
|
+
type RouteTree = {
|
|
56
|
+
path: `/${string}`;
|
|
57
|
+
handles: RouteHandle[];
|
|
58
|
+
children?: RouteTree[];
|
|
59
|
+
};
|
|
60
|
+
export declare function treeFromRouteHandles(handles: RouteHandle[]): RouteTree;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import * as FileSystem from "@effect/platform/FileSystem";
|
|
2
|
+
import * as Array from "effect/Array";
|
|
3
|
+
import * as Context from "effect/Context";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Function from "effect/Function";
|
|
6
|
+
import * as Layer from "effect/Layer";
|
|
7
|
+
import * as Record from "effect/Record";
|
|
8
|
+
import * as Stream from "effect/Stream";
|
|
9
|
+
import * as NPath from "node:path";
|
|
10
|
+
import * as NUrl from "node:url";
|
|
11
|
+
import * as Development from "./Development.js";
|
|
12
|
+
import * as FileRouterCodegen from "./FileRouterCodegen.js";
|
|
13
|
+
import * as FileRouterPattern from "./FileRouterPattern.js";
|
|
14
|
+
export class FileRouter extends Context.Tag("effect-start/FileRouter")() {
|
|
15
|
+
}
|
|
16
|
+
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
|
+
export function parseRoute(path) {
|
|
21
|
+
const segs = parse(path);
|
|
22
|
+
const lastSeg = segs.at(-1);
|
|
23
|
+
const handleMatch = lastSeg?._tag === "LiteralSegment"
|
|
24
|
+
&& lastSeg.value.match(/^(route|layer)\.(tsx?|jsx?)$/);
|
|
25
|
+
const handle = handleMatch ? handleMatch[1] : null;
|
|
26
|
+
if (!handle) {
|
|
27
|
+
throw new Error(`Invalid route path "${path}": must end with a valid handle (route or layer)`);
|
|
28
|
+
}
|
|
29
|
+
// rest segments must be the last segment before the handle
|
|
30
|
+
const pathSegments = segs.slice(0, -1); // All segments except the handle
|
|
31
|
+
const restIndex = pathSegments.findIndex(seg => seg._tag === "RestSegment");
|
|
32
|
+
if (restIndex !== -1) {
|
|
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
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// No rest: all path segments are literal, param, or group
|
|
49
|
+
for (const seg of pathSegments) {
|
|
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
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const routePathSegments = pathSegments.filter(seg => seg._tag !== "GroupSegment");
|
|
58
|
+
const routePath = FileRouterPattern.format(routePathSegments);
|
|
59
|
+
return {
|
|
60
|
+
handle,
|
|
61
|
+
modulePath: path,
|
|
62
|
+
routePath,
|
|
63
|
+
segments: pathSegments,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generates a manifest file that references all routes.
|
|
68
|
+
*/
|
|
69
|
+
export function layer(options) {
|
|
70
|
+
let manifestPath = options.path;
|
|
71
|
+
if (manifestPath.startsWith("file://")) {
|
|
72
|
+
manifestPath = NUrl.fileURLToPath(manifestPath);
|
|
73
|
+
}
|
|
74
|
+
if (NPath.extname(manifestPath) === "") {
|
|
75
|
+
manifestPath = NPath.join(manifestPath, "index.ts");
|
|
76
|
+
}
|
|
77
|
+
const routesPath = NPath.dirname(manifestPath);
|
|
78
|
+
const manifestFilename = NPath.basename(manifestPath);
|
|
79
|
+
const resolvedManifestPath = NPath.resolve(routesPath, manifestFilename);
|
|
80
|
+
return Layer.provide(Layer.effect(FileRouter, Effect.promise(() => options.load())), Layer.scopedDiscard(Effect.gen(function* () {
|
|
81
|
+
yield* FileRouterCodegen.update(routesPath, manifestFilename);
|
|
82
|
+
const stream = Function.pipe(Development.watchSource({
|
|
83
|
+
path: routesPath,
|
|
84
|
+
filter: (e) => !e.path.includes("node_modules"),
|
|
85
|
+
}), Stream.onError((e) => Effect.logError(e)));
|
|
86
|
+
yield* Function.pipe(stream,
|
|
87
|
+
// filter out edits to gen file
|
|
88
|
+
Stream.filter(e => e.path !== resolvedManifestPath), Stream.runForEach(() => FileRouterCodegen.update(routesPath, manifestFilename)), Effect.fork);
|
|
89
|
+
})));
|
|
90
|
+
}
|
|
91
|
+
export function fromManifest(manifest) {
|
|
92
|
+
return Effect.gen(function* () {
|
|
93
|
+
const mounts = {};
|
|
94
|
+
yield* Effect.forEach(manifest.routes, (lazyRoute) => Effect.gen(function* () {
|
|
95
|
+
const routeModule = yield* Effect.promise(() => lazyRoute.load());
|
|
96
|
+
const layerModules = lazyRoute.layers
|
|
97
|
+
? yield* Effect.forEach(lazyRoute.layers, (loadLayer) => Effect.promise(() => loadLayer()))
|
|
98
|
+
: [];
|
|
99
|
+
// Start with the route from the route module
|
|
100
|
+
let mergedRouteSet = routeModule.default;
|
|
101
|
+
// Concatenate each layer's routes into the routeSet
|
|
102
|
+
for (const m of layerModules) {
|
|
103
|
+
const layerRouteSet = m.default;
|
|
104
|
+
if (RouteSet.isRouteSet(layerRouteSet)) {
|
|
105
|
+
mergedRouteSet = Route.merge(layerRouteSet, mergedRouteSet);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
mounts[lazyRoute.path] = mergedRouteSet;
|
|
109
|
+
}));
|
|
110
|
+
return Router.make(mounts, RouteSet.make());
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
export function walkRoutesDirectory(dir) {
|
|
114
|
+
return Effect.gen(function* () {
|
|
115
|
+
const fs = yield* FileSystem.FileSystem;
|
|
116
|
+
const files = yield* fs.readDirectory(dir, { recursive: true });
|
|
117
|
+
return getRouteHandlesFromPaths(files);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Given a list of paths, return a list of route handles.
|
|
122
|
+
*/
|
|
123
|
+
export function getRouteHandlesFromPaths(paths) {
|
|
124
|
+
const handles = paths
|
|
125
|
+
.map(f => f.match(ROUTE_PATH_REGEX))
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.map(v => {
|
|
128
|
+
const path = v[0];
|
|
129
|
+
try {
|
|
130
|
+
return parseRoute(path);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
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);
|
|
149
|
+
});
|
|
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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PlatformError } from "@effect/platform/Error";
|
|
2
|
+
import * as FileSystem from "@effect/platform/FileSystem";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import * as FileRouter from "./FileRouter.ts";
|
|
6
|
+
import * as FileRouterPattern from "./FileRouterPattern.ts";
|
|
7
|
+
export declare function validateRouteModule(module: unknown): module is FileRouter.RouteModule;
|
|
8
|
+
export declare function generatePathParamsSchema(segments: ReadonlyArray<FileRouterPattern.Segment>): Schema.Struct<any> | null;
|
|
9
|
+
/**
|
|
10
|
+
* Validates all route modules in the given route handles.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateRouteModules(routesPath: string, handles: FileRouter.OrderedRouteHandles): Effect.Effect<void, PlatformError, FileSystem.FileSystem>;
|
|
13
|
+
export declare function generateCode(handles: FileRouter.OrderedRouteHandles): string;
|
|
14
|
+
/**
|
|
15
|
+
* Updates the manifest file only if the generated content differs from the existing file.
|
|
16
|
+
* This prevents infinite loops when watching for file changes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function update(routesPath: string, manifestPath?: string): Effect.Effect<void, PlatformError, FileSystem.FileSystem>;
|
|
19
|
+
export declare function dump(routesPath: string, manifestPath?: string): Effect.Effect<void, PlatformError, FileSystem.FileSystem>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import * as FileSystem from "@effect/platform/FileSystem";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Function from "effect/Function";
|
|
12
|
+
import * as Schema from "effect/Schema";
|
|
13
|
+
import * as NPath from "node:path";
|
|
14
|
+
import * as FileRouter from "./FileRouter.js";
|
|
15
|
+
import * as SchemaExtra from "./SchemaExtra.js";
|
|
16
|
+
export function validateRouteModule(module) {
|
|
17
|
+
if (typeof module !== "object" || module === null) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (!("default" in module)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
// TODO: verify we're exporting a proper shape
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
export function generatePathParamsSchema(segments) {
|
|
27
|
+
const fields = {};
|
|
28
|
+
for (const segment of segments) {
|
|
29
|
+
if (segment._tag === "ParamSegment"
|
|
30
|
+
|| segment._tag === "RestSegment") {
|
|
31
|
+
fields[segment.name] = segment.optional
|
|
32
|
+
? Function.pipe(Schema.String, Schema.optional)
|
|
33
|
+
: Schema.String;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (Object.keys(fields).length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return Schema.Struct(fields);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validates all route modules in the given route handles.
|
|
43
|
+
*/
|
|
44
|
+
export function validateRouteModules(routesPath, handles) {
|
|
45
|
+
return Effect.gen(function* () {
|
|
46
|
+
const fs = yield* FileSystem.FileSystem;
|
|
47
|
+
const routeHandles = handles.filter(h => h.handle === "route");
|
|
48
|
+
for (const handle of routeHandles) {
|
|
49
|
+
const routeModulePath = NPath.resolve(routesPath, handle.modulePath);
|
|
50
|
+
const expectedSchema = generatePathParamsSchema(handle.segments);
|
|
51
|
+
const fileExists = yield* fs.exists(routeModulePath);
|
|
52
|
+
if (!fileExists) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const module = yield* Effect.promise(() => import(__rewriteRelativeImportExtension(routeModulePath)));
|
|
56
|
+
if (!validateRouteModule(module)) {
|
|
57
|
+
yield* Effect.logWarning(`Route module ${routeModulePath} should export default Route`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const routeSet = module.default;
|
|
61
|
+
// extract user schema
|
|
62
|
+
const userSchema = undefined;
|
|
63
|
+
if (expectedSchema
|
|
64
|
+
&& userSchema
|
|
65
|
+
&& !SchemaExtra.schemaEqual(userSchema, expectedSchema)) {
|
|
66
|
+
const relativeFilePath = NPath.relative(process.cwd(), routeModulePath);
|
|
67
|
+
yield* Effect.logError(`Route '${relativeFilePath}' has incorrect PathParams schema, expected schemaPathParams(${SchemaExtra.formatSchemaCode(expectedSchema)})`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function generateCode(handles) {
|
|
73
|
+
const routerModuleId = "effect-start";
|
|
74
|
+
// Group routes by path to find layers
|
|
75
|
+
const routesByPath = new Map();
|
|
76
|
+
for (const handle of handles) {
|
|
77
|
+
const existing = routesByPath.get(handle.routePath) || { layers: [] };
|
|
78
|
+
if (handle.handle === "route") {
|
|
79
|
+
existing.route = handle;
|
|
80
|
+
}
|
|
81
|
+
else if (handle.handle === "layer") {
|
|
82
|
+
existing.layers.push(handle);
|
|
83
|
+
}
|
|
84
|
+
routesByPath.set(handle.routePath, existing);
|
|
85
|
+
}
|
|
86
|
+
// Generate route definitions
|
|
87
|
+
const routes = [];
|
|
88
|
+
// Helper to check if layer's path is an ancestor of route's path
|
|
89
|
+
const layerMatchesRoute = (layer, route) => {
|
|
90
|
+
// Get the directory of the layer (strip the filename like layer.tsx)
|
|
91
|
+
const layerDir = layer.modulePath.replace(/\/?(layer)\.(tsx?|jsx?)$/, "");
|
|
92
|
+
// Layer at root (empty layerDir) applies to all routes
|
|
93
|
+
if (layerDir === "")
|
|
94
|
+
return true;
|
|
95
|
+
// Route's modulePath must start with the layer's directory
|
|
96
|
+
return route.modulePath.startsWith(layerDir + "/");
|
|
97
|
+
};
|
|
98
|
+
// Find layers for each route by walking up the path hierarchy
|
|
99
|
+
for (const [path, { route }] of routesByPath) {
|
|
100
|
+
if (!route)
|
|
101
|
+
continue; // Skip paths that only have layers
|
|
102
|
+
// Collect all parent layers that match the route's groups
|
|
103
|
+
const allLayers = [];
|
|
104
|
+
let currentPath = path;
|
|
105
|
+
while (true) {
|
|
106
|
+
const pathData = routesByPath.get(currentPath);
|
|
107
|
+
if (pathData?.layers) {
|
|
108
|
+
const matchingLayers = pathData.layers.filter(layer => layerMatchesRoute(layer, route));
|
|
109
|
+
allLayers.unshift(...matchingLayers);
|
|
110
|
+
}
|
|
111
|
+
if (currentPath === "/")
|
|
112
|
+
break;
|
|
113
|
+
// Move to parent path
|
|
114
|
+
const parentPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
|
|
115
|
+
currentPath = parentPath || "/";
|
|
116
|
+
}
|
|
117
|
+
// Generate layers array
|
|
118
|
+
const layersCode = allLayers.length > 0
|
|
119
|
+
? `\n layers: [\n ${allLayers.map(layer => `() => import("./${layer.modulePath}")`).join(",\n ")},\n ],`
|
|
120
|
+
: "";
|
|
121
|
+
const routeCode = ` {
|
|
122
|
+
path: "${path}",
|
|
123
|
+
load: () => import("./${route.modulePath}"),${layersCode}
|
|
124
|
+
},`;
|
|
125
|
+
routes.push(routeCode);
|
|
126
|
+
}
|
|
127
|
+
const header = `/**
|
|
128
|
+
* Auto-generated by effect-start.
|
|
129
|
+
*/`;
|
|
130
|
+
const routesArray = routes.length > 0
|
|
131
|
+
? `[\n${routes.join("\n")}\n]`
|
|
132
|
+
: "[]";
|
|
133
|
+
return `${header}
|
|
134
|
+
|
|
135
|
+
export const routes = ${routesArray} as const
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Updates the manifest file only if the generated content differs from the existing file.
|
|
140
|
+
* This prevents infinite loops when watching for file changes.
|
|
141
|
+
*/
|
|
142
|
+
export function update(routesPath, manifestPath = "manifest.ts") {
|
|
143
|
+
return Effect.gen(function* () {
|
|
144
|
+
manifestPath = NPath.resolve(routesPath, manifestPath);
|
|
145
|
+
const fs = yield* FileSystem.FileSystem;
|
|
146
|
+
const files = yield* fs.readDirectory(routesPath, { recursive: true });
|
|
147
|
+
const handles = FileRouter.getRouteHandlesFromPaths(files);
|
|
148
|
+
// Validate route modules
|
|
149
|
+
yield* validateRouteModules(routesPath, handles);
|
|
150
|
+
const newCode = generateCode(handles);
|
|
151
|
+
// Check if file exists and content differs
|
|
152
|
+
const existingCode = yield* fs
|
|
153
|
+
.readFileString(manifestPath)
|
|
154
|
+
.pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
155
|
+
if (existingCode !== newCode) {
|
|
156
|
+
yield* Effect.logDebug(`Updating file routes manifest: ${manifestPath}`);
|
|
157
|
+
yield* fs.writeFileString(manifestPath, newCode);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
yield* Effect.logDebug(`File routes manifest unchanged: ${manifestPath}`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export function dump(routesPath, manifestPath = "manifest.ts") {
|
|
165
|
+
return Effect.gen(function* () {
|
|
166
|
+
manifestPath = NPath.resolve(routesPath, manifestPath);
|
|
167
|
+
const fs = yield* FileSystem.FileSystem;
|
|
168
|
+
const files = yield* fs.readDirectory(routesPath, { recursive: true });
|
|
169
|
+
const handles = FileRouter.getRouteHandlesFromPaths(files);
|
|
170
|
+
// Validate route modules
|
|
171
|
+
yield* validateRouteModules(routesPath, handles);
|
|
172
|
+
const code = generateCode(handles);
|
|
173
|
+
yield* Effect.logDebug(`Generating file routes manifest: ${manifestPath}`);
|
|
174
|
+
yield* fs.writeFileString(manifestPath, code);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as RouterPattern from "./RouterPattern.ts";
|
|
2
|
+
export type GroupSegment<Name extends string = string> = {
|
|
3
|
+
_tag: "GroupSegment";
|
|
4
|
+
name: Name;
|
|
5
|
+
};
|
|
6
|
+
export type Segment = RouterPattern.Segment | GroupSegment;
|
|
7
|
+
export declare function parse(pattern: string): Segment[];
|
|
8
|
+
export declare function formatSegment(seg: Segment): string;
|
|
9
|
+
export declare function format(segments: Segment[]): `/${string}`;
|