@johnhenry/letterpress 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/publish.yml +38 -0
- package/API.md +174 -0
- package/app/routes/echo/all.mjs +14 -0
- package/app/routes/index.html +20 -0
- package/app/routes/products/get.mjs +6 -0
- package/app/routes/products/post.mjs +6 -0
- package/app/routes/user/#id/get.mjs +9 -0
- package/app/routes/user/get.mjs +6 -0
- package/app/server.mjs +7 -0
- package/create-request.mjs +108 -0
- package/create-response.mjs +6 -0
- package/create-route.mjs +185 -0
- package/create-router.mjs +61 -0
- package/demo/client.mjs +173 -0
- package/demo/index.html +128 -0
- package/demo/registry.example.js +30 -0
- package/demo/server.mjs +53 -0
- package/demo/style.css +49 -0
- package/fs-router.mjs +161 -0
- package/index.mjs +8 -0
- package/license +21 -0
- package/logo.jpeg +0 -0
- package/package.json +47 -0
- package/readme.md +259 -0
- package/test/dynamic-run.mjs +68 -0
- package/test/fs-router.mjs +83 -0
- package/test/http-expression.mjs +602 -0
- package/test/index.mjs +6 -0
- package/test/req-res.mjs +289 -0
- package/test/route.mjs +116 -0
- package/test/router.mjs +213 -0
- package/tsconfig.json +12 -0
- package/types.d.ts +85 -0
- package/utility/cook.mjs +5 -0
- package/utility/deconstruct.mjs +5 -0
- package/utility/dynamic-run.mjs +30 -0
- package/utility/http-expression.mjs +158 -0
- package/utility/index.mjs +3 -0
package/types.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Represents a route handler function
|
|
2
|
+
export type Route = (
|
|
3
|
+
request: Request,
|
|
4
|
+
context?: Record<string, any>
|
|
5
|
+
) => Response | Promise<Response>;
|
|
6
|
+
|
|
7
|
+
// Extension for the Router to add endpoints
|
|
8
|
+
export type RouterExtension = {
|
|
9
|
+
endpoint: (
|
|
10
|
+
template: TemplateStringsArray,
|
|
11
|
+
...substitutions: any[]
|
|
12
|
+
) => Router;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Combines Route and RouterExtension
|
|
16
|
+
export type Router = Route & RouterExtension;
|
|
17
|
+
|
|
18
|
+
// Configuration options for a Route
|
|
19
|
+
export type RouteInit = {
|
|
20
|
+
headers?: HeadersInit | Headers;
|
|
21
|
+
status?: number;
|
|
22
|
+
statusText?: string;
|
|
23
|
+
streaming?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Middleware for Route
|
|
27
|
+
export type RouteMiddleware =
|
|
28
|
+
| RouteInit
|
|
29
|
+
| ((request: Request) => RouteInit);
|
|
30
|
+
|
|
31
|
+
// Function to create a Route
|
|
32
|
+
export type CreateRoute = (
|
|
33
|
+
init?: RouteMiddleware
|
|
34
|
+
) => (template: TemplateStringsArray, ...substitutions: any[]) => Route;
|
|
35
|
+
|
|
36
|
+
// Configuration options for a Router
|
|
37
|
+
export type RouterInit = {
|
|
38
|
+
baseUrl?: string;
|
|
39
|
+
defaultHandler?: Route;
|
|
40
|
+
errorHandler?: (
|
|
41
|
+
error: Error,
|
|
42
|
+
request: Request
|
|
43
|
+
) => Response | Promise<Response>;
|
|
44
|
+
cache?: CacheOptions;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Middleware for Router
|
|
48
|
+
export type RouterMiddleware =
|
|
49
|
+
| RouterInit
|
|
50
|
+
| ((request: Request) => RouterInit);
|
|
51
|
+
|
|
52
|
+
// Function to create a Router
|
|
53
|
+
export type CreateRouter = (init?: RouterMiddleware) => Router;
|
|
54
|
+
|
|
55
|
+
// Cache options
|
|
56
|
+
export type CacheOptions = {
|
|
57
|
+
// Add cache-related options here
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// InlineParam options
|
|
61
|
+
export type InlineParamOptions = {
|
|
62
|
+
name: string;
|
|
63
|
+
optional?: boolean;
|
|
64
|
+
type?: "string" | "number" | "boolean";
|
|
65
|
+
default?: any;
|
|
66
|
+
cast?: (value: string) => any;
|
|
67
|
+
max?: number;
|
|
68
|
+
min?: number;
|
|
69
|
+
array?: boolean;
|
|
70
|
+
delimiter?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// HeaderMatch options
|
|
74
|
+
export type HeaderMatchOptions = {
|
|
75
|
+
name: string;
|
|
76
|
+
value?: string;
|
|
77
|
+
operator?: "=" | "^=" | "$=" | "~=" | "*=" | ">" | "<" | ">=" | "<=";
|
|
78
|
+
negate?: boolean;
|
|
79
|
+
set?: string[];
|
|
80
|
+
range?: [number, number];
|
|
81
|
+
rangeInclusive?: [boolean, boolean];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type InlineParam = (options: InlineParamOptions) => string;
|
|
85
|
+
export type HeaderMatch = (options: HeaderMatchOptions) => string;
|
package/utility/cook.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import theresWaldo from "theres-waldo";
|
|
5
|
+
|
|
6
|
+
export const dynamicRun = async (code, directory) => {
|
|
7
|
+
const tempFileName = `temp-${randomUUID()}.mjs`;
|
|
8
|
+
const tempFilePath = path.join(directory, tempFileName);
|
|
9
|
+
|
|
10
|
+
let written = false;
|
|
11
|
+
try {
|
|
12
|
+
await fs.writeFile(tempFilePath, code);
|
|
13
|
+
written = true;
|
|
14
|
+
const importedModule = await import(tempFilePath);
|
|
15
|
+
// Best-effort cleanup: a failure to unlink here must not mask a
|
|
16
|
+
// successful import, nor leave the caller without its result.
|
|
17
|
+
await fs.unlink(tempFilePath).catch(() => {});
|
|
18
|
+
return importedModule;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// Only attempt cleanup if the file was actually written - otherwise
|
|
21
|
+
// (e.g. writeFile itself failed because the directory isn't writable)
|
|
22
|
+
// this unlink would throw its own ENOENT and mask the real error.
|
|
23
|
+
if (written) {
|
|
24
|
+
await fs.unlink(tempFilePath).catch(() => {});
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default dynamicRun;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export const InlineParameter = ({
|
|
2
|
+
name,
|
|
3
|
+
type = "string",
|
|
4
|
+
optional = false,
|
|
5
|
+
transform = (x) => x,
|
|
6
|
+
}) => {
|
|
7
|
+
return { type: "inlineParam", name, paramType: type, optional, transform };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const HeaderMatch = ({
|
|
11
|
+
name,
|
|
12
|
+
operator = "exists",
|
|
13
|
+
value,
|
|
14
|
+
negate = false,
|
|
15
|
+
}) => {
|
|
16
|
+
return { type: "headerMatch", name, operator, value, negate };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const HTTPExpression = (strings, ...expressions) => {
|
|
20
|
+
let method,
|
|
21
|
+
pathPattern = "",
|
|
22
|
+
headerMatchers = [];
|
|
23
|
+
let currentPart = "METHOD";
|
|
24
|
+
let buffer = "";
|
|
25
|
+
|
|
26
|
+
const processBuffer = () => {
|
|
27
|
+
if (currentPart === "METHOD") {
|
|
28
|
+
method = buffer.trim();
|
|
29
|
+
currentPart = "PATH";
|
|
30
|
+
} else if (currentPart === "PATH") {
|
|
31
|
+
pathPattern += buffer;
|
|
32
|
+
}
|
|
33
|
+
buffer = "";
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < strings.length; i++) {
|
|
37
|
+
const str = strings[i];
|
|
38
|
+
for (let j = 0; j < str.length; j++) {
|
|
39
|
+
const char = str[j];
|
|
40
|
+
if (char === " " && currentPart === "METHOD") {
|
|
41
|
+
processBuffer();
|
|
42
|
+
} else if (char === "[" && currentPart !== "HEADER_VALUE") {
|
|
43
|
+
processBuffer();
|
|
44
|
+
currentPart = "HEADERS";
|
|
45
|
+
buffer = "[";
|
|
46
|
+
} else if (char === "]" && currentPart === "HEADERS") {
|
|
47
|
+
buffer += char;
|
|
48
|
+
headerMatchers.push(parseHeaderMatcher(buffer));
|
|
49
|
+
buffer = "";
|
|
50
|
+
} else {
|
|
51
|
+
buffer += char;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
processBuffer();
|
|
56
|
+
|
|
57
|
+
if (i < expressions.length) {
|
|
58
|
+
const expr = expressions[i];
|
|
59
|
+
if (expr && expr.type === "inlineParam") {
|
|
60
|
+
pathPattern += `:${expr.name}`;
|
|
61
|
+
} else if (expr && expr.type === "headerMatch") {
|
|
62
|
+
headerMatchers.push(createHeaderMatcherFunction(expr));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pathPattern = pathPattern.trim();
|
|
68
|
+
const pathRegex = new RegExp(
|
|
69
|
+
"^" + pathPattern.replace(/:\w+/g, "([^/]+)") + "$"
|
|
70
|
+
);
|
|
71
|
+
const paramNames = (pathPattern.match(/:\w+/g) || []).map((p) => p.slice(1));
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
test(request) {
|
|
75
|
+
if (request.method !== method) return false;
|
|
76
|
+
if (!pathRegex.test(new URL(request.url).pathname)) return false;
|
|
77
|
+
return headerMatchers.every((matcher) => matcher(request.headers));
|
|
78
|
+
},
|
|
79
|
+
exec(request) {
|
|
80
|
+
if (!this.test(request)) return null;
|
|
81
|
+
const match = new URL(request.url).pathname.match(pathRegex);
|
|
82
|
+
const params = Object.fromEntries(
|
|
83
|
+
paramNames.map((name, i) => [name, match[i + 1]])
|
|
84
|
+
);
|
|
85
|
+
return { ...params, method, headers: request.headers };
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const parseHeaderMatcher = (headerString) => {
|
|
91
|
+
const match = headerString.match(/\[(!?)([^=~^$*<>]+)([=~^$*<>]+)?(.+)?\]/);
|
|
92
|
+
if (!match) {
|
|
93
|
+
throw new Error(`Invalid header matcher: ${headerString}`);
|
|
94
|
+
}
|
|
95
|
+
const [, negation, name, operator, value] = match;
|
|
96
|
+
return createHeaderMatcherFunction({
|
|
97
|
+
name: name.trim(),
|
|
98
|
+
operator: operator?.trim(),
|
|
99
|
+
value: value?.trim(),
|
|
100
|
+
negate: !!negation,
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const createHeaderMatcherFunction = ({ name, operator, value, negate }) => {
|
|
105
|
+
return (headers) => {
|
|
106
|
+
const headerValue = headers.get(name);
|
|
107
|
+
const exists = headers.has(name);
|
|
108
|
+
let match = false;
|
|
109
|
+
|
|
110
|
+
if (!operator || operator === "exists") {
|
|
111
|
+
match = exists;
|
|
112
|
+
} else if (!exists) {
|
|
113
|
+
match = false;
|
|
114
|
+
} else {
|
|
115
|
+
switch (operator) {
|
|
116
|
+
case "=":
|
|
117
|
+
match = headerValue === value;
|
|
118
|
+
break;
|
|
119
|
+
case "~=": {
|
|
120
|
+
let regex = value || "";
|
|
121
|
+
let flags = "";
|
|
122
|
+
if (regex.startsWith("(?i)")) {
|
|
123
|
+
regex = regex.slice(4);
|
|
124
|
+
flags = "i";
|
|
125
|
+
}
|
|
126
|
+
match = new RegExp(regex, flags).test(headerValue);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "^=":
|
|
130
|
+
match = headerValue.startsWith(value || "");
|
|
131
|
+
break;
|
|
132
|
+
case "$=":
|
|
133
|
+
match = headerValue.endsWith(value || "");
|
|
134
|
+
break;
|
|
135
|
+
case "*=":
|
|
136
|
+
match = headerValue.includes(value || "");
|
|
137
|
+
break;
|
|
138
|
+
case ">":
|
|
139
|
+
match = parseFloat(headerValue) > parseFloat(value || "0");
|
|
140
|
+
break;
|
|
141
|
+
case "<":
|
|
142
|
+
match = parseFloat(headerValue) < parseFloat(value || "0");
|
|
143
|
+
break;
|
|
144
|
+
case ">=":
|
|
145
|
+
match = parseFloat(headerValue) >= parseFloat(value || "0");
|
|
146
|
+
break;
|
|
147
|
+
case "<=":
|
|
148
|
+
match = parseFloat(headerValue) <= parseFloat(value || "0");
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
match = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return negate ? !match : match;
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default HTTPExpression;
|