lynnix 0.0.1
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/README.md +542 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/lynnix.d.ts +46 -0
- package/dist/lynnix.d.ts.map +1 -0
- package/dist/lynnix.js +199 -0
- package/dist/lynnix.js.map +1 -0
- package/dist/utils/augmentRequest.d.ts +12 -0
- package/dist/utils/augmentRequest.d.ts.map +1 -0
- package/dist/utils/augmentRequest.js +66 -0
- package/dist/utils/augmentRequest.js.map +1 -0
- package/dist/utils/augmentResponse.d.ts +17 -0
- package/dist/utils/augmentResponse.d.ts.map +1 -0
- package/dist/utils/augmentResponse.js +56 -0
- package/dist/utils/augmentResponse.js.map +1 -0
- package/dist/utils/buildRoutesMap.d.ts +19 -0
- package/dist/utils/buildRoutesMap.d.ts.map +1 -0
- package/dist/utils/buildRoutesMap.js +48 -0
- package/dist/utils/buildRoutesMap.js.map +1 -0
- package/dist/utils/error.d.ts +18 -0
- package/dist/utils/error.d.ts.map +1 -0
- package/dist/utils/error.js +20 -0
- package/dist/utils/error.js.map +1 -0
- package/dist/utils/findClosestBoundary.d.ts +14 -0
- package/dist/utils/findClosestBoundary.d.ts.map +1 -0
- package/dist/utils/findClosestBoundary.js +29 -0
- package/dist/utils/findClosestBoundary.js.map +1 -0
- package/dist/utils/getMiddlewareChain.d.ts +19 -0
- package/dist/utils/getMiddlewareChain.d.ts.map +1 -0
- package/dist/utils/getMiddlewareChain.js +31 -0
- package/dist/utils/getMiddlewareChain.js.map +1 -0
- package/dist/utils/handleHttpError.d.ts +24 -0
- package/dist/utils/handleHttpError.d.ts.map +1 -0
- package/dist/utils/handleHttpError.js +23 -0
- package/dist/utils/handleHttpError.js.map +1 -0
- package/dist/utils/handleNotFound.d.ts +33 -0
- package/dist/utils/handleNotFound.d.ts.map +1 -0
- package/dist/utils/handleNotFound.js +71 -0
- package/dist/utils/handleNotFound.js.map +1 -0
- package/dist/utils/lruCache.d.ts +17 -0
- package/dist/utils/lruCache.d.ts.map +1 -0
- package/dist/utils/lruCache.js +36 -0
- package/dist/utils/lruCache.js.map +1 -0
- package/dist/utils/lynnixRequest.d.ts +22 -0
- package/dist/utils/lynnixRequest.d.ts.map +1 -0
- package/dist/utils/lynnixRequest.js +21 -0
- package/dist/utils/lynnixRequest.js.map +1 -0
- package/dist/utils/lynnixResponse.d.ts +53 -0
- package/dist/utils/lynnixResponse.d.ts.map +1 -0
- package/dist/utils/lynnixResponse.js +142 -0
- package/dist/utils/lynnixResponse.js.map +1 -0
- package/dist/utils/matchRoute.d.ts +20 -0
- package/dist/utils/matchRoute.d.ts.map +1 -0
- package/dist/utils/matchRoute.js +55 -0
- package/dist/utils/matchRoute.js.map +1 -0
- package/dist/utils/parseReqBody.d.ts +28 -0
- package/dist/utils/parseReqBody.d.ts.map +1 -0
- package/dist/utils/parseReqBody.js +415 -0
- package/dist/utils/parseReqBody.js.map +1 -0
- package/dist/utils/runMiddlewares.d.ts +5 -0
- package/dist/utils/runMiddlewares.d.ts.map +1 -0
- package/dist/utils/runMiddlewares.js +18 -0
- package/dist/utils/runMiddlewares.js.map +1 -0
- package/dist/utils/sortRoutes.d.ts +16 -0
- package/dist/utils/sortRoutes.d.ts.map +1 -0
- package/dist/utils/sortRoutes.js +44 -0
- package/dist/utils/sortRoutes.js.map +1 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# Lynnix
|
|
2
|
+
|
|
3
|
+
**File-based routing for htmx, powered by Mutor.js.**
|
|
4
|
+
|
|
5
|
+
Lynnix is a lightweight, framework-agnostic routing and SSR middleware for Node.js that makes building htmx applications feel natural. Drop your files in the right place, export a function, and Lynnix handles the rest — routing, rendering, middleware chains, htmx-aware responses, and error boundaries, all wired together automatically.
|
|
6
|
+
|
|
7
|
+
No magic config files. No build step. Just a filesystem that speaks HTTP.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { createLynnixApp } from "lynnix";
|
|
11
|
+
import express from "express";
|
|
12
|
+
|
|
13
|
+
const app = express();
|
|
14
|
+
const handler = await createLynnixApp("app");
|
|
15
|
+
|
|
16
|
+
app.use(express.static("public"));
|
|
17
|
+
app.use(handler);
|
|
18
|
+
app.listen(3000);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That's it. Everything else comes from your files.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Why Lynnix?
|
|
26
|
+
|
|
27
|
+
htmx is a breath of fresh air — it brings back the simplicity of server-rendered HTML without sacrificing interactivity. But as your application grows, wiring up routes, rendering templates, and managing partial responses by hand gets tedious fast.
|
|
28
|
+
|
|
29
|
+
Lynnix gives htmx applications the structure they deserve. It handles the routing and rendering layer so you can focus on what actually matters: building your product.
|
|
30
|
+
|
|
31
|
+
It's built on [Mutor.js](https://github.com/allAboutJS/Mutor.js) — a fast, TypeScript-native, zero-dependency template engine — so your templates are expressive, secure, and compiled for performance.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install lynnix
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Lynnix has a lean set of optional peer dependencies that unlock additional features:
|
|
42
|
+
|
|
43
|
+
| Package | What it unlocks |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `cookie` | Cookie parsing and setting |
|
|
46
|
+
| `@fastify/busboy` | `multipart/form-data` and `application/x-www-form-urlencoded` body parsing |
|
|
47
|
+
| `body-parser` | `application/json` body parsing |
|
|
48
|
+
| `qs` | Advanced query string and URL-encoded body parsing |
|
|
49
|
+
|
|
50
|
+
Install only what you need. Lynnix will work without any of them and warn you in the console if a feature requires one that isn't installed.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Getting Started
|
|
55
|
+
|
|
56
|
+
### With bare `node:http`
|
|
57
|
+
|
|
58
|
+
Lynnix works with Node's built-in HTTP server out of the box. For static files, pair it with [`send-static`](https://www.npmjs.com/package/send-static):
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { createLynnixApp } from "lynnix";
|
|
62
|
+
import sendStatic from "send-static";
|
|
63
|
+
import * as http from "node:http";
|
|
64
|
+
|
|
65
|
+
async function main() {
|
|
66
|
+
const serve = sendStatic("public", { index: false });
|
|
67
|
+
const handle = await createLynnixApp("app");
|
|
68
|
+
|
|
69
|
+
const server = http.createServer((req, res) => {
|
|
70
|
+
serve(req, res, () => handle(req, res));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
server.listen(3000, () => {
|
|
74
|
+
console.log("Server running on http://localhost:3000");
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### With Express
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { createLynnixApp } from "lynnix";
|
|
85
|
+
import express from "express";
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
const app = express();
|
|
89
|
+
const handler = await createLynnixApp("app");
|
|
90
|
+
|
|
91
|
+
app.use(express.static("public"));
|
|
92
|
+
app.use(handler);
|
|
93
|
+
|
|
94
|
+
app.listen(3000, () => {
|
|
95
|
+
console.log("Server running on http://localhost:3000");
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `createLynnixApp(path, mutorConfig?, bodyParserOptions?)`
|
|
103
|
+
|
|
104
|
+
| Parameter | Type | Description |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `path` | `string` | The root directory of your application (e.g. `"app"`) |
|
|
107
|
+
| `mutorConfig` | `PartialMutorConfig` | Optional Mutor.js configuration (excluding `rootDir`) |
|
|
108
|
+
| `bodyParserOptions` | `ParseReqBodyOptions` | Optional body parser limits and settings |
|
|
109
|
+
|
|
110
|
+
Returns a standard `(req, res) => void` request handler you can mount anywhere.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Project Structure
|
|
115
|
+
|
|
116
|
+
A Lynnix application lives inside a single directory (conventionally `app/`). The filesystem is your router.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
app/
|
|
120
|
+
├── components/
|
|
121
|
+
│ └── header.html
|
|
122
|
+
├── dashboard/
|
|
123
|
+
│ ├── posts/
|
|
124
|
+
│ │ ├── [slug]/
|
|
125
|
+
│ │ │ └── loader.js
|
|
126
|
+
│ │ ├── loader.js
|
|
127
|
+
│ │ └── page.html
|
|
128
|
+
│ ├── layout.html
|
|
129
|
+
│ ├── loader.js
|
|
130
|
+
│ ├── middleware.js
|
|
131
|
+
│ ├── not-found.html
|
|
132
|
+
│ └── page.html
|
|
133
|
+
├── loader.js
|
|
134
|
+
├── not-found.html
|
|
135
|
+
└── page.html
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Each directory maps to a route. The files inside determine how that route behaves.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## File Conventions
|
|
143
|
+
|
|
144
|
+
These are the reserved filenames Lynnix recognises in any route directory:
|
|
145
|
+
|
|
146
|
+
| File | Purpose |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `page.html` | Full-page HTML response for regular requests |
|
|
149
|
+
| `fragment.html` | Partial HTML response for htmx requests |
|
|
150
|
+
| `loader.js` / `loader.ts` | HTTP method handlers and data loading |
|
|
151
|
+
| `middleware.js` / `middleware.ts` | Route-level middleware |
|
|
152
|
+
| `not-found.html` | 404 page for regular requests |
|
|
153
|
+
| `fragment.not-found.html` | 404 fragment for htmx requests |
|
|
154
|
+
| `error.html` | Error page for regular requests |
|
|
155
|
+
| `fragment.error.html` | Error fragment for htmx requests |
|
|
156
|
+
|
|
157
|
+
Any other file (components, utilities, layouts) is invisible to the router and can be named freely.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Routing
|
|
162
|
+
|
|
163
|
+
### Static Routes
|
|
164
|
+
|
|
165
|
+
A directory named `about` maps to `/about`. Nest them as deep as you like.
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
app/
|
|
169
|
+
├── about/
|
|
170
|
+
│ └── page.html → /about
|
|
171
|
+
├── blog/
|
|
172
|
+
│ └── page.html → /blog
|
|
173
|
+
└── page.html → /
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Dynamic Routes
|
|
177
|
+
|
|
178
|
+
Wrap a directory name in square brackets to create a dynamic segment. The captured value is available in your loader as `req.params`.
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
app/
|
|
182
|
+
└── posts/
|
|
183
|
+
├── [slug]/
|
|
184
|
+
│ ├── loader.js
|
|
185
|
+
│ └── page.html → /posts/:slug
|
|
186
|
+
└── page.html → /posts
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
// app/posts/[slug]/loader.js
|
|
191
|
+
export function GET(req) {
|
|
192
|
+
const { slug } = req.params;
|
|
193
|
+
const post = db.posts.find(slug);
|
|
194
|
+
return { post };
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Catch-All Routes
|
|
199
|
+
|
|
200
|
+
Double brackets capture every path segment from that point onward. Use this for wildcard pages, CMS-driven routes, or custom 404 experiences.
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
app/
|
|
204
|
+
└── [[slug]]/
|
|
205
|
+
├── loader.js
|
|
206
|
+
└── page.html → matches /anything, /a/b/c, /a/b/c/d ...
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The entire remaining path is available as a string in `req.params`:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
// app/[[slug]]/loader.js
|
|
213
|
+
export function GET(req) {
|
|
214
|
+
const { slug } = req.params; // e.g. "docs/getting-started/installation"
|
|
215
|
+
return { slug };
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Route Priority
|
|
220
|
+
|
|
221
|
+
When multiple routes could match the same path, Lynnix resolves the conflict by **specificity** — the more concrete a route is, the higher its priority. Specificity is determined by three factors in order:
|
|
222
|
+
|
|
223
|
+
**1. Tier** — Static routes always beat dynamic routes, which always beat catch-all routes.
|
|
224
|
+
|
|
225
|
+
**2. Static segment count** — Within the same tier, routes with more concrete (non-dynamic) segments win. `/posts/featured` has two static segments and beats `/posts/[slug]` which has one. `/[category]/featured` has one static segment and beats `/[category]/[slug]` which has none.
|
|
226
|
+
|
|
227
|
+
**3. Depth** — When two routes in the same tier have the same number of static segments, shallower routes win for static and dynamic routes (less ambiguous), while deeper routes win for catch-all routes (a more constrained prefix is more specific).
|
|
228
|
+
|
|
229
|
+
A few examples to make it concrete:
|
|
230
|
+
|
|
231
|
+
| Path | Matches |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `/posts/featured` | `/posts/featured` — static wins |
|
|
234
|
+
| `/posts/hello-world` | `/posts/[slug]` — dynamic picks it up |
|
|
235
|
+
| `/electronics/featured` | `/[category]/featured` — more static segments wins |
|
|
236
|
+
| `/electronics/some-product` | `/[category]/[slug]` — falls through to two-dynamic route |
|
|
237
|
+
| `/posts/a/b/c` | `/posts/[[slug]]` — deeper catch-all prefix beats shallower |
|
|
238
|
+
| `/anything/goes/here` | `/[[slug]]` — root catch-all is the last resort |
|
|
239
|
+
|
|
240
|
+
You never have to think about this ordering explicitly — Lynnix sorts your routes at startup so the right one always wins.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Loaders
|
|
245
|
+
|
|
246
|
+
A `loader.js` file exports named functions matching the HTTP methods they handle. Method names are uppercased.
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
// app/posts/loader.js
|
|
250
|
+
export function GET(req, res) {
|
|
251
|
+
return { posts: db.posts.all() };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function POST(req, res) {
|
|
255
|
+
const { title, content } = req.body;
|
|
256
|
+
db.posts.create({ title, content });
|
|
257
|
+
res.redirect("/posts");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function DELETE(req, res) {
|
|
261
|
+
db.posts.delete(req.params.slug);
|
|
262
|
+
res.status(200).end();
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Whatever you return from a loader becomes `data` in your template:
|
|
267
|
+
|
|
268
|
+
```html
|
|
269
|
+
<!-- app/posts/page.html -->
|
|
270
|
+
{{ for post of data.posts }}
|
|
271
|
+
<article>
|
|
272
|
+
<h2>{{ post.title }}</h2>
|
|
273
|
+
</article>
|
|
274
|
+
{{ endfor }}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
If the response has already been ended inside the loader (via `res.redirect()`, `res.end()`, etc.), Lynnix skips rendering entirely. If a route has no loader, non-GET requests return `405 Method Not Allowed` automatically.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Middleware
|
|
282
|
+
|
|
283
|
+
A `middleware.js` file exports a single default function. It runs before the loader on every request to that route and all routes nested beneath it.
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
// app/dashboard/middleware.js
|
|
287
|
+
import { users } from "../lib/users.js";
|
|
288
|
+
|
|
289
|
+
export default function dashboardMiddleware(req, res) {
|
|
290
|
+
const userId = req.cookies.auth;
|
|
291
|
+
|
|
292
|
+
if (!userId) {
|
|
293
|
+
res.redirect("/sign-in");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const user = users.find((u) => u.id === userId);
|
|
298
|
+
|
|
299
|
+
if (!user) {
|
|
300
|
+
res.redirect("/sign-in");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
req.user = user;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Middleware chains run top-down** — from the root of your app to the matched route. A middleware at `app/middleware.js` runs on every request. A middleware at `app/dashboard/middleware.js` runs on every request under `/dashboard`. If any middleware ends the response, the chain stops and the loader never runs.
|
|
309
|
+
|
|
310
|
+
There is no `next` function. Returning from the middleware function is enough to continue.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Layouts
|
|
315
|
+
|
|
316
|
+
Layouts let you define a reusable HTML shell and inject page content into it. They're a Mutor.js feature that Lynnix makes available across your entire application.
|
|
317
|
+
|
|
318
|
+
### Declaring a Layout
|
|
319
|
+
|
|
320
|
+
Any template that starts with `{{# layout "name" }}` is registered as a named layout at startup. The `{{ ::slot }}` tag marks where page content gets injected.
|
|
321
|
+
|
|
322
|
+
```html
|
|
323
|
+
<!-- app/dashboard/layout.html -->
|
|
324
|
+
{{# layout "dashboard_layout" }}
|
|
325
|
+
|
|
326
|
+
<!doctype html>
|
|
327
|
+
<html lang="en">
|
|
328
|
+
<head>
|
|
329
|
+
<title>{{ data.title }}</title>
|
|
330
|
+
</head>
|
|
331
|
+
<body>
|
|
332
|
+
<aside><!-- sidebar --></aside>
|
|
333
|
+
<main>{{ ::slot }}</main>
|
|
334
|
+
</body>
|
|
335
|
+
</html>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Using a Layout
|
|
339
|
+
|
|
340
|
+
Any page or fragment that starts with `{{# use "name" }}` is rendered inside that layout. No boilerplate, no repeated markup.
|
|
341
|
+
|
|
342
|
+
```html
|
|
343
|
+
<!-- app/dashboard/page.html -->
|
|
344
|
+
{{# use "dashboard_layout" }}
|
|
345
|
+
|
|
346
|
+
<h1>Welcome, {{ data.user.name }}</h1>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
The filename doesn't matter to Lynnix — `layout.html` is just a convention. What matters is the `{{# layout }}` declaration inside the file.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Fragments (htmx Partial Rendering)
|
|
354
|
+
|
|
355
|
+
When htmx makes a request, it sends an `HX-Request: true` header. Lynnix detects this automatically and renders `fragment.html` instead of `page.html`, giving you clean partial responses without any conditional logic in your loader.
|
|
356
|
+
|
|
357
|
+
```html
|
|
358
|
+
<!-- app/posts/fragment.html -->
|
|
359
|
+
<div id="posts-list">
|
|
360
|
+
{{ for post of data.posts }}
|
|
361
|
+
<article>{{ post.title }}</article>
|
|
362
|
+
{{ endfor }}
|
|
363
|
+
</div>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Your loader doesn't need to change — the same return value feeds both `page.html` and `fragment.html`. If a route has no `fragment.html`, Lynnix returns an empty `200` for htmx requests.
|
|
367
|
+
|
|
368
|
+
You can also check `req.isHtmx` in your loader if you need to branch on request type:
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
export function GET(req) {
|
|
372
|
+
if (!req.isHtmx) {
|
|
373
|
+
return { title: "Posts", posts: db.posts.all() };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return { posts: db.posts.all() };
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Error Handling
|
|
383
|
+
|
|
384
|
+
### Not Found
|
|
385
|
+
|
|
386
|
+
Throw a `NotFoundError` from any loader or middleware to render the nearest `not-found.html` boundary up the directory tree.
|
|
387
|
+
|
|
388
|
+
```js
|
|
389
|
+
import { NotFoundError } from "lynnix";
|
|
390
|
+
|
|
391
|
+
export function GET(req) {
|
|
392
|
+
const post = db.posts.find(req.params.slug);
|
|
393
|
+
|
|
394
|
+
if (!post) {
|
|
395
|
+
throw new NotFoundError();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { post };
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
For htmx requests, Lynnix serves `fragment.not-found.html` instead. If no boundary is found, Lynnix returns a plain `404`.
|
|
403
|
+
|
|
404
|
+
### HTTP Errors
|
|
405
|
+
|
|
406
|
+
Throw an `HttpError` with a status code and optional metadata for any other error scenario.
|
|
407
|
+
|
|
408
|
+
```js
|
|
409
|
+
import { HttpError } from "lynnix";
|
|
410
|
+
|
|
411
|
+
export function GET(req) {
|
|
412
|
+
if (!req.user.isAdmin) {
|
|
413
|
+
throw new HttpError(403, { message: "Admins only" });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
In your error template, you have access to `{{ error }}`, `{{ pathname }}`, and `{{ data }}` (the metadata you passed in).
|
|
419
|
+
|
|
420
|
+
```html
|
|
421
|
+
<!-- app/error.html -->
|
|
422
|
+
<h1>{{ error.code }}</h1>
|
|
423
|
+
<p>{{ data.message }}</p>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Boundary Resolution
|
|
427
|
+
|
|
428
|
+
Lynnix walks up the directory tree from the current route to find the nearest error or not-found boundary. This means a `not-found.html` at `app/dashboard/not-found.html` catches 404s for any unmatched route under `/dashboard`, while `app/not-found.html` serves as the global fallback.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Request API
|
|
433
|
+
|
|
434
|
+
The `req` object passed to loaders and middleware implements `LynnixServerRequest`:
|
|
435
|
+
|
|
436
|
+
| Property | Type | Description |
|
|
437
|
+
|---|---|---|
|
|
438
|
+
| `req.raw` | `http.IncomingMessage` | The underlying Node.js request object |
|
|
439
|
+
| `req.body` | `Record<string, unknown>` | Parsed request body |
|
|
440
|
+
| `req.files` | `LynnixUploadedFiles` | Uploaded files (multipart only) |
|
|
441
|
+
| `req.cookies` | `Record<string, string>` | Parsed request cookies (requires `cookie`) |
|
|
442
|
+
| `req.params` | `Record<string, string>` | Dynamic and catch-all route parameters |
|
|
443
|
+
| `req.query` | `Record<string, unknown>` | Parsed query string |
|
|
444
|
+
| `req.htmx` | `Record<string, string>` | All `hx-*` request headers |
|
|
445
|
+
| `req.isHtmx` | `boolean` | `true` if the request was made by htmx |
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Response API
|
|
450
|
+
|
|
451
|
+
The `res` object passed to loaders and middleware implements `LynnixServerResponse`:
|
|
452
|
+
|
|
453
|
+
### Core
|
|
454
|
+
|
|
455
|
+
| Method | Description |
|
|
456
|
+
|---|---|
|
|
457
|
+
| `res.status(code)` | Set the HTTP status code. Returns `this` for chaining. |
|
|
458
|
+
| `res.end(value?)` | End the response, optionally with a body. |
|
|
459
|
+
| `res.html(content)` | Send an HTML response with the correct `Content-Type`. |
|
|
460
|
+
| `res.json(content)` | Send a JSON response with the correct `Content-Type`. |
|
|
461
|
+
| `res.redirect(url, permanent?)` | Redirect the client. htmx-aware — sets `HX-Redirect` for htmx requests. Pass `true` for a `301` permanent redirect. |
|
|
462
|
+
|
|
463
|
+
### Cookies
|
|
464
|
+
|
|
465
|
+
| Method | Description |
|
|
466
|
+
|---|---|
|
|
467
|
+
| `res.setCookie(name, value, options)` | Set a cookie. Requires the `cookie` peer dependency. |
|
|
468
|
+
| `res.deleteCookie(name)` | Delete a cookie by setting it as expired. Requires the `cookie` peer dependency. |
|
|
469
|
+
| `res.cookies` | The current response cookies as a plain object. |
|
|
470
|
+
|
|
471
|
+
### htmx Response Headers
|
|
472
|
+
|
|
473
|
+
These methods are no-ops for non-htmx requests, so you can call them freely without checking `req.isHtmx`.
|
|
474
|
+
|
|
475
|
+
| Method | Description |
|
|
476
|
+
|---|---|
|
|
477
|
+
| `res.htmxTrigger(event)` | Trigger a client-side event via `HX-Trigger`. |
|
|
478
|
+
| `res.htmxTriggerAfterSwap(event)` | Trigger an event after the swap via `HX-Trigger-After-Swap`. |
|
|
479
|
+
| `res.htmxTriggerAfterSettle(event)` | Trigger an event after settle via `HX-Trigger-After-Settle`. |
|
|
480
|
+
| `res.htmxPush(url)` | Push a URL to the browser history via `HX-Push-Url`. Pass `false` to prevent pushing. |
|
|
481
|
+
| `res.htmxReplaceUrl(url)` | Replace the current URL via `HX-Replace-Url`. |
|
|
482
|
+
| `res.htmxRedirect(url)` | Client-side redirect via `HX-Redirect`. |
|
|
483
|
+
| `res.htmxLocation(location)` | Navigate without a full page reload via `HX-Location`. |
|
|
484
|
+
| `res.htmxReswap(strategy)` | Override the swap strategy via `HX-Reswap`. |
|
|
485
|
+
| `res.htmxRetarget(selector)` | Override the target element via `HX-Retarget`. |
|
|
486
|
+
| `res.htmxReselect(selector)` | Override the select expression via `HX-Reselect`. |
|
|
487
|
+
| `res.htmxRefresh()` | Trigger a full page refresh via `HX-Refresh`. |
|
|
488
|
+
|
|
489
|
+
### Raw Access
|
|
490
|
+
|
|
491
|
+
`res.raw` gives you direct access to the underlying `http.ServerResponse` for anything Lynnix doesn't cover.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Framework Integration
|
|
496
|
+
|
|
497
|
+
Lynnix needs two things from the framework you're using: a request object with `headers` and a response object with `end` and `setHeader`. Anything that provides those works.
|
|
498
|
+
|
|
499
|
+
The `req.raw` and `res.raw` escape hatches expose the underlying objects directly. Any code that touches `.raw` is framework-specific — keep that in mind when writing loaders you want to stay portable.
|
|
500
|
+
|
|
501
|
+
### Express
|
|
502
|
+
|
|
503
|
+
Express works out of the box. Mount Lynnix as middleware after your static file and body parser middleware.
|
|
504
|
+
|
|
505
|
+
```js
|
|
506
|
+
app.use(express.static("public"));
|
|
507
|
+
app.use(express.urlencoded({ extended: true }));
|
|
508
|
+
app.use(express.json());
|
|
509
|
+
app.use(cookieParser());
|
|
510
|
+
app.use(await createLynnixApp("app"));
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
> **Note:** If you're using Express's body parsing middleware, Lynnix's built-in body parser will defer to it automatically. You don't need both.
|
|
514
|
+
|
|
515
|
+
### Bare `node:http`
|
|
516
|
+
|
|
517
|
+
Use `send-static` for static files and let Lynnix handle everything else. See [Getting Started](#getting-started).
|
|
518
|
+
|
|
519
|
+
### Fastify
|
|
520
|
+
|
|
521
|
+
Fastify's `reply` object exposes `reply.raw` for the underlying `ServerResponse`. Pass `req.raw` and `reply.raw` to the Lynnix handler:
|
|
522
|
+
|
|
523
|
+
```js
|
|
524
|
+
fastify.all("/*", (req, reply) => {
|
|
525
|
+
handler(req.raw, reply.raw);
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Cookie handling differs between frameworks — if you're using Fastify, install `@fastify/cookie` and set cookies via `res.raw` directly, or use Lynnix's built-in `res.setCookie` with the `cookie` peer dependency.
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Built With
|
|
534
|
+
|
|
535
|
+
- [Mutor.js](https://github.com/allAboutJS/Mutor) — the template engine powering Lynnix's rendering layer
|
|
536
|
+
- [htmx](https://htmx.org) — the hypermedia library Lynnix is designed around
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## License
|
|
541
|
+
|
|
542
|
+
MIT © [Onah Victor](https://github.com/allAboutJS)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lynnix
|
|
3
|
+
* File-based hypermedia routing middleware for Node.js, Mutor.js, and HTMX.
|
|
4
|
+
*
|
|
5
|
+
* @author Onah Victor <victoronah.dev@gmail.com>
|
|
6
|
+
* @repository https://github.com/allAboutJS/Lynnix
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
import createLynnixApp from "./lynnix.js";
|
|
10
|
+
import type LynnixRequest from "./utils/lynnixRequest.js";
|
|
11
|
+
import type LynnixResponse from "./utils/lynnixResponse.js";
|
|
12
|
+
export type { MutorConfig, PartialMutorConfig } from "mutorjs/server";
|
|
13
|
+
export type { CookieOptions, HtmxHeaders, LynnixServerRequest, LynnixServerResponse, LynnixUploadedFiles, ParsedRequestBody, QsModule, } from "./types.js";
|
|
14
|
+
export { HttpError, NotFoundError } from "./utils/error.js";
|
|
15
|
+
export { createLynnixApp, type LynnixRequest, type LynnixResponse };
|
|
16
|
+
export default createLynnixApp;
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,eAAe,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,aAAa,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,cAAc,MAAM,2BAA2B,CAAC;AAE5D,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACtE,YAAY,EACX,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,QAAQ,GACR,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,CAAC;AAEpE,eAAe,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lynnix
|
|
3
|
+
* File-based hypermedia routing middleware for Node.js, Mutor.js, and HTMX.
|
|
4
|
+
*
|
|
5
|
+
* @author Onah Victor <victoronah.dev@gmail.com>
|
|
6
|
+
* @repository https://github.com/allAboutJS/Lynnix
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
import createLynnixApp from "./lynnix.js";
|
|
10
|
+
export { HttpError, NotFoundError } from "./utils/error.js";
|
|
11
|
+
export { createLynnixApp };
|
|
12
|
+
export default createLynnixApp;
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,eAAe,MAAM,aAAa,CAAC;AAe1C,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAA2C,CAAC;AAEpE,eAAe,eAAe,CAAC"}
|
package/dist/lynnix.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lynnix
|
|
3
|
+
* File-based hypermedia routing middleware for Node.js, Mutor.js, and HTMX.
|
|
4
|
+
*
|
|
5
|
+
* @author Onah Victor <victoronah.dev@gmail.com>
|
|
6
|
+
* @repository https://github.com/allAboutJS/Lynnix
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
import type * as http from "node:http";
|
|
10
|
+
import { type PartialMutorConfig } from "mutorjs/server";
|
|
11
|
+
import type { ParseReqBodyOptions } from "./types.js";
|
|
12
|
+
/**
|
|
13
|
+
* @param path The root directory of the mutor instance
|
|
14
|
+
* @returns A request handler function that serves the mutor instance
|
|
15
|
+
* @example
|
|
16
|
+
*
|
|
17
|
+
* import createLynnixApp from "lynnix";
|
|
18
|
+
* import * as http from "node:http";
|
|
19
|
+
* import sendStatic from "serve-static";
|
|
20
|
+
*
|
|
21
|
+
* const handler = createLynnixApp("app");
|
|
22
|
+
* const serve = serveStatic("./public", { index: false });
|
|
23
|
+
*
|
|
24
|
+
* const server = http.createServer((req, res) => {
|
|
25
|
+
* serve(req, res, () => handler(req, res));
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // OR
|
|
29
|
+
*
|
|
30
|
+
* import express from "express";
|
|
31
|
+
*
|
|
32
|
+
* const server = express();
|
|
33
|
+
* const handler = createLynnixApp("app");
|
|
34
|
+
*
|
|
35
|
+
* server.use(express.urlencoded({ extended: true }));
|
|
36
|
+
* server.use(express.json());
|
|
37
|
+
* server.static("./public");
|
|
38
|
+
*
|
|
39
|
+
* server.use(handler);
|
|
40
|
+
*
|
|
41
|
+
* // FINALLY
|
|
42
|
+
*
|
|
43
|
+
* server.listen(3000);
|
|
44
|
+
*/
|
|
45
|
+
export default function createLynnixApp(path: string, mutorConfig?: Omit<PartialMutorConfig, "rootDir">, bodyParserOptions?: ParseReqBodyOptions): Promise<(_req: http.IncomingMessage, _res: http.ServerResponse<http.IncomingMessage>) => Promise<http.ServerResponse<http.IncomingMessage>>>;
|
|
46
|
+
//# sourceMappingURL=lynnix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lynnix.d.ts","sourceRoot":"","sources":["../src/lynnix.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAEvC,OAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AActD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAA8B,eAAe,CAC5C,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAM,EACrD,iBAAiB,GAAE,mBAAwB,kBAmBpC,IAAI,CAAC,eAAe,QACpB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,eAAe,CAAC,yDA4KhD"}
|