@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/readme.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Letterpress
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@johnhenry/letterpress)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
<img alt="Letterpress Logo" width="512" height="512" src="./logo.jpeg" style="width:512px;height:512px"/>
|
|
7
|
+
|
|
8
|
+
> Previously published as `leroute`, last unscoped version `0.0.1`. Now
|
|
9
|
+
> `@johnhenry/letterpress`, restarting at `0.0.0`.
|
|
10
|
+
|
|
11
|
+
Letterpress is a flexible and powerful routing library for handling HTTP requests and responses in JavaScript and TypeScript applications.
|
|
12
|
+
|
|
13
|
+
Letterpress works great with [leserve](https://www.npmjs.com/package/@johnhenry/leserve), a library for serving endpoints.
|
|
14
|
+
|
|
15
|
+
## 🚀 Features
|
|
16
|
+
|
|
17
|
+
- Intuitive API using tagged template strings for route creation
|
|
18
|
+
- Support for simple and complex routing patterns
|
|
19
|
+
- Flexible response generation using template literals
|
|
20
|
+
- Middleware support for request and response processing
|
|
21
|
+
- Streaming response capabilities
|
|
22
|
+
- Full TypeScript support with included type definitions
|
|
23
|
+
|
|
24
|
+
## 📦 Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @johnhenry/letterpress
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or using yarn:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
yarn add @johnhenry/letterpress
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 🛠 Usage
|
|
37
|
+
|
|
38
|
+
### Basic Example
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
import serve from "@johnhenry/leserve";
|
|
42
|
+
import { createRouter, createRoute } from "@johnhenry/letterpress";
|
|
43
|
+
|
|
44
|
+
// Create a router
|
|
45
|
+
const router = createRouter();
|
|
46
|
+
|
|
47
|
+
// Define a simple route
|
|
48
|
+
router.endpoint`GET /``
|
|
49
|
+
<!DOCTYPE html>
|
|
50
|
+
<html lang="en">
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="UTF-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
54
|
+
<title>Welcome to Route</title>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<h1>Welcome to Route!</h1>
|
|
58
|
+
<p>The current time is: ${() => new Date().toISOString()}</p>
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
// Define a route with parameters
|
|
64
|
+
router.endpoint`GET /user/:id``
|
|
65
|
+
<!DOCTYPE html>
|
|
66
|
+
<html lang="en">
|
|
67
|
+
<head>
|
|
68
|
+
<meta charset="UTF-8">
|
|
69
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
70
|
+
<title>User Profile</title>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<h1>User Profile</h1>
|
|
74
|
+
<p>User ID: ${(_, { params }) => params.id}</p>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
// Start the server
|
|
80
|
+
serve({ port: 8080 }, router);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Advanced Usage
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
import serve from "@johnhenry/leserve";
|
|
87
|
+
import { createRouter, createRoute } from "@johnhenry/letterpress";
|
|
88
|
+
|
|
89
|
+
const router = createRouter();
|
|
90
|
+
|
|
91
|
+
// JSON API endpoint
|
|
92
|
+
router.endpoint`GET /api/data`(async (request) => {
|
|
93
|
+
const data = {
|
|
94
|
+
message: "Hello, World!",
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
return new Response(JSON.stringify(data), {
|
|
98
|
+
headers: { "Content-Type": "application/json" },
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Protected route with header matching
|
|
103
|
+
router.endpoint`GET /protected [Authorization: Bearer *]``
|
|
104
|
+
HTTP/1.1 200 OK
|
|
105
|
+
Content-Type: text/plain
|
|
106
|
+
|
|
107
|
+
This is a protected resource
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
// Custom error handling
|
|
111
|
+
router.endpoint`GET /error`(() => {
|
|
112
|
+
throw new Error("Intentional error");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const errorHandler = (error, request) => {
|
|
116
|
+
console.error("Error:", error);
|
|
117
|
+
return new Response("An error occurred", { status: 500 });
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
serve({ port: 8080 }, router, { errorHandler });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 📘 API Reference
|
|
124
|
+
|
|
125
|
+
### `createRouter(options?: RouterInit): Router`
|
|
126
|
+
|
|
127
|
+
Creates a new router instance.
|
|
128
|
+
|
|
129
|
+
#### Options:
|
|
130
|
+
|
|
131
|
+
- `baseUrl`: Base URL for all routes (optional)
|
|
132
|
+
- `defaultHandler`: Default route handler (optional)
|
|
133
|
+
- `errorHandler`: Custom error handler function (optional)
|
|
134
|
+
- `cache`: Caching options (optional)
|
|
135
|
+
|
|
136
|
+
### `createRoute(options?: RouteInit): Route`
|
|
137
|
+
|
|
138
|
+
Creates a new route handler.
|
|
139
|
+
|
|
140
|
+
#### Options:
|
|
141
|
+
|
|
142
|
+
- `headers`: Initial headers for the response (optional)
|
|
143
|
+
- `status`: HTTP status code (optional)
|
|
144
|
+
- `statusText`: HTTP status text (optional)
|
|
145
|
+
- `streaming`: Enable streaming response (optional)
|
|
146
|
+
|
|
147
|
+
### `HTTPExpression`
|
|
148
|
+
|
|
149
|
+
A tagged-template function for matching HTTP requests against a method/path/header pattern.
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
import { HTTPExpression } from "@johnhenry/letterpress";
|
|
153
|
+
|
|
154
|
+
const expr = HTTPExpression`GET /users/:id`;
|
|
155
|
+
expr.test(request); // boolean — does this request match?
|
|
156
|
+
expr.exec(request); // matched params (plus method/headers), or null
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `createRequest`, `createResponse`
|
|
160
|
+
|
|
161
|
+
Tagged-template functions for building `Request`/`Response` objects from raw HTTP-message-shaped template literals. See [api.md](./api.md) for details.
|
|
162
|
+
|
|
163
|
+
### `createFSRouter`
|
|
164
|
+
|
|
165
|
+
Builds a `Router` from a filesystem-based route directory.
|
|
166
|
+
|
|
167
|
+
### `deconstruct`, `cook`
|
|
168
|
+
|
|
169
|
+
Lower-level utility functions used to parse and process tagged HTTP template literals. See [api.md](./api.md) for details.
|
|
170
|
+
|
|
171
|
+
> Note: `@johnhenry/letterpress` does not export a `serve` function. To actually run a server, pair it with [leserve](https://www.npmjs.com/package/@johnhenry/leserve) (or any server of your choice) as shown in the usage examples above.
|
|
172
|
+
|
|
173
|
+
## 📜 Changelog
|
|
174
|
+
|
|
175
|
+
All notable changes to this project will be documented in this file.
|
|
176
|
+
|
|
177
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
178
|
+
|
|
179
|
+
This project will adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once it reaches 1.0.0.
|
|
180
|
+
|
|
181
|
+
### [Unreleased]
|
|
182
|
+
|
|
183
|
+
#### Changed (breaking)
|
|
184
|
+
|
|
185
|
+
- **Renamed the package from `leroute` to `@johnhenry/letterpress`**, and
|
|
186
|
+
restarted the version at `0.0.0` (previously published as `leroute`, last
|
|
187
|
+
unscoped version `0.0.1`).
|
|
188
|
+
- **Renamed the exported identifiers to match**: `createLeRoute` →
|
|
189
|
+
`createRoute`, `createLeRouter` → `createRouter`, `LeRoute` → `Route`,
|
|
190
|
+
`LeRouteInit` → `RouteInit`, `LeRouteMiddleware` → `RouteMiddleware`,
|
|
191
|
+
`LeRouter` → `Router`, `LeRouterExtension` → `RouterExtension`,
|
|
192
|
+
`LeRouterInit` → `RouterInit`, `LeRouterMiddleware` → `RouterMiddleware`.
|
|
193
|
+
`create-leroute.mjs`/`create-lerouter.mjs` were also renamed to
|
|
194
|
+
`create-route.mjs`/`create-router.mjs` (and their test files
|
|
195
|
+
correspondingly), matching the new export names.
|
|
196
|
+
- **Raised `engines.node` to `>=26.0.0`**, matching the rest of the
|
|
197
|
+
`@johnhenry/*` family's floor.
|
|
198
|
+
|
|
199
|
+
#### Fixed
|
|
200
|
+
|
|
201
|
+
- 🐛 `test/index.mjs` imported `lerouter.mjs` twice and never imported `leroute.mjs`, silently dropping the `createRoute` test suite from `npm test`
|
|
202
|
+
- 🐛 `test/leroute.mjs` imported `createRoute` from `create-router.mjs` (which doesn't export it) instead of `create-route.mjs`, which would have thrown once the missing import above was fixed
|
|
203
|
+
- 🐛 `package.json` declared `"license": "ISC"` while the `LICENSE` file and README badge are MIT; changed to `"license": "MIT"` to match
|
|
204
|
+
- 📝 README and API docs referenced `serve` and `tagRequest` as exports of `leroute`; neither is exported — `serve` comes from the separate `leserve` package, and `tagRequest` doesn't exist. Docs corrected to list the real exports
|
|
205
|
+
- 📝 API docs showed `HTTPExpression(...)` as a constructor with `.method`/`.path`/`.version` properties; corrected to show it as a tagged-template function returning `{ test(request), exec(request) }`
|
|
206
|
+
- 🔒 **Security**: `router.endpoint` reused the per-request context object (which carries the incoming request's `Headers`) as the response-init object passed to `createRoute`, so every incoming request header — `Authorization`, `Cookie`, arbitrary custom headers — was echoed back on every response
|
|
207
|
+
- 🐛 `createRouter`'s dispatch loop called `handler(request, ctx)` without `await`, so an async handler that threw was never caught by the configured `errorHandler`/`defaultHandler` and instead crashed as an unhandled rejection
|
|
208
|
+
- 🐛 `createRoute` threw `Cannot read properties of null/undefined (reading 'toString')` on a `${null}`/`${undefined}` substitution instead of rendering nothing (ordinary template-literal semantics), even though a substitution *function* returning `undefined` was already treated as "no output"
|
|
209
|
+
- 🐛 A substitution *function* returning a `ReadableStream`/`Blob`/`ArrayBuffer`/`Uint8Array` fell through to `.toString()`, producing the literal string `"[object ReadableStream]"` instead of streaming the actual content (direct substitutions of these types already worked correctly)
|
|
210
|
+
- 🐛 `utility/dynamic-run.mjs` unconditionally attempted to `fs.unlink()` its temp file even when `fs.writeFile()` itself had failed, masking the real write error behind a misleading `ENOENT` — which `fs-router.mjs` then silently treated as "route not found" instead of surfacing a genuine permissions/disk-full failure. A cleanup (`unlink`) failure *after* a successful import also discarded the successful result
|
|
211
|
+
- 🐛 `fs-router.mjs`'s `index.html` handling wrote its synthesized temp module into leroute's own installed package directory instead of the matched route's directory — breaking the feature entirely in any deployment where the package install directory is read-only (containers, CI-built images)
|
|
212
|
+
|
|
213
|
+
#### Added
|
|
214
|
+
|
|
215
|
+
- `"types"` field in `package.json` pointing to the existing `types.d.ts`
|
|
216
|
+
- A real `description` and `keywords` in `package.json` (previously empty)
|
|
217
|
+
|
|
218
|
+
#### Removed
|
|
219
|
+
|
|
220
|
+
- Deleted `utility/cd.mjs`, an unused debug scratch file with no imports and no tests
|
|
221
|
+
|
|
222
|
+
### [0.0.0] - 2024-08-26
|
|
223
|
+
|
|
224
|
+
#### Added
|
|
225
|
+
|
|
226
|
+
- 🎉 Initial release of Route
|
|
227
|
+
- 🛠 Core routing functionality
|
|
228
|
+
- 🛠 HTTP request and response handling
|
|
229
|
+
- 🛠 TypeScript definitions
|
|
230
|
+
- 🛠 Support for route parameters and query strings
|
|
231
|
+
|
|
232
|
+
## 🤝 Contributing
|
|
233
|
+
|
|
234
|
+
We welcome contributions to Route! Here's how you can help:
|
|
235
|
+
|
|
236
|
+
1. Fork the repository
|
|
237
|
+
2. Create a new branch: `git checkout -b feature/your-feature-name`
|
|
238
|
+
3. Make your changes and commit them: `git commit -m 'Add some feature'`
|
|
239
|
+
4. Push to the branch: `git push origin feature/your-feature-name`
|
|
240
|
+
5. Submit a pull request
|
|
241
|
+
|
|
242
|
+
Please make sure to update tests as appropriate and adhere to the existing coding style.
|
|
243
|
+
|
|
244
|
+
## 📄 License
|
|
245
|
+
|
|
246
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
247
|
+
|
|
248
|
+
## 🙏 Acknowledgements
|
|
249
|
+
|
|
250
|
+
- Thanks to all contributors who have helped shape Route
|
|
251
|
+
- Inspired by modern web development practices and the need for flexible routing solutions
|
|
252
|
+
|
|
253
|
+
## 📬 Contact
|
|
254
|
+
|
|
255
|
+
For questions, suggestions, or issues, please open an issue on the GitHub repository or contact the maintainers directly.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
Happy routing with Route! 🚀
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { promises as fs } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import dynamicRun from "../utility/dynamic-run.mjs";
|
|
7
|
+
|
|
8
|
+
test("dynamicRun - surfaces the original error when writing the temp file fails, instead of masking it with a cleanup ENOENT", async () => {
|
|
9
|
+
// Regression test: dynamicRun's catch block used to call
|
|
10
|
+
// `await fs.unlink(tempFilePath)` unconditionally, even when
|
|
11
|
+
// `fs.writeFile` itself was the thing that failed (so the temp file was
|
|
12
|
+
// never created). That unlink attempt threw its own ENOENT, which
|
|
13
|
+
// replaced/masked the real underlying error (e.g. a permissions or
|
|
14
|
+
// disk-full failure) - and fs-router.mjs specifically treats ENOENT as
|
|
15
|
+
// "route not found", silently turning a real write failure into a 404.
|
|
16
|
+
const originalWriteFile = fs.writeFile;
|
|
17
|
+
const originalUnlink = fs.unlink;
|
|
18
|
+
let unlinkCalled = false;
|
|
19
|
+
|
|
20
|
+
fs.writeFile = async () => {
|
|
21
|
+
const error = new Error("simulated EACCES: permission denied");
|
|
22
|
+
error.code = "EACCES";
|
|
23
|
+
throw error;
|
|
24
|
+
};
|
|
25
|
+
fs.unlink = async (...args) => {
|
|
26
|
+
unlinkCalled = true;
|
|
27
|
+
return originalUnlink(...args);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await assert.rejects(
|
|
32
|
+
() => dynamicRun("export default 1;", os.tmpdir()),
|
|
33
|
+
(error) => {
|
|
34
|
+
assert.equal(error.code, "EACCES");
|
|
35
|
+
assert.match(error.message, /simulated EACCES/);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
assert.equal(
|
|
40
|
+
unlinkCalled,
|
|
41
|
+
false,
|
|
42
|
+
"unlink should not be attempted for a file that was never written"
|
|
43
|
+
);
|
|
44
|
+
} finally {
|
|
45
|
+
fs.writeFile = originalWriteFile;
|
|
46
|
+
fs.unlink = originalUnlink;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("dynamicRun - a cleanup (unlink) failure after a successful import does not discard the imported module", async () => {
|
|
51
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "letterpress-dynrun-"));
|
|
52
|
+
const originalUnlink = fs.unlink;
|
|
53
|
+
fs.unlink = async () => {
|
|
54
|
+
const error = new Error("simulated cleanup failure");
|
|
55
|
+
throw error;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const result = await dynamicRun(
|
|
60
|
+
"export default 42;",
|
|
61
|
+
tmpDir
|
|
62
|
+
);
|
|
63
|
+
assert.equal(result.default, 42);
|
|
64
|
+
} finally {
|
|
65
|
+
fs.unlink = originalUnlink;
|
|
66
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import { createFSRouter } from "../fs-router.mjs";
|
|
7
|
+
|
|
8
|
+
test("fs-router - writes the synthesized index.html temp module into the matched route directory", async () => {
|
|
9
|
+
// Regression test: fs-router used to pass letterpress's own package directory
|
|
10
|
+
// (from theresWaldo(import.meta.url)) to dynamicRun instead of the
|
|
11
|
+
// matched route's directory. That meant every index.html request required
|
|
12
|
+
// the *installed package's own directory* (e.g. node_modules/@johnhenry/letterpress) to
|
|
13
|
+
// be writable - unrelated to, and often less permissive than, the app's
|
|
14
|
+
// own route directory - and had nothing to do with where the index.html
|
|
15
|
+
// actually lived.
|
|
16
|
+
const tmpRoot = await fs.mkdtemp(
|
|
17
|
+
path.join(os.tmpdir(), "letterpress-fsrouter-")
|
|
18
|
+
);
|
|
19
|
+
const routeDir = path.join(tmpRoot, "routes");
|
|
20
|
+
await fs.mkdir(routeDir, { recursive: true });
|
|
21
|
+
await fs.writeFile(
|
|
22
|
+
path.join(routeDir, "index.html"),
|
|
23
|
+
"<p>hello ${() => 1 + 1}</p>"
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const writtenPaths = [];
|
|
27
|
+
const originalWriteFile = fs.writeFile;
|
|
28
|
+
fs.writeFile = (filePath, ...rest) => {
|
|
29
|
+
writtenPaths.push(filePath);
|
|
30
|
+
return originalWriteFile(filePath, ...rest);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let response;
|
|
34
|
+
try {
|
|
35
|
+
const router = createFSRouter(routeDir);
|
|
36
|
+
const request = new Request("http://localhost/", {
|
|
37
|
+
headers: { host: "localhost" },
|
|
38
|
+
});
|
|
39
|
+
response = await router(request);
|
|
40
|
+
} finally {
|
|
41
|
+
fs.writeFile = originalWriteFile;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
assert.equal(response.status, 200);
|
|
46
|
+
assert.match(await response.text(), /hello 2/);
|
|
47
|
+
assert.equal(writtenPaths.length, 1);
|
|
48
|
+
assert.equal(
|
|
49
|
+
path.dirname(writtenPaths[0]),
|
|
50
|
+
routeDir,
|
|
51
|
+
"temp module should be written into the matched route directory, not the letterpress package directory"
|
|
52
|
+
);
|
|
53
|
+
} finally {
|
|
54
|
+
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("fs-router - cleans up its temp module after serving an index.html route", async () => {
|
|
59
|
+
const tmpRoot = await fs.mkdtemp(
|
|
60
|
+
path.join(os.tmpdir(), "letterpress-fsrouter-")
|
|
61
|
+
);
|
|
62
|
+
const routeDir = path.join(tmpRoot, "routes");
|
|
63
|
+
await fs.mkdir(routeDir, { recursive: true });
|
|
64
|
+
await fs.writeFile(path.join(routeDir, "index.html"), "<p>hi</p>");
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const router = createFSRouter(routeDir);
|
|
68
|
+
const request = new Request("http://localhost/", {
|
|
69
|
+
headers: { host: "localhost" },
|
|
70
|
+
});
|
|
71
|
+
const response = await router(request);
|
|
72
|
+
assert.equal(response.status, 200);
|
|
73
|
+
|
|
74
|
+
const remaining = await fs.readdir(routeDir);
|
|
75
|
+
assert.deepEqual(
|
|
76
|
+
remaining.sort(),
|
|
77
|
+
["index.html"],
|
|
78
|
+
"no orphaned temp-*.mjs file should remain after the request"
|
|
79
|
+
);
|
|
80
|
+
} finally {
|
|
81
|
+
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|