@johnhenry/letterpress 0.0.3 → 0.1.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/CHANGELOG.md +56 -0
- package/README.md +65 -0
- package/create-router.mjs +50 -3
- package/package.json +6 -2
- package/rewrite.d.ts +61 -0
- package/rewrite.mjs +176 -0
- package/test/exports.mjs +28 -0
- package/test/index.mjs +1 -0
- package/test/rewrite.mjs +202 -0
- package/test/router.mjs +81 -0
- package/test/types/fixture.ts +34 -0
- package/tsconfig.json +1 -0
- package/types.d.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,62 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
|
6
6
|
|
|
7
7
|
This project will adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once it reaches 1.0.0.
|
|
8
8
|
|
|
9
|
+
## [0.1.0] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- ✨ **`createRewriter`**: `leroute` (the package letterpress was renamed
|
|
14
|
+
from -- see the README's provenance note) had a `createRewriter` export
|
|
15
|
+
(`leroute/rewrite`) for request/response rewriting: rewriting an inbound
|
|
16
|
+
request's target path/headers before it reaches a handler, and rewriting
|
|
17
|
+
a handler's response headers/status on the way back out. This never got
|
|
18
|
+
carried over during the leroute -> `@johnhenry/letterpress` rename --
|
|
19
|
+
it simply wasn't in the initial `@johnhenry/letterpress` source, a gap
|
|
20
|
+
in that migration rather than an intentional removal. It was discovered
|
|
21
|
+
while porting a downstream consumer, `prism` (a live HTTP request
|
|
22
|
+
inspector/proxy), whose proxy mode depends on it for rewriting proxied
|
|
23
|
+
requests/responses. Ported unchanged in behavior as `rewrite.mjs`
|
|
24
|
+
(`createRewriter`, plus `rewriteRequest`/`rewriteResponse`/`addRule`/
|
|
25
|
+
`removeRule`/`getRules`/`middleware`), exposed via a dedicated
|
|
26
|
+
`@johnhenry/letterpress/rewrite` subpath -- mirroring how
|
|
27
|
+
`createFSRouter` is isolated on `@johnhenry/letterpress/fs` rather than
|
|
28
|
+
the main barrel -- with its own `rewrite.d.ts` types wired into
|
|
29
|
+
`package.json`'s `exports` map and `tsconfig.json`. Tests ported from
|
|
30
|
+
leroute's `test/rewrite.mjs` into `test/rewrite.mjs`, plus new
|
|
31
|
+
`./rewrite` subpath-export regression tests in `test/exports.mjs` and
|
|
32
|
+
`test/types/fixture.ts` coverage, following the same pattern the `./fs`
|
|
33
|
+
subpath already established.
|
|
34
|
+
|
|
35
|
+
- ✨ **`router.mount(prefix, subHandler)`**: another `leroute` capability
|
|
36
|
+
(`create-lerouter.mjs`'s `router.mount`) dropped during the same rename,
|
|
37
|
+
found via the same `prism` port -- prism's own top-level router uses it
|
|
38
|
+
to mount its dashboard sub-router under `/inspect`. Delegates every
|
|
39
|
+
request under `prefix` to `subHandler` with the prefix stripped from the
|
|
40
|
+
forwarded request's path; `subHandler` may be a plain `(Request) =>
|
|
41
|
+
Response` function or an object exposing one as `.fetch`, so a `Router`
|
|
42
|
+
can mount another `Router`. Ported onto the main barrel (`create-router.mjs`,
|
|
43
|
+
right alongside `router.endpoint`, not an isolated subpath like `rewrite`
|
|
44
|
+
-- unlike `rewrite`/`fs`, `mount` is core routing behavior every router
|
|
45
|
+
needs, not an optional extra), dropping only the old implementation's
|
|
46
|
+
now-obsolete `middleware` and `request.raw` preservation (the current
|
|
47
|
+
`createRouter` doesn't have either concept). New `Mountable` type in
|
|
48
|
+
`types.d.ts`; seven new tests in `test/router.mjs` covering delegation,
|
|
49
|
+
the bare-prefix-with-no-trailing-segment case, 404 fallthrough for
|
|
50
|
+
non-matching paths, the `.fetch`-object form, query-string/method
|
|
51
|
+
preservation on the forwarded request, and ctx forwarding (below).
|
|
52
|
+
|
|
53
|
+
- ✨ **The router's own dispatch now accepts an optional second `ctx`
|
|
54
|
+
argument** (`router(request, ctx?)`), merged underneath `init` and the
|
|
55
|
+
matched route's own params so a route's params can't be shadowed by it,
|
|
56
|
+
and forwarded to `defaultHandler` too. This was also present in `leroute`
|
|
57
|
+
(`router(request, ctx = {})`) and dropped during the rename; brought back
|
|
58
|
+
specifically so `mount()` can hand a mounted sub-router its `mountPrefix`
|
|
59
|
+
(used by `prism` to reconstruct the full request path for display in its
|
|
60
|
+
captured-request feed, purely cosmetic but a real fidelity gap otherwise)
|
|
61
|
+
-- caught by testing `mount()` end-to-end against `prism` itself, not by
|
|
62
|
+
a direct comparison against the old source this time. Existing single-arg
|
|
63
|
+
`router(request)` callers are unaffected (`ctx` defaults to `{}`).
|
|
64
|
+
|
|
9
65
|
## [0.0.3] - 2026-09-26
|
|
10
66
|
|
|
11
67
|
### Fixed
|
package/README.md
CHANGED
|
@@ -148,6 +148,23 @@ Creates a new router instance.
|
|
|
148
148
|
- `errorHandler`: Custom error handler function (optional)
|
|
149
149
|
- `cache`: Caching options (optional)
|
|
150
150
|
|
|
151
|
+
### `router.mount(prefix: string, subHandler: Mountable): Router`
|
|
152
|
+
|
|
153
|
+
Delegates every request under `prefix` to `subHandler`, with the prefix
|
|
154
|
+
stripped from the forwarded request's path — e.g. `router.mount("/api",
|
|
155
|
+
apiRouter)` sends a request for `/api/users` to `apiRouter` as `/users`.
|
|
156
|
+
`subHandler` may be a plain `(Request) => Response` function or an object
|
|
157
|
+
exposing one as `.fetch` — a `Router` satisfies both, so one router can
|
|
158
|
+
mount another:
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
const api = createRouter();
|
|
162
|
+
api.endpoint`GET /users``[]`;
|
|
163
|
+
|
|
164
|
+
const router = createRouter();
|
|
165
|
+
router.mount("/api", api); // GET /api/users -> api's GET /users
|
|
166
|
+
```
|
|
167
|
+
|
|
151
168
|
### `createRoute(options?: RouteInit): Route`
|
|
152
169
|
|
|
153
170
|
Creates a new route handler.
|
|
@@ -191,6 +208,47 @@ import { createFSRouter } from "@johnhenry/letterpress/fs";
|
|
|
191
208
|
> else (`createRouter`, `createRoute`, `createRequest`, `createResponse`,
|
|
192
209
|
> `HTTPExpression`) is pure and safe to bundle for the browser.
|
|
193
210
|
|
|
211
|
+
### `createRewriter`
|
|
212
|
+
|
|
213
|
+
Builds a request/response rewriter for target-URL/path rewriting -- e.g.
|
|
214
|
+
rewriting an inbound path before it reaches a route, or rewriting a
|
|
215
|
+
proxied response's headers/status on the way back out. Useful for proxy
|
|
216
|
+
middleware (rewrite the target URL/path of a proxied request, adjust
|
|
217
|
+
headers, force a status code) as well as plain routing (redirect old
|
|
218
|
+
paths to new ones).
|
|
219
|
+
|
|
220
|
+
```javascript
|
|
221
|
+
import { createRewriter } from "@johnhenry/letterpress/rewrite";
|
|
222
|
+
|
|
223
|
+
const rewriter = createRewriter([
|
|
224
|
+
{
|
|
225
|
+
match: { path: /^\/api\/v1\/(.*)/ },
|
|
226
|
+
action: { rewritePath: "/api/v2/$1" },
|
|
227
|
+
},
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
// Wrap a handler: the handler receives the rewritten request, and its
|
|
231
|
+
// response is run back through the rewriter before being returned.
|
|
232
|
+
const handler = rewriter.middleware(async (request) => {
|
|
233
|
+
const url = new URL(request.url);
|
|
234
|
+
return new Response(`routed to ${url.pathname}`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Or call rewriteRequest/rewriteResponse directly, and manage rules with
|
|
238
|
+
// addRule/removeRule/getRules.
|
|
239
|
+
rewriter.addRule({ match: { path: "/teapot" }, action: { setStatus: 418 } });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
> `createRewriter` is imported from the `@johnhenry/letterpress/rewrite`
|
|
243
|
+
> subpath, not the main `@johnhenry/letterpress` barrel, mirroring how
|
|
244
|
+
> `createFSRouter` is isolated on `@johnhenry/letterpress/fs` above.
|
|
245
|
+
>
|
|
246
|
+
> This existed in `leroute` (the package letterpress was renamed from --
|
|
247
|
+
> see the provenance note near the top of this README) but was dropped
|
|
248
|
+
> during that rename. It's ported here, discovered missing while porting a
|
|
249
|
+
> downstream consumer (`prism`, a request inspector/proxy) that relies on
|
|
250
|
+
> it for proxy-mode rewriting -- see CHANGELOG.md.
|
|
251
|
+
|
|
194
252
|
### `deconstruct`, `cook`
|
|
195
253
|
|
|
196
254
|
Lower-level utility functions used to parse and process tagged HTTP template literals. See [api.md](./api.md) for details.
|
|
@@ -241,6 +299,13 @@ is designed to pair with a sibling package that does.
|
|
|
241
299
|
`Route`-shaped function works too; `leserve` is the tested, documented
|
|
242
300
|
pairing, not a hard dependency (it's a `devDependency` here, used only in
|
|
243
301
|
the demo scripts).
|
|
302
|
+
- **[`@johnhenry/prism`](https://github.com/johnhenry/prism)** -- a live
|
|
303
|
+
HTTP request inspector/proxy that uses `createRouter()` as its top-level
|
|
304
|
+
router. Porting it surfaced two real gaps left over from letterpress's
|
|
305
|
+
own rename from `leroute`: `router.mount()` (with the `ctx`-forwarding
|
|
306
|
+
its `mountPrefix` depends on) and `createRewriter`
|
|
307
|
+
(`@johnhenry/letterpress/rewrite`) -- both closed as part of that port,
|
|
308
|
+
see CHANGELOG.md.
|
|
244
309
|
|
|
245
310
|
## 🤝 Contributing
|
|
246
311
|
|
package/create-router.mjs
CHANGED
|
@@ -11,7 +11,12 @@ export const createRouter = (initial = {}) => {
|
|
|
11
11
|
...init
|
|
12
12
|
} = initial;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// `ctx` is an optional caller-supplied context object (e.g. `.mount()`
|
|
15
|
+
// uses it to pass `mountPrefix` down to a mounted sub-router) merged
|
|
16
|
+
// underneath the router's own `init` and the matched route's own params --
|
|
17
|
+
// so a route's own params can't be shadowed by a mount's ctx, but a
|
|
18
|
+
// handler can still read whatever the caller passed in.
|
|
19
|
+
const router = async (request, ctx = {}) => {
|
|
15
20
|
try {
|
|
16
21
|
for (const [matcher, handler] of routes) {
|
|
17
22
|
const match = matcher(request);
|
|
@@ -20,10 +25,10 @@ export const createRouter = (initial = {}) => {
|
|
|
20
25
|
// directly) is required so that a handler that throws inside an
|
|
21
26
|
// async function (i.e. rejects its returned promise) is still
|
|
22
27
|
// caught below and routed to errorHandler.
|
|
23
|
-
return await handler(request, { ...init, ...match });
|
|
28
|
+
return await handler(request, { ...ctx, ...init, ...match });
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
|
-
return await defaultHandler(request);
|
|
31
|
+
return await defaultHandler(request, ctx);
|
|
27
32
|
} catch (error) {
|
|
28
33
|
return errorHandler(error, request);
|
|
29
34
|
}
|
|
@@ -57,5 +62,47 @@ export const createRouter = (initial = {}) => {
|
|
|
57
62
|
return router;
|
|
58
63
|
};
|
|
59
64
|
};
|
|
65
|
+
|
|
66
|
+
// Delegate every request under `prefix` to `subHandler`, with the prefix
|
|
67
|
+
// stripped from the forwarded request's path -- e.g. `router.mount("/api",
|
|
68
|
+
// apiRouter)` sends a request for `/api/users` to `apiRouter` as `/users`.
|
|
69
|
+
// `subHandler` may be a plain `(Request) => Response` function or an
|
|
70
|
+
// object exposing one as `.fetch` (matching how `Router` itself works, so
|
|
71
|
+
// one router can mount another).
|
|
72
|
+
router.mount = (prefix, subHandler) => {
|
|
73
|
+
const normalizedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
74
|
+
|
|
75
|
+
const matcher = (request) => {
|
|
76
|
+
const url = new URL(request.url);
|
|
77
|
+
if (
|
|
78
|
+
url.pathname === normalizedPrefix ||
|
|
79
|
+
url.pathname.startsWith(normalizedPrefix + "/")
|
|
80
|
+
) {
|
|
81
|
+
return { params: {}, method: request.method, headers: {} };
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handler = (request) => {
|
|
87
|
+
const url = new URL(request.url);
|
|
88
|
+
const newPath = url.pathname.slice(normalizedPrefix.length) || "/";
|
|
89
|
+
const newUrl = new URL(newPath + url.search + url.hash, url.origin);
|
|
90
|
+
|
|
91
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
92
|
+
const newRequest = new Request(newUrl.toString(), {
|
|
93
|
+
method: request.method,
|
|
94
|
+
headers: request.headers,
|
|
95
|
+
body: hasBody ? request.body : undefined,
|
|
96
|
+
duplex: hasBody ? "half" : undefined,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const sub = typeof subHandler === "function" ? subHandler : subHandler.fetch;
|
|
100
|
+
return sub(newRequest, { mountPrefix: normalizedPrefix });
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
routes.push([matcher, handler]);
|
|
104
|
+
return router;
|
|
105
|
+
};
|
|
106
|
+
|
|
60
107
|
return router;
|
|
61
108
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnhenry/letterpress",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A tagged-template-string HTTP routing library for defining and matching request handlers with plain JavaScript syntax.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"types": "types.d.ts",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"types": "./fs.d.ts",
|
|
14
14
|
"default": "./fs-router.mjs"
|
|
15
15
|
},
|
|
16
|
+
"./rewrite": {
|
|
17
|
+
"types": "./rewrite.d.ts",
|
|
18
|
+
"default": "./rewrite.mjs"
|
|
19
|
+
},
|
|
16
20
|
"./package.json": "./package.json",
|
|
17
21
|
"./*": "./*"
|
|
18
22
|
},
|
|
@@ -58,4 +62,4 @@
|
|
|
58
62
|
"dependencies": {
|
|
59
63
|
"theres-waldo": "^0.0.0"
|
|
60
64
|
}
|
|
61
|
-
}
|
|
65
|
+
}
|
package/rewrite.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Type declarations for the @johnhenry/letterpress/rewrite subpath.
|
|
2
|
+
//
|
|
3
|
+
// createRewriter is a request/response rewriter for target-URL/path
|
|
4
|
+
// rewriting (e.g. rewriting an inbound path before it reaches a route, or
|
|
5
|
+
// rewriting a proxied response's headers/status on the way back out). It
|
|
6
|
+
// existed in `leroute` (the package letterpress was renamed from) but was
|
|
7
|
+
// dropped during that rename; it's kept on its own subpath here (rather
|
|
8
|
+
// than the main barrel) to mirror how `./fs` is isolated -- see
|
|
9
|
+
// rewrite.mjs's top-of-file comment and CHANGELOG.md.
|
|
10
|
+
|
|
11
|
+
export type RewriteMatch = {
|
|
12
|
+
path?: string | RegExp;
|
|
13
|
+
method?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type RewriteAction = {
|
|
17
|
+
setHeader?: Record<string, string>;
|
|
18
|
+
removeHeader?: string[];
|
|
19
|
+
// Replacement string for the matched path; supports `$1`-style regex
|
|
20
|
+
// capture group references when `match.path` is a RegExp.
|
|
21
|
+
rewritePath?: string;
|
|
22
|
+
// Response only.
|
|
23
|
+
setStatus?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RewriteRule = {
|
|
27
|
+
match?: RewriteMatch;
|
|
28
|
+
action?: RewriteAction;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// The shape returned by getRules(): a rule with its index in the rule
|
|
32
|
+
// list, and any RegExp `match.path` normalized back to its `.source`
|
|
33
|
+
// string.
|
|
34
|
+
export type RewriteRuleSnapshot = {
|
|
35
|
+
index: number;
|
|
36
|
+
match: {
|
|
37
|
+
path?: string;
|
|
38
|
+
method?: string;
|
|
39
|
+
};
|
|
40
|
+
action?: RewriteAction;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Wraps a Route-shaped handler ((request, context) => Response) with
|
|
44
|
+
// request/response rewriting.
|
|
45
|
+
export type RewriterMiddleware = (
|
|
46
|
+
next: (request: Request, context?: Record<string, any>) => Response | Promise<Response>
|
|
47
|
+
) => (request: Request, context?: Record<string, any>) => Promise<Response>;
|
|
48
|
+
|
|
49
|
+
export type Rewriter = {
|
|
50
|
+
middleware: RewriterMiddleware;
|
|
51
|
+
rewriteRequest: (request: Request) => Request;
|
|
52
|
+
rewriteResponse: (response: Response, request?: Request) => Response;
|
|
53
|
+
addRule: (rule: RewriteRule) => void;
|
|
54
|
+
removeRule: (index: number) => void;
|
|
55
|
+
getRules: () => RewriteRuleSnapshot[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type CreateRewriter = (initialRules?: RewriteRule[]) => Rewriter;
|
|
59
|
+
|
|
60
|
+
export declare const createRewriter: CreateRewriter;
|
|
61
|
+
export default createRewriter;
|
package/rewrite.mjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file rewrite.mjs
|
|
3
|
+
* @description Create a request/response rewriter for target-URL/path
|
|
4
|
+
* rewriting -- e.g. rewriting an inbound path before it reaches a route, or
|
|
5
|
+
* rewriting a proxied response's headers/status on the way back out.
|
|
6
|
+
*
|
|
7
|
+
* This existed in `leroute` (the package letterpress was renamed from --
|
|
8
|
+
* see the README's provenance note) as `createRewriter`, but was dropped
|
|
9
|
+
* during the leroute -> @johnhenry/letterpress rename. It's ported here
|
|
10
|
+
* unchanged in behavior, discovered missing while porting a downstream
|
|
11
|
+
* consumer (`prism`, a request inspector/proxy) that relies on it for
|
|
12
|
+
* proxy-mode rewriting.
|
|
13
|
+
*
|
|
14
|
+
* Rules format:
|
|
15
|
+
* {
|
|
16
|
+
* match: { path?: string|RegExp, method?: string },
|
|
17
|
+
* action: {
|
|
18
|
+
* setHeader?: { [name]: value },
|
|
19
|
+
* removeHeader?: string[],
|
|
20
|
+
* rewritePath?: string, // replacement string (supports $1 from regex)
|
|
21
|
+
* setStatus?: number, // response only
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* @param {Array} [initialRules=[]]
|
|
26
|
+
*/
|
|
27
|
+
export const createRewriter = (initialRules = []) => {
|
|
28
|
+
const rules = [...initialRules];
|
|
29
|
+
|
|
30
|
+
const matches = (rule, request) => {
|
|
31
|
+
if (!rule.match) return true;
|
|
32
|
+
const url = new URL(request.url);
|
|
33
|
+
if (rule.match.method && rule.match.method.toUpperCase() !== request.method) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (rule.match.path) {
|
|
37
|
+
const pattern = rule.match.path;
|
|
38
|
+
if (pattern instanceof RegExp) {
|
|
39
|
+
return pattern.test(url.pathname);
|
|
40
|
+
}
|
|
41
|
+
// String match — treat as prefix or exact
|
|
42
|
+
if (!url.pathname.startsWith(pattern)) return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const rewriteRequest = (request) => {
|
|
48
|
+
let url = new URL(request.url);
|
|
49
|
+
let headers = new Headers(request.headers);
|
|
50
|
+
let pathChanged = false;
|
|
51
|
+
|
|
52
|
+
for (const rule of rules) {
|
|
53
|
+
if (!matches(rule, request)) continue;
|
|
54
|
+
const action = rule.action || {};
|
|
55
|
+
|
|
56
|
+
if (action.setHeader) {
|
|
57
|
+
for (const [name, value] of Object.entries(action.setHeader)) {
|
|
58
|
+
headers.set(name, value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (action.removeHeader) {
|
|
62
|
+
for (const name of action.removeHeader) {
|
|
63
|
+
headers.delete(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (action.rewritePath) {
|
|
67
|
+
if (rule.match?.path instanceof RegExp) {
|
|
68
|
+
url.pathname = url.pathname.replace(rule.match.path, action.rewritePath);
|
|
69
|
+
} else {
|
|
70
|
+
url.pathname = action.rewritePath;
|
|
71
|
+
}
|
|
72
|
+
pathChanged = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (pathChanged || !headersEqual(headers, request.headers)) {
|
|
77
|
+
return new Request(url, {
|
|
78
|
+
method: request.method,
|
|
79
|
+
headers,
|
|
80
|
+
body: request.body,
|
|
81
|
+
duplex: "half",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return request;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const rewriteResponse = (response, request) => {
|
|
88
|
+
let headers = new Headers(response.headers);
|
|
89
|
+
let status = response.status;
|
|
90
|
+
let statusText = response.statusText;
|
|
91
|
+
let changed = false;
|
|
92
|
+
|
|
93
|
+
for (const rule of rules) {
|
|
94
|
+
// When request is provided, filter rules through matches()
|
|
95
|
+
// Rules with no match clause still apply globally
|
|
96
|
+
if (request && !matches(rule, request)) continue;
|
|
97
|
+
|
|
98
|
+
const action = rule.action || {};
|
|
99
|
+
if (action.setHeader) {
|
|
100
|
+
for (const [name, value] of Object.entries(action.setHeader)) {
|
|
101
|
+
headers.set(name, value);
|
|
102
|
+
}
|
|
103
|
+
changed = true;
|
|
104
|
+
}
|
|
105
|
+
if (action.removeHeader) {
|
|
106
|
+
for (const name of action.removeHeader) {
|
|
107
|
+
headers.delete(name);
|
|
108
|
+
}
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
if (action.setStatus) {
|
|
112
|
+
status = action.setStatus;
|
|
113
|
+
changed = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (changed) {
|
|
118
|
+
return new Response(response.body, { status, statusText, headers });
|
|
119
|
+
}
|
|
120
|
+
return response;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Wraps a Route-shaped handler ((request, context) => Response) with
|
|
125
|
+
* request/response rewriting: the handler receives the rewritten
|
|
126
|
+
* request, and the handler's response is then run through
|
|
127
|
+
* rewriteResponse before being returned.
|
|
128
|
+
* @param {Function} next - The next handler: (request, context) => Response
|
|
129
|
+
* @returns {Function} Wrapped handler: (request, context) => Response
|
|
130
|
+
*/
|
|
131
|
+
const middleware = (next) => async (request, context) => {
|
|
132
|
+
const rewrittenReq = rewriteRequest(request);
|
|
133
|
+
const response = await next(rewrittenReq, context);
|
|
134
|
+
return rewriteResponse(response, request);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const addRule = (rule) => {
|
|
138
|
+
// Convert path string to regex if it looks like one
|
|
139
|
+
if (rule.match?.path && typeof rule.match.path === "string" && rule.match.path.startsWith("/") && rule.match.path.endsWith("/")) {
|
|
140
|
+
const inner = rule.match.path.slice(1, -1);
|
|
141
|
+
try {
|
|
142
|
+
rule.match.path = new RegExp(inner);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new Error(`Invalid regex pattern: ${rule.match.path}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
rules.push(rule);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const removeRule = (index) => {
|
|
151
|
+
if (index >= 0 && index < rules.length) {
|
|
152
|
+
rules.splice(index, 1);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const getRules = () =>
|
|
157
|
+
rules.map((r, i) => ({
|
|
158
|
+
index: i,
|
|
159
|
+
match: {
|
|
160
|
+
path: r.match?.path instanceof RegExp ? r.match.path.source : r.match?.path,
|
|
161
|
+
method: r.match?.method,
|
|
162
|
+
},
|
|
163
|
+
action: r.action,
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
return { middleware, rewriteRequest, rewriteResponse, addRule, removeRule, getRules };
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const headersEqual = (a, b) => {
|
|
170
|
+
const aEntries = [...a].sort(([k1], [k2]) => k1.localeCompare(k2));
|
|
171
|
+
const bEntries = [...b].sort(([k1], [k2]) => k1.localeCompare(k2));
|
|
172
|
+
if (aEntries.length !== bEntries.length) return false;
|
|
173
|
+
return aEntries.every(([k, v], i) => bEntries[i][0] === k && bEntries[i][1] === v);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export default createRewriter;
|
package/test/exports.mjs
CHANGED
|
@@ -67,3 +67,31 @@ test("@johnhenry/letterpress (self-reference) does not carry createFSRouter", as
|
|
|
67
67
|
const barrel = await import("@johnhenry/letterpress");
|
|
68
68
|
assert.strictEqual("createFSRouter" in barrel, false);
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
// Regression tests closing the leroute -> @johnhenry/letterpress migration
|
|
72
|
+
// gap: `createRewriter` existed in leroute (see rewrite.mjs's top-of-file
|
|
73
|
+
// comment and CHANGELOG.md) but was never carried over during the rename.
|
|
74
|
+
// It's exposed the same way createFSRouter is -- a dedicated subpath, not
|
|
75
|
+
// the main barrel -- for consistency with that existing precedent.
|
|
76
|
+
|
|
77
|
+
test("createRewriter is exported from rewrite.mjs directly", async () => {
|
|
78
|
+
const rewriteModule = await import("../rewrite.mjs");
|
|
79
|
+
assert.strictEqual(typeof rewriteModule.createRewriter, "function");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("package.json exposes a ./rewrite subpath export for createRewriter", async () => {
|
|
83
|
+
const pkg = await import("../package.json", { with: { type: "json" } });
|
|
84
|
+
const exportsMap = pkg.default.exports;
|
|
85
|
+
assert.strictEqual(exportsMap["./rewrite"].default, "./rewrite.mjs");
|
|
86
|
+
assert.strictEqual(exportsMap["./rewrite"].types, "./rewrite.d.ts");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("@johnhenry/letterpress/rewrite resolves createRewriter via self-reference", async () => {
|
|
90
|
+
const rewriteSubpath = await import("@johnhenry/letterpress/rewrite");
|
|
91
|
+
assert.strictEqual(typeof rewriteSubpath.createRewriter, "function");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("@johnhenry/letterpress (self-reference) does not carry createRewriter", async () => {
|
|
95
|
+
const barrel = await import("@johnhenry/letterpress");
|
|
96
|
+
assert.strictEqual("createRewriter" in barrel, false);
|
|
97
|
+
});
|
package/test/index.mjs
CHANGED
package/test/rewrite.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createRewriter } from "../rewrite.mjs";
|
|
4
|
+
|
|
5
|
+
// Ported from leroute's test/rewrite.mjs. createRewriter existed in
|
|
6
|
+
// leroute (the package letterpress was renamed from) but was dropped
|
|
7
|
+
// during the leroute -> @johnhenry/letterpress rename; this closes that
|
|
8
|
+
// migration gap -- see rewrite.mjs's top-of-file comment and
|
|
9
|
+
// CHANGELOG.md. Test cases are unchanged from the leroute original.
|
|
10
|
+
|
|
11
|
+
describe("Rewrite", () => {
|
|
12
|
+
test("should rewrite request path with regex capture groups", () => {
|
|
13
|
+
const rewriter = createRewriter([
|
|
14
|
+
{
|
|
15
|
+
match: { path: /^\/api\/v1\/(.*)/ },
|
|
16
|
+
action: { rewritePath: "/api/v2/$1" },
|
|
17
|
+
},
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const req = new Request("http://localhost/api/v1/users");
|
|
21
|
+
const rewritten = rewriter.rewriteRequest(req);
|
|
22
|
+
const url = new URL(rewritten.url);
|
|
23
|
+
assert.equal(url.pathname, "/api/v2/users");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("should set and remove request headers", () => {
|
|
27
|
+
const rewriter = createRewriter([
|
|
28
|
+
{
|
|
29
|
+
action: {
|
|
30
|
+
setHeader: { "x-custom": "hello" },
|
|
31
|
+
removeHeader: ["x-remove-me"],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const req = new Request("http://localhost/test", {
|
|
37
|
+
headers: { "x-remove-me": "gone" },
|
|
38
|
+
});
|
|
39
|
+
const rewritten = rewriter.rewriteRequest(req);
|
|
40
|
+
assert.equal(rewritten.headers.get("x-custom"), "hello");
|
|
41
|
+
assert.equal(rewritten.headers.get("x-remove-me"), null);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("should set and remove response headers", () => {
|
|
45
|
+
const rewriter = createRewriter([
|
|
46
|
+
{
|
|
47
|
+
action: {
|
|
48
|
+
setHeader: { "x-powered-by": "letterpress" },
|
|
49
|
+
removeHeader: ["server"],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const res = new Response("ok", {
|
|
55
|
+
headers: { server: "old" },
|
|
56
|
+
});
|
|
57
|
+
const rewritten = rewriter.rewriteResponse(res);
|
|
58
|
+
assert.equal(rewritten.headers.get("x-powered-by"), "letterpress");
|
|
59
|
+
assert.equal(rewritten.headers.get("server"), null);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("should set response status", () => {
|
|
63
|
+
const rewriter = createRewriter([
|
|
64
|
+
{
|
|
65
|
+
match: { path: "/teapot" },
|
|
66
|
+
action: { setStatus: 418 },
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const req = new Request("http://localhost/teapot");
|
|
71
|
+
const res = new Response("ok", { status: 200 });
|
|
72
|
+
const rewritten = rewriter.rewriteResponse(res, req);
|
|
73
|
+
assert.equal(rewritten.status, 418);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("response rules only apply when request matches (bug fix)", () => {
|
|
77
|
+
const rewriter = createRewriter([
|
|
78
|
+
{
|
|
79
|
+
match: { path: "/admin" },
|
|
80
|
+
action: { setHeader: { "x-admin": "true" } },
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const req = new Request("http://localhost/public");
|
|
85
|
+
const res = new Response("ok");
|
|
86
|
+
const rewritten = rewriter.rewriteResponse(res, req);
|
|
87
|
+
// Rule should NOT apply because /public doesn't match /admin
|
|
88
|
+
assert.equal(rewritten.headers.get("x-admin"), null);
|
|
89
|
+
// Response should be the same object (unchanged)
|
|
90
|
+
assert.equal(rewritten, res);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("global rules (no match clause) apply to all responses", () => {
|
|
94
|
+
const rewriter = createRewriter([
|
|
95
|
+
{
|
|
96
|
+
action: { setHeader: { "x-global": "yes" } },
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
const req = new Request("http://localhost/anything");
|
|
101
|
+
const res = new Response("ok");
|
|
102
|
+
const rewritten = rewriter.rewriteResponse(res, req);
|
|
103
|
+
assert.equal(rewritten.headers.get("x-global"), "yes");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("middleware wraps handler correctly", async () => {
|
|
107
|
+
const rewriter = createRewriter([
|
|
108
|
+
{
|
|
109
|
+
match: { path: /^\/old\/(.*)/ },
|
|
110
|
+
action: {
|
|
111
|
+
rewritePath: "/new/$1",
|
|
112
|
+
setHeader: { "x-rewritten": "true" },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
// A handler that echoes back the pathname it received
|
|
118
|
+
const handler = async (request) => {
|
|
119
|
+
const url = new URL(request.url);
|
|
120
|
+
return new Response(url.pathname, { status: 200 });
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const wrapped = rewriter.middleware(handler);
|
|
124
|
+
const req = new Request("http://localhost/old/stuff");
|
|
125
|
+
const res = await wrapped(req, {});
|
|
126
|
+
|
|
127
|
+
// Handler should have received the rewritten path
|
|
128
|
+
const body = await res.text();
|
|
129
|
+
assert.equal(body, "/new/stuff");
|
|
130
|
+
// Response should have the rewritten header
|
|
131
|
+
assert.equal(res.headers.get("x-rewritten"), "true");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("addRule with /regex/ string conversion", () => {
|
|
135
|
+
const rewriter = createRewriter();
|
|
136
|
+
rewriter.addRule({
|
|
137
|
+
match: { path: "/^\\/api\\/(.*)/" },
|
|
138
|
+
action: { rewritePath: "/v2/$1" },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const rules = rewriter.getRules();
|
|
142
|
+
assert.equal(rules.length, 1);
|
|
143
|
+
assert.equal(rules[0].match.path, "^\\/api\\/(.*)");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("addRule with invalid regex throws descriptive error", () => {
|
|
147
|
+
const rewriter = createRewriter();
|
|
148
|
+
assert.throws(
|
|
149
|
+
() => {
|
|
150
|
+
rewriter.addRule({
|
|
151
|
+
match: { path: "/[invalid/" },
|
|
152
|
+
action: {},
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
(err) => {
|
|
156
|
+
assert.ok(err.message.includes("Invalid regex pattern"));
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("removeRule / getRules CRUD", () => {
|
|
163
|
+
const rewriter = createRewriter();
|
|
164
|
+
rewriter.addRule({ match: { path: "/a" }, action: {} });
|
|
165
|
+
rewriter.addRule({ match: { path: "/b" }, action: {} });
|
|
166
|
+
rewriter.addRule({ match: { path: "/c" }, action: {} });
|
|
167
|
+
|
|
168
|
+
assert.equal(rewriter.getRules().length, 3);
|
|
169
|
+
|
|
170
|
+
rewriter.removeRule(1);
|
|
171
|
+
const rules = rewriter.getRules();
|
|
172
|
+
assert.equal(rules.length, 2);
|
|
173
|
+
assert.equal(rules[0].match.path, "/a");
|
|
174
|
+
assert.equal(rules[1].match.path, "/c");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("removeRule ignores out-of-bounds index", () => {
|
|
178
|
+
const rewriter = createRewriter();
|
|
179
|
+
rewriter.addRule({ match: { path: "/a" }, action: {} });
|
|
180
|
+
rewriter.removeRule(5);
|
|
181
|
+
rewriter.removeRule(-1);
|
|
182
|
+
assert.equal(rewriter.getRules().length, 1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("method matching", () => {
|
|
186
|
+
const rewriter = createRewriter([
|
|
187
|
+
{
|
|
188
|
+
match: { method: "POST" },
|
|
189
|
+
action: { setHeader: { "x-method": "post" } },
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
const getReq = new Request("http://localhost/test", { method: "GET" });
|
|
194
|
+
const postReq = new Request("http://localhost/test", { method: "POST" });
|
|
195
|
+
|
|
196
|
+
const getResult = rewriter.rewriteRequest(getReq);
|
|
197
|
+
const postResult = rewriter.rewriteRequest(postReq);
|
|
198
|
+
|
|
199
|
+
assert.equal(getResult.headers.get("x-method"), null);
|
|
200
|
+
assert.equal(postResult.headers.get("x-method"), "post");
|
|
201
|
+
});
|
|
202
|
+
});
|
package/test/router.mjs
CHANGED
|
@@ -171,6 +171,87 @@ test("Router - Function handler", async () => {
|
|
|
171
171
|
assert.equal(response.status, 200);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
test("Router - mount delegates matching requests with the prefix stripped", async () => {
|
|
175
|
+
const router = createRouter();
|
|
176
|
+
const sub = createRouter();
|
|
177
|
+
sub.endpoint`GET /users``sub: ${(_, { params }) => "users"}`;
|
|
178
|
+
router.mount("/api", sub);
|
|
179
|
+
|
|
180
|
+
const response = await router(new Request("http://example.com/api/users"));
|
|
181
|
+
assert.equal(response.status, 200);
|
|
182
|
+
assert.equal(await response.text(), "sub: users");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("Router - mount forwards mountPrefix to the sub-router's own handlers via ctx", async () => {
|
|
186
|
+
const router = createRouter();
|
|
187
|
+
const sub = createRouter();
|
|
188
|
+
sub.endpoint`GET /users`((_, ctx) => new Response(ctx.mountPrefix));
|
|
189
|
+
router.mount("/api", sub);
|
|
190
|
+
|
|
191
|
+
const response = await router(new Request("http://example.com/api/users"));
|
|
192
|
+
assert.equal(await response.text(), "/api");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("Router - a caller-supplied ctx reaches matched handlers and defaultHandler alike", async () => {
|
|
196
|
+
const router = createRouter({
|
|
197
|
+
defaultHandler: (_, ctx) => new Response(`default:${ctx.tag}`),
|
|
198
|
+
});
|
|
199
|
+
router.endpoint`GET /hit`((_, ctx) => new Response(`hit:${ctx.tag}`));
|
|
200
|
+
|
|
201
|
+
const hit = await router(new Request("http://example.com/hit"), { tag: "x" });
|
|
202
|
+
assert.equal(await hit.text(), "hit:x");
|
|
203
|
+
|
|
204
|
+
const miss = await router(new Request("http://example.com/miss"), { tag: "y" });
|
|
205
|
+
assert.equal(await miss.text(), "default:y");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("Router - mount matches the bare prefix itself (no trailing segment)", async () => {
|
|
209
|
+
const router = createRouter();
|
|
210
|
+
const sub = createRouter();
|
|
211
|
+
sub.endpoint`GET /``root`;
|
|
212
|
+
router.mount("/api", sub);
|
|
213
|
+
|
|
214
|
+
const response = await router(new Request("http://example.com/api"));
|
|
215
|
+
assert.equal(await response.text(), "root");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("Router - mount falls through to the outer router's 404 for non-matching paths", async () => {
|
|
219
|
+
const router = createRouter();
|
|
220
|
+
const sub = createRouter();
|
|
221
|
+
sub.endpoint`GET /users``sub: users`;
|
|
222
|
+
router.mount("/api", sub);
|
|
223
|
+
|
|
224
|
+
const response = await router(new Request("http://example.com/other"));
|
|
225
|
+
assert.equal(response.status, 404);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("Router - mount accepts an object exposing .fetch (so a router can mount another router)", async () => {
|
|
229
|
+
const router = createRouter();
|
|
230
|
+
const sub = createRouter();
|
|
231
|
+
sub.endpoint`GET /ping``pong`;
|
|
232
|
+
// `sub` is itself callable (Route & RouterExtension), but this exercises
|
|
233
|
+
// the `.fetch` branch explicitly by wrapping it in a plain object.
|
|
234
|
+
router.mount("/api", { fetch: sub });
|
|
235
|
+
|
|
236
|
+
const response = await router(new Request("http://example.com/api/ping"));
|
|
237
|
+
assert.equal(await response.text(), "pong");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("Router - mount preserves query strings and method/body on the forwarded request", async () => {
|
|
241
|
+
const router = createRouter();
|
|
242
|
+
const sub = createRouter();
|
|
243
|
+
sub.endpoint`GET /search`(async (request) => {
|
|
244
|
+
const url = new URL(request.url);
|
|
245
|
+
return new Response(url.search);
|
|
246
|
+
});
|
|
247
|
+
router.mount("/api", sub);
|
|
248
|
+
|
|
249
|
+
const response = await router(
|
|
250
|
+
new Request("http://example.com/api/search?q=hello")
|
|
251
|
+
);
|
|
252
|
+
assert.equal(await response.text(), "?q=hello");
|
|
253
|
+
});
|
|
254
|
+
|
|
174
255
|
// Commented out tests
|
|
175
256
|
/*
|
|
176
257
|
test("Router - InlineParam", async () => {
|
package/test/types/fixture.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
HTTPExpression,
|
|
39
39
|
} from "@johnhenry/letterpress";
|
|
40
40
|
import { createFSRouter } from "@johnhenry/letterpress/fs";
|
|
41
|
+
import { createRewriter } from "@johnhenry/letterpress/rewrite";
|
|
41
42
|
|
|
42
43
|
// createRouter: a Router is a Route (request, context?) => Response, plus
|
|
43
44
|
// an `.endpoint` tagged-template extension that registers routes.
|
|
@@ -136,3 +137,36 @@ async function checkFsRouter() {
|
|
|
136
137
|
return response === null || response instanceof Response;
|
|
137
138
|
}
|
|
138
139
|
void checkFsRouter;
|
|
140
|
+
|
|
141
|
+
// createRewriter: only reachable via the "/rewrite" subpath (not the main
|
|
142
|
+
// barrel), mirroring createFSRouter's "/fs" subpath precedent. Ported from
|
|
143
|
+
// leroute -- see rewrite.mjs's top-of-file comment and CHANGELOG.md.
|
|
144
|
+
const rewriter = createRewriter([
|
|
145
|
+
{
|
|
146
|
+
match: { path: /^\/old\/(.*)/ },
|
|
147
|
+
action: { rewritePath: "/new/$1", setHeader: { "x-rewritten": "true" } },
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
const rewrittenRequest: Request = rewriter.rewriteRequest(
|
|
151
|
+
new Request("http://localhost/old/stuff")
|
|
152
|
+
);
|
|
153
|
+
void rewrittenRequest;
|
|
154
|
+
const rewrittenResponse: Response = rewriter.rewriteResponse(
|
|
155
|
+
new Response("ok"),
|
|
156
|
+
new Request("http://localhost/old/stuff")
|
|
157
|
+
);
|
|
158
|
+
void rewrittenResponse;
|
|
159
|
+
rewriter.addRule({ match: { path: "/teapot" }, action: { setStatus: 418 } });
|
|
160
|
+
rewriter.removeRule(0);
|
|
161
|
+
const ruleSnapshots = rewriter.getRules();
|
|
162
|
+
const firstRuleIndex: number = ruleSnapshots[0]?.index ?? -1;
|
|
163
|
+
void firstRuleIndex;
|
|
164
|
+
const wrapped = rewriter.middleware(async (request: Request) => {
|
|
165
|
+
void request;
|
|
166
|
+
return new Response("ok");
|
|
167
|
+
});
|
|
168
|
+
async function checkRewriterMiddleware() {
|
|
169
|
+
const response = await wrapped(new Request("http://localhost/old/stuff"), {});
|
|
170
|
+
return response instanceof Response;
|
|
171
|
+
}
|
|
172
|
+
void checkRewriterMiddleware;
|
package/tsconfig.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -19,12 +19,18 @@ export type RouterEndpointResponder = {
|
|
|
19
19
|
(handler: Route): Router;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
// A handler mountable under a Router prefix: a plain (Request) => Response
|
|
23
|
+
// function, or an object exposing one as `.fetch` (so a Router can mount
|
|
24
|
+
// another Router).
|
|
25
|
+
export type Mountable = Route | { fetch: Route };
|
|
26
|
+
|
|
22
27
|
// Extension for the Router to add endpoints
|
|
23
28
|
export type RouterExtension = {
|
|
24
29
|
endpoint: (
|
|
25
30
|
template: TemplateStringsArray,
|
|
26
31
|
...substitutions: any[]
|
|
27
32
|
) => RouterEndpointResponder;
|
|
33
|
+
mount: (prefix: string, subHandler: Mountable) => Router;
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
// Combines Route and RouterExtension
|