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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as RouterPattern from "./RouterPattern.js";
|
|
2
|
+
export function parse(pattern) {
|
|
3
|
+
const trimmedPath = pattern.replace(/(^\/)|(\/$)/g, "");
|
|
4
|
+
if (trimmedPath === "") {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
const segmentStrings = trimmedPath
|
|
8
|
+
.split("/")
|
|
9
|
+
.filter(s => s !== "");
|
|
10
|
+
if (segmentStrings.length === 0) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const segments = segmentStrings.map((s) => {
|
|
14
|
+
// (group) - Groups (FileRouter-specific)
|
|
15
|
+
const groupMatch = s.match(/^\((\w+)\)$/);
|
|
16
|
+
if (groupMatch) {
|
|
17
|
+
return { _tag: "GroupSegment", name: groupMatch[1] };
|
|
18
|
+
}
|
|
19
|
+
// Delegate to RouterPattern for all other segment types
|
|
20
|
+
return RouterPattern.parseSegment(s);
|
|
21
|
+
});
|
|
22
|
+
if (segments.some((seg) => seg === null)) {
|
|
23
|
+
throw new Error(`Invalid path segment in "${pattern}": contains invalid characters or format`);
|
|
24
|
+
}
|
|
25
|
+
return segments;
|
|
26
|
+
}
|
|
27
|
+
export function formatSegment(seg) {
|
|
28
|
+
if (seg._tag === "GroupSegment")
|
|
29
|
+
return `(${seg.name})`;
|
|
30
|
+
return RouterPattern.formatSegment(seg);
|
|
31
|
+
}
|
|
32
|
+
export function format(segments) {
|
|
33
|
+
const joined = segments.map(formatSegment).join("/");
|
|
34
|
+
return (joined ? `/${joined}` : "/");
|
|
35
|
+
}
|
package/dist/Http.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
|
|
2
|
+
type Respondable = Response | Promise<Response>;
|
|
3
|
+
export type WebHandler = (request: Request) => Respondable;
|
|
4
|
+
export type WebMiddleware = (request: Request, next: WebHandler) => Respondable;
|
|
5
|
+
export declare function fetch(handler: WebHandler, init: Omit<RequestInit, "body"> & ({
|
|
6
|
+
url: string;
|
|
7
|
+
} | {
|
|
8
|
+
path: `/${string}`;
|
|
9
|
+
}) & {
|
|
10
|
+
body?: RequestInit["body"] | Record<string, unknown>;
|
|
11
|
+
}): Promise<Response>;
|
|
12
|
+
export declare function createAbortableRequest(init: Omit<RequestInit, "signal"> & ({
|
|
13
|
+
url: string;
|
|
14
|
+
} | {
|
|
15
|
+
path: `/${string}`;
|
|
16
|
+
})): {
|
|
17
|
+
request: Request;
|
|
18
|
+
abort: () => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function mapHeaders(headers: Headers): Record<string, string | undefined>;
|
|
21
|
+
export declare function parseCookies(cookieHeader: string | null): Record<string, string | undefined>;
|
|
22
|
+
export declare function mapUrlSearchParams(params: URLSearchParams): Record<string, string | ReadonlyArray<string> | undefined>;
|
|
23
|
+
export interface FilePart {
|
|
24
|
+
readonly _tag: "File";
|
|
25
|
+
readonly key: string;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly contentType: string;
|
|
28
|
+
readonly content: Uint8Array;
|
|
29
|
+
}
|
|
30
|
+
export interface FieldPart {
|
|
31
|
+
readonly _tag: "Field";
|
|
32
|
+
readonly key: string;
|
|
33
|
+
readonly value: string;
|
|
34
|
+
}
|
|
35
|
+
export type MultipartPart = FilePart | FieldPart;
|
|
36
|
+
export declare function parseFormData(request: Request): Promise<Record<string, ReadonlyArray<FilePart> | ReadonlyArray<string> | string>>;
|
|
37
|
+
export {};
|
package/dist/Http.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as Values from "./Values.js";
|
|
2
|
+
export function fetch(handler, init) {
|
|
3
|
+
const url = "path" in init
|
|
4
|
+
? `http://localhost${init.path}`
|
|
5
|
+
: init.url;
|
|
6
|
+
const isPlain = Values.isPlainObject(init.body);
|
|
7
|
+
const headers = new Headers(init.headers);
|
|
8
|
+
if (isPlain && !headers.has("Content-Type")) {
|
|
9
|
+
headers.set("Content-Type", "application/json");
|
|
10
|
+
}
|
|
11
|
+
const body = isPlain ? JSON.stringify(init.body) : init.body;
|
|
12
|
+
const request = new Request(url, {
|
|
13
|
+
...init,
|
|
14
|
+
headers,
|
|
15
|
+
body: body,
|
|
16
|
+
});
|
|
17
|
+
return Promise.resolve(handler(request));
|
|
18
|
+
}
|
|
19
|
+
export function createAbortableRequest(init) {
|
|
20
|
+
const url = "path" in init
|
|
21
|
+
? `http://localhost${init.path}`
|
|
22
|
+
: init.url;
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const request = new Request(url, { ...init, signal: controller.signal });
|
|
25
|
+
return { request, abort: () => controller.abort() };
|
|
26
|
+
}
|
|
27
|
+
export function mapHeaders(headers) {
|
|
28
|
+
const result = {};
|
|
29
|
+
headers.forEach((value, key) => {
|
|
30
|
+
result[key.toLowerCase()] = value;
|
|
31
|
+
});
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
export function parseCookies(cookieHeader) {
|
|
35
|
+
if (!cookieHeader)
|
|
36
|
+
return {};
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const part of cookieHeader.split(";")) {
|
|
39
|
+
const idx = part.indexOf("=");
|
|
40
|
+
if (idx === -1) {
|
|
41
|
+
// Cookie without value (e.g., "name" or just whitespace)
|
|
42
|
+
const key = part.trim();
|
|
43
|
+
if (key) {
|
|
44
|
+
result[key] = undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const key = part.slice(0, idx).trim();
|
|
49
|
+
const value = part.slice(idx + 1).trim();
|
|
50
|
+
if (key) {
|
|
51
|
+
result[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
export function mapUrlSearchParams(params) {
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const key of new Set(params.keys())) {
|
|
60
|
+
const values = params.getAll(key);
|
|
61
|
+
result[key] = values.length === 1 ? values[0] : values;
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
export async function parseFormData(request) {
|
|
66
|
+
const formData = await request.formData();
|
|
67
|
+
const result = {};
|
|
68
|
+
for (const key of new Set(formData.keys())) {
|
|
69
|
+
const values = formData.getAll(key);
|
|
70
|
+
const first = values[0];
|
|
71
|
+
if (typeof first === "string") {
|
|
72
|
+
result[key] = values.length === 1 ? first : values;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const files = [];
|
|
76
|
+
for (const value of values) {
|
|
77
|
+
if (typeof value !== "string") {
|
|
78
|
+
const content = new Uint8Array(await value.arrayBuffer());
|
|
79
|
+
files.push({
|
|
80
|
+
_tag: "File",
|
|
81
|
+
key,
|
|
82
|
+
name: value.name,
|
|
83
|
+
contentType: value.type || "application/octet-stream",
|
|
84
|
+
content,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
result[key] = files;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { HttpServerRequest, HttpServerResponse } from "@effect/platform";
|
|
2
|
+
import * as HttpApp from "@effect/platform/HttpApp";
|
|
3
|
+
import { RouteNotFound } from "@effect/platform/HttpServerError";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
export declare const renderError: (error: unknown, accept?: string) => Effect.Effect<HttpServerResponse.HttpServerResponse, never, HttpServerRequest.HttpServerRequest>;
|
|
6
|
+
export declare function handleErrors<E, R>(app: HttpApp.Default<E, R>): HttpApp.Default<Exclude<E, RouteNotFound>, R | HttpServerRequest.HttpServerRequest>;
|
|
7
|
+
export declare const withErrorHandled: <E, R>(app: HttpApp.Default<E, R>) => Effect.Effect<HttpServerResponse.HttpServerResponse, never, HttpServerRequest.HttpServerRequest | R>;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { HttpServerRequest, HttpServerResponse, } from "@effect/platform";
|
|
2
|
+
import * as HttpMiddleware from "@effect/platform/HttpMiddleware";
|
|
3
|
+
import { Cause, Effect, Option, ParseResult, Record, } from "effect";
|
|
4
|
+
/**
|
|
5
|
+
* Groups: function, path
|
|
6
|
+
*/
|
|
7
|
+
const StackLinePattern = /^at (.*?) \((.*?)\)/;
|
|
8
|
+
const ERROR_PAGE_CSS = `
|
|
9
|
+
:root {
|
|
10
|
+
--error-red: #c00;
|
|
11
|
+
--error-red-dark: #a00;
|
|
12
|
+
--bg-error: #fee;
|
|
13
|
+
--bg-light: #f5f5f5;
|
|
14
|
+
--bg-white: #fff;
|
|
15
|
+
--border-color: #ddd;
|
|
16
|
+
--text-dark: #333;
|
|
17
|
+
--text-gray: #666;
|
|
18
|
+
--text-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
|
|
23
|
+
body {
|
|
24
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 0;
|
|
27
|
+
color: var(--text-dark);
|
|
28
|
+
line-height: 1.6;
|
|
29
|
+
min-height: 100dvh;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.error-page { width: 100%; margin: 0; }
|
|
33
|
+
|
|
34
|
+
.error-header {
|
|
35
|
+
background: var(--error-red);
|
|
36
|
+
color: white;
|
|
37
|
+
padding: 2rem 2.5rem;
|
|
38
|
+
margin: 0;
|
|
39
|
+
font-family: var(--text-mono);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.error-header h1 {
|
|
43
|
+
margin: 0 0 0.5rem 0;
|
|
44
|
+
font-size: 2rem;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
font-family: var(--text-mono);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.error-message {
|
|
50
|
+
margin: 0;
|
|
51
|
+
font-size: 1.1rem;
|
|
52
|
+
opacity: 0.95;
|
|
53
|
+
font-family: var(--text-mono);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.error-content {
|
|
57
|
+
background: var(--bg-white);
|
|
58
|
+
padding: 2rem 2.5rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.stack-trace {
|
|
62
|
+
margin: 1.5rem 0;
|
|
63
|
+
border: 1px solid var(--border-color);
|
|
64
|
+
border-radius: 4px;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.stack-trace-header {
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
padding: 0.75rem 1rem;
|
|
71
|
+
background: var(--bg-light);
|
|
72
|
+
border-bottom: 1px solid var(--border-color);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.stack-list {
|
|
76
|
+
list-style: none;
|
|
77
|
+
padding: 0;
|
|
78
|
+
margin: 0;
|
|
79
|
+
max-height: 400px;
|
|
80
|
+
overflow-y: auto;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.stack-list li {
|
|
84
|
+
padding: 0.5rem 1rem;
|
|
85
|
+
font-family: var(--text-mono);
|
|
86
|
+
font-size: 0.875rem;
|
|
87
|
+
border-bottom: 1px solid var(--border-color);
|
|
88
|
+
background: var(--bg-white);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.stack-list li:last-child { border-bottom: none; }
|
|
92
|
+
|
|
93
|
+
.stack-list li:hover { background: #fafafa; }
|
|
94
|
+
|
|
95
|
+
.stack-list code {
|
|
96
|
+
background: transparent;
|
|
97
|
+
padding: 0;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
color: var(--error-red-dark);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.stack-list .path { color: var(--text-gray); margin-left: 0.5rem; }
|
|
103
|
+
|
|
104
|
+
.request-info {
|
|
105
|
+
margin: 1.5rem 0;
|
|
106
|
+
border: 1px solid var(--border-color);
|
|
107
|
+
border-radius: 4px;
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.request-info-header {
|
|
112
|
+
font-weight: 700;
|
|
113
|
+
padding: 0.75rem 1rem;
|
|
114
|
+
background: var(--bg-light);
|
|
115
|
+
border-bottom: 1px solid var(--border-color);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.request-info-content {
|
|
119
|
+
padding: 1rem;
|
|
120
|
+
font-family: var(--text-mono);
|
|
121
|
+
font-size: 0.875rem;
|
|
122
|
+
white-space: pre-wrap;
|
|
123
|
+
word-break: break-all;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@media (max-width: 768px) {
|
|
127
|
+
.error-header, .error-content { padding: 1.5rem 1rem; }
|
|
128
|
+
.error-header h1 { font-size: 1.5rem; }
|
|
129
|
+
}
|
|
130
|
+
`;
|
|
131
|
+
function errorHtml(data) {
|
|
132
|
+
let detailsHtml = "";
|
|
133
|
+
if (data.details) {
|
|
134
|
+
const detailsObj = data.details;
|
|
135
|
+
if ("stack" in detailsObj && Array.isArray(detailsObj.stack)) {
|
|
136
|
+
const stackFrames = detailsObj.stack;
|
|
137
|
+
detailsHtml = renderStackTrace(stackFrames);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const requestHtml = data.requestContext
|
|
141
|
+
? renderRequestContext(data.requestContext)
|
|
142
|
+
: "";
|
|
143
|
+
const messageHtml = data.message
|
|
144
|
+
? `<p class="error-message">${escapeHtml(data.message)}</p>`
|
|
145
|
+
: "";
|
|
146
|
+
const headerTitle = data.errorName ?? "UnexpectedError";
|
|
147
|
+
const html = `<!DOCTYPE html>
|
|
148
|
+
<html lang="en">
|
|
149
|
+
<head>
|
|
150
|
+
<meta charset="UTF-8">
|
|
151
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
152
|
+
<title>${headerTitle} - Error ${data.status}</title>
|
|
153
|
+
<style>${ERROR_PAGE_CSS}</style>
|
|
154
|
+
</head>
|
|
155
|
+
<body>
|
|
156
|
+
<div class="error-header">
|
|
157
|
+
<h1>${escapeHtml(headerTitle)}</h1>
|
|
158
|
+
${messageHtml}
|
|
159
|
+
</div>
|
|
160
|
+
<div class="error-content">
|
|
161
|
+
${detailsHtml}
|
|
162
|
+
${requestHtml}
|
|
163
|
+
</div>
|
|
164
|
+
</body>
|
|
165
|
+
</html>`;
|
|
166
|
+
return HttpServerResponse.text(html, {
|
|
167
|
+
status: data.status,
|
|
168
|
+
contentType: "text/html",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function errorText(status, tag, details) {
|
|
172
|
+
const text = details ? `${tag}\n${JSON.stringify(details, null, 2)}` : tag;
|
|
173
|
+
return HttpServerResponse.text(text, { status });
|
|
174
|
+
}
|
|
175
|
+
function respondWithError(accept, status, tag, message, details, requestContext, errorName) {
|
|
176
|
+
if (accept.includes("text/html")) {
|
|
177
|
+
return errorHtml({
|
|
178
|
+
status,
|
|
179
|
+
tag,
|
|
180
|
+
message,
|
|
181
|
+
details,
|
|
182
|
+
requestContext,
|
|
183
|
+
errorName,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (accept.includes("text/plain")) {
|
|
187
|
+
return errorText(status, tag, details);
|
|
188
|
+
}
|
|
189
|
+
return HttpServerResponse.unsafeJson({ error: { _tag: tag, ...details } }, { status });
|
|
190
|
+
}
|
|
191
|
+
export const renderError = (error, accept = "") => Effect.gen(function* () {
|
|
192
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
193
|
+
const requestContext = {
|
|
194
|
+
url: request.url,
|
|
195
|
+
method: request.method,
|
|
196
|
+
headers: filterSensitiveHeaders(request.headers),
|
|
197
|
+
};
|
|
198
|
+
let unwrappedError;
|
|
199
|
+
if (Cause.isCause(error)) {
|
|
200
|
+
const failure = Cause.failureOption(error).pipe(Option.getOrUndefined);
|
|
201
|
+
if (failure?.["_tag"]) {
|
|
202
|
+
unwrappedError = failure;
|
|
203
|
+
}
|
|
204
|
+
yield* Effect.logError(error);
|
|
205
|
+
}
|
|
206
|
+
switch (unwrappedError?._tag) {
|
|
207
|
+
case "RouteNotFound":
|
|
208
|
+
return respondWithError(accept, 404, "RouteNotFound", "The page you were looking for doesn't exist", undefined, requestContext);
|
|
209
|
+
case "RequestError": {
|
|
210
|
+
const message = unwrappedError.reason === "Decode"
|
|
211
|
+
? "Request body is invalid"
|
|
212
|
+
: "Request could not be processed";
|
|
213
|
+
return respondWithError(accept, 400, "RequestError", message, {
|
|
214
|
+
reason: unwrappedError.reason,
|
|
215
|
+
}, requestContext);
|
|
216
|
+
}
|
|
217
|
+
case "ParseError": {
|
|
218
|
+
const issues = yield* ParseResult.ArrayFormatter.formatIssue(unwrappedError.issue);
|
|
219
|
+
const cleanIssues = issues.map((v) => Record.remove(v, "_tag"));
|
|
220
|
+
return respondWithError(accept, 400, "ParseError", "Validation failed", {
|
|
221
|
+
issues: cleanIssues,
|
|
222
|
+
}, requestContext);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (Cause.isCause(error)) {
|
|
226
|
+
const defects = [...Cause.defects(error)];
|
|
227
|
+
const defect = defects[0];
|
|
228
|
+
if (defect instanceof Error) {
|
|
229
|
+
const stackFrames = extractPrettyStack(defect.stack ?? "");
|
|
230
|
+
return respondWithError(accept, 500, "UnexpectedError", defect.message, {
|
|
231
|
+
name: defect.name,
|
|
232
|
+
stack: stackFrames,
|
|
233
|
+
}, requestContext, defect.name);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return respondWithError(accept, 500, "UnexpectedError", "An unexpected error occurred", undefined, requestContext, "UnexpectedError");
|
|
237
|
+
});
|
|
238
|
+
function parseStackFrame(line) {
|
|
239
|
+
const match = line.trim().match(StackLinePattern);
|
|
240
|
+
if (!match)
|
|
241
|
+
return null;
|
|
242
|
+
const [_, fn, fullPath] = match;
|
|
243
|
+
const relativePath = fullPath.replace(process.cwd(), ".");
|
|
244
|
+
let type;
|
|
245
|
+
if (relativePath.includes("node_modules")) {
|
|
246
|
+
type = "node_modules";
|
|
247
|
+
}
|
|
248
|
+
else if (relativePath.startsWith("./src")
|
|
249
|
+
|| relativePath.startsWith("./examples")) {
|
|
250
|
+
type = "application";
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
type = "framework";
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
function: fn,
|
|
257
|
+
file: relativePath,
|
|
258
|
+
type,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function extractPrettyStack(stack) {
|
|
262
|
+
return stack
|
|
263
|
+
.split("\n")
|
|
264
|
+
.slice(1)
|
|
265
|
+
.map(parseStackFrame)
|
|
266
|
+
.filter((frame) => frame !== null);
|
|
267
|
+
}
|
|
268
|
+
function renderStackFrames(frames) {
|
|
269
|
+
if (frames.length === 0) {
|
|
270
|
+
return "<li>No stack frames</li>";
|
|
271
|
+
}
|
|
272
|
+
return frames
|
|
273
|
+
.map((f) => `<li><code>${f.function}</code> at <span class="path">${f.file}</span></li>`)
|
|
274
|
+
.join("");
|
|
275
|
+
}
|
|
276
|
+
function renderStackTrace(frames) {
|
|
277
|
+
return `
|
|
278
|
+
<div class="stack-trace">
|
|
279
|
+
<div class="stack-trace-header">Stack Trace (${frames.length})</div>
|
|
280
|
+
<ul class="stack-list">${renderStackFrames(frames)}</ul>
|
|
281
|
+
</div>
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
function escapeHtml(unsafe) {
|
|
285
|
+
return unsafe
|
|
286
|
+
.replace(/&/g, "&")
|
|
287
|
+
.replace(/</g, "<")
|
|
288
|
+
.replace(/>/g, ">")
|
|
289
|
+
.replace(/"/g, """)
|
|
290
|
+
.replace(/'/g, "'");
|
|
291
|
+
}
|
|
292
|
+
function filterSensitiveHeaders(headers) {
|
|
293
|
+
const sensitive = ["authorization", "cookie", "x-api-key"];
|
|
294
|
+
return Object.fromEntries(Object.entries(headers).filter(([key]) => !sensitive.includes(key.toLowerCase())));
|
|
295
|
+
}
|
|
296
|
+
function renderRequestContext(context) {
|
|
297
|
+
const headersText = Object
|
|
298
|
+
.entries(context.headers)
|
|
299
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
300
|
+
.join("\n");
|
|
301
|
+
const requestText = `${context.method} ${context.url}\n${headersText}`;
|
|
302
|
+
return `
|
|
303
|
+
<div class="request-info">
|
|
304
|
+
<div class="request-info-header">Request</div>
|
|
305
|
+
<div class="request-info-content">${escapeHtml(requestText)}</div>
|
|
306
|
+
</div>
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
export function handleErrors(app) {
|
|
310
|
+
return Effect.gen(function* () {
|
|
311
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
312
|
+
const accept = request.headers.accept ?? "";
|
|
313
|
+
return yield* app.pipe(Effect.catchAllCause((cause) => renderError(cause, accept)));
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
export const withErrorHandled = HttpMiddleware.make(app => Effect.gen(function* () {
|
|
317
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
318
|
+
const accept = request.headers.accept ?? "";
|
|
319
|
+
return yield* app.pipe(Effect.catchAllCause((cause) => renderError(cause, accept)));
|
|
320
|
+
}));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function makeUrlFromRequest(request) {
|
|
2
|
+
const origin = request.headers.origin
|
|
3
|
+
?? request.headers.host
|
|
4
|
+
?? "http://localhost";
|
|
5
|
+
const protocol = request.headers["x-forwarded-proto"] ?? "http";
|
|
6
|
+
const host = request.headers.host ?? "localhost";
|
|
7
|
+
const base = origin.startsWith("http")
|
|
8
|
+
? origin
|
|
9
|
+
: `${protocol}://${host}`;
|
|
10
|
+
return new URL(request.url, base);
|
|
11
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export type PathPattern = `/${string}`;
|
|
2
|
+
export type Segments<Path extends string> = Path extends `/${infer Rest}` ? Segments<Rest> : Path extends `${infer Head}/${infer Tail}` ? [Head, ...Segments<Tail>] : Path extends "" ? [] : [Path];
|
|
3
|
+
export type Params<T extends string> = string extends T ? Record<string, string> : T extends `${infer _Start}:${infer Param}?/${infer Rest}` ? {
|
|
4
|
+
[K in Param]?: string;
|
|
5
|
+
} & Params<`/${Rest}`> : T extends `${infer _Start}:${infer Param}/${infer Rest}` ? {
|
|
6
|
+
[K in Param]: string;
|
|
7
|
+
} & Params<`/${Rest}`> : T extends `${infer _Start}:${infer Param}+` ? {
|
|
8
|
+
[K in Param]: string;
|
|
9
|
+
} : T extends `${infer _Start}:${infer Param}*` ? {
|
|
10
|
+
[K in Param]?: string;
|
|
11
|
+
} : T extends `${infer _Start}:${infer Param}?` ? {
|
|
12
|
+
[K in Param]?: string;
|
|
13
|
+
} : T extends `${infer _Start}:${infer Param}` ? {
|
|
14
|
+
[K in Param]: string;
|
|
15
|
+
} : {};
|
|
16
|
+
export type ValidateResult = {
|
|
17
|
+
ok: true;
|
|
18
|
+
segments: string[];
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function validate(path: string): ValidateResult;
|
|
24
|
+
export declare function match(pattern: string, path: string): Record<string, string> | null;
|
|
25
|
+
export declare function toRegex(path: string): RegExp;
|
|
26
|
+
/**
|
|
27
|
+
* Converts to Express path pattern.
|
|
28
|
+
*
|
|
29
|
+
* @see https://expressjs.com/en/guide/routing.html
|
|
30
|
+
*
|
|
31
|
+
* - `:param` → `:param`
|
|
32
|
+
* - `:param?` → `{/:param}`
|
|
33
|
+
* - `:param+` → `/*param`
|
|
34
|
+
* - `:param*` → `/`, `/*param`
|
|
35
|
+
*/
|
|
36
|
+
export declare function toExpress(path: string): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Converts to URLPattern path pattern.
|
|
39
|
+
*
|
|
40
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API
|
|
41
|
+
*
|
|
42
|
+
* - `:param` → `:param`
|
|
43
|
+
* - `:param?` → `:param?`
|
|
44
|
+
* - `:param+` → `:param+`
|
|
45
|
+
* - `:param*` → `:param*`
|
|
46
|
+
*/
|
|
47
|
+
export declare function toURLPattern(path: string): string[];
|
|
48
|
+
/**
|
|
49
|
+
* Converts to React Router path pattern.
|
|
50
|
+
*
|
|
51
|
+
* @see https://reactrouter.com/start/framework/routing
|
|
52
|
+
*
|
|
53
|
+
* - `:param` → `:param`
|
|
54
|
+
* - `:param?` → `:param?`
|
|
55
|
+
* - `:param+` → `*` (splat, required)
|
|
56
|
+
* - `:param*` → `/`, `/*` (splat, optional - two routes)
|
|
57
|
+
*/
|
|
58
|
+
export declare function toReactRouter(path: string): string[];
|
|
59
|
+
/**
|
|
60
|
+
* Alias for toReactRouter.
|
|
61
|
+
*
|
|
62
|
+
* @see https://reactrouter.com/start/framework/routing
|
|
63
|
+
*/
|
|
64
|
+
export declare const toRemix: typeof toReactRouter;
|
|
65
|
+
/**
|
|
66
|
+
* Converts to Remix file-based route naming convention.
|
|
67
|
+
*
|
|
68
|
+
* Returns a file path segment (without extension) for Remix's
|
|
69
|
+
* flat file routing convention.
|
|
70
|
+
*
|
|
71
|
+
* @see https://remix.run/docs/file-conventions/routes
|
|
72
|
+
*
|
|
73
|
+
* - `:param` → `$param`
|
|
74
|
+
* - `:param?` → `($param)`
|
|
75
|
+
* - `:param+` → `$` (splat)
|
|
76
|
+
* - `:param*` → `($)` (optional splat) - Note: not officially supported
|
|
77
|
+
*/
|
|
78
|
+
export declare function toRemixFile(path: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Converts to TanStack Router path/file pattern.
|
|
81
|
+
*
|
|
82
|
+
* TanStack uses the same `$param` syntax for both route paths and file names.
|
|
83
|
+
* Returns a dot-separated file name (without extension).
|
|
84
|
+
*
|
|
85
|
+
* @see https://tanstack.com/router/v1/docs/framework/react/guide/path-params
|
|
86
|
+
* @see https://tanstack.com/router/v1/docs/framework/react/routing/file-naming-conventions
|
|
87
|
+
*
|
|
88
|
+
* - `:param` → `$param`
|
|
89
|
+
* - `:param?` → `{-$param}` (optional segment)
|
|
90
|
+
* - `:param+` → `$` (splat)
|
|
91
|
+
* - `:param*` → `$` (splat, optional not supported - treated as required)
|
|
92
|
+
*/
|
|
93
|
+
export declare function toTanStack(path: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Converts to Hono path pattern.
|
|
96
|
+
*
|
|
97
|
+
* Hono uses unnamed wildcards - they are NOT accessible via c.req.param().
|
|
98
|
+
* Use c.req.path to access the matched path for wildcard routes.
|
|
99
|
+
*
|
|
100
|
+
* @see https://hono.dev/docs/api/routing
|
|
101
|
+
*
|
|
102
|
+
* - `:param` → `:param`
|
|
103
|
+
* - `:param?` → `:param?`
|
|
104
|
+
* - `:param+` → `*` (unnamed, required)
|
|
105
|
+
* - `:param*` → `/`, `/*` (unnamed, optional - two routes)
|
|
106
|
+
*/
|
|
107
|
+
export declare function toHono(path: string): string[];
|
|
108
|
+
/**
|
|
109
|
+
* Converts to Effect HttpRouter / find-my-way path pattern.
|
|
110
|
+
*
|
|
111
|
+
* Effect uses colon-style params with unnamed wildcards.
|
|
112
|
+
*
|
|
113
|
+
* @see https://effect.website/docs/platform/http-router
|
|
114
|
+
*
|
|
115
|
+
* - `:param` → `:param`
|
|
116
|
+
* - `:param?` → `:param?`
|
|
117
|
+
* - `:param+` → `*` (unnamed)
|
|
118
|
+
* - `:param*` → `/`, `/*` (two routes)
|
|
119
|
+
*/
|
|
120
|
+
export declare function toEffect(path: string): string[];
|
|
121
|
+
/**
|
|
122
|
+
* Converts to Bun.serve path pattern.
|
|
123
|
+
*
|
|
124
|
+
* Since Bun doesn't support optional params (`:param?`), optional segments
|
|
125
|
+
* are expanded into multiple routes.
|
|
126
|
+
*
|
|
127
|
+
* @see https://bun.sh/docs/api/http#routing
|
|
128
|
+
*
|
|
129
|
+
* - `:param` → `:param`
|
|
130
|
+
* - `:param?` → `/`, `/:param` (two routes)
|
|
131
|
+
* - `:param+` → `/*`
|
|
132
|
+
* - `:param*` → `/`, `/*` (two routes)
|
|
133
|
+
*/
|
|
134
|
+
export declare function toBun(path: string): PathPattern[];
|