@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/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;
@@ -0,0 +1,5 @@
1
+ // https://2ality.com/2016/11/computing-tag-functions.html
2
+ export const cook = (strs, ...substs) => {
3
+ return substs.reduce((prev, cur, i) => prev + cur + strs[i + 1], strs[0]);
4
+ };
5
+ export default cook;
@@ -0,0 +1,5 @@
1
+ export const deconstruct = (strings, ...substitutions) => {
2
+ const { raw } = strings;
3
+ return { strings: [...strings], substitutions, raw };
4
+ };
5
+ export default deconstruct;
@@ -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;
@@ -0,0 +1,3 @@
1
+ export { HTTPExpression } from "./http-expression.mjs";
2
+ export { deconstruct } from "./deconstruct.mjs";
3
+ export { cook } from "./cook.mjs";