kempo-server 1.10.5 → 1.10.7
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 +6 -0
- package/SPA.md +153 -0
- package/llm.txt +140 -0
- package/package.json +2 -3
- package/spa/.config.json +15 -0
- package/spa/app.html +20 -0
- package/spa/pages/about.html +2 -0
- package/spa/pages/contact.html +2 -0
- package/spa/pages/index.html +1 -0
- package/spa/pages/page1.html +1 -0
- package/spa/pages/settings.html +2 -0
- package/spa/spa.js +32 -0
package/README.md
CHANGED
|
@@ -366,6 +366,12 @@ npm run tests:gui # Start the GUI test runner
|
|
|
366
366
|
For advanced usage (filters, flags, GUI options), see:
|
|
367
367
|
https://github.com/dustinpoissant/kempo-testing-framework
|
|
368
368
|
|
|
369
|
+
## Single-Page Applications
|
|
370
|
+
|
|
371
|
+
Kempo Server makes it easy to serve SPAs by using `customRoutes` to redirect all HTML requests to your shell page while still serving individual page fragments from a `pages/` directory.
|
|
372
|
+
|
|
373
|
+
See **[SPA.md](./SPA.md)** for a full walkthrough.
|
|
374
|
+
|
|
369
375
|
## Documentation
|
|
370
376
|
|
|
371
377
|
- **[Getting Started](./docs/getting-started.html)** - Installation and basic usage
|
package/SPA.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Single-Page Applications
|
|
2
|
+
|
|
3
|
+
Kempo Server supports single-page applications (SPAs) with no framework required. The approach uses `customRoutes` to redirect every HTML URL to one shell page, while individual page content lives as HTML fragments in a `pages/` directory. Client-side JavaScript handles navigation using the History API.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
1. All top-level HTML requests (e.g. `/about.html`) are redirected to `app.html` via `customRoutes`.
|
|
8
|
+
2. `app.html` is the shell — it contains the `<nav>`, a `<main>` content area, and loads `spa.js`.
|
|
9
|
+
3. `spa.js` reads the current URL, fetches the matching fragment from `/pages/`, and injects it into `<main>`.
|
|
10
|
+
4. Click events on `<a>` tags are intercepted to update the URL with `history.pushState` and load content without a full page reload.
|
|
11
|
+
5. The `popstate` event handles browser back/forward navigation.
|
|
12
|
+
|
|
13
|
+
Requests to `/pages/*.html` are **not** redirected, so the fragments are still served directly by the server.
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
spa/
|
|
19
|
+
├─ .config.json ← server config (routes + caching)
|
|
20
|
+
├─ app.html ← shell page (loaded for every route)
|
|
21
|
+
├─ spa.js ← client-side routing logic
|
|
22
|
+
└─ pages/
|
|
23
|
+
├─ index.html ← home page fragment
|
|
24
|
+
├─ page1.html
|
|
25
|
+
├─ about.html
|
|
26
|
+
├─ contact.html
|
|
27
|
+
└─ settings.html
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Create a `.config.json` in your SPA root:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"customRoutes": {
|
|
37
|
+
"/kempo.css": "../node_modules/kempo-css/dist/kempo.min.css",
|
|
38
|
+
"/*.html": "./app.html",
|
|
39
|
+
"/": "./app.html"
|
|
40
|
+
},
|
|
41
|
+
"middleware": {
|
|
42
|
+
"security": {
|
|
43
|
+
"enabled": true,
|
|
44
|
+
"headers": {
|
|
45
|
+
"Cache-Control": "public, max-age=3600"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `"/*.html": "./app.html"` — the `*` wildcard matches a single path segment, so it catches `/about.html` but not `/pages/about.html`.
|
|
53
|
+
- `"/": "./app.html"` — handles requests to the root.
|
|
54
|
+
- The `Cache-Control` header tells browsers to cache responses for 1 hour.
|
|
55
|
+
|
|
56
|
+
## Shell Page (`app.html`)
|
|
57
|
+
|
|
58
|
+
The shell page is a minimal HTML document. Navigation links use absolute paths so they work regardless of the current URL. The `<main>` element is where page content gets injected.
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<!doctype html>
|
|
62
|
+
<html lang="en">
|
|
63
|
+
<head>
|
|
64
|
+
<meta charset="UTF-8">
|
|
65
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
66
|
+
<title>My SPA</title>
|
|
67
|
+
<link rel="stylesheet" href="/kempo.css" />
|
|
68
|
+
</head>
|
|
69
|
+
<body>
|
|
70
|
+
<nav>
|
|
71
|
+
<a href="/">Home</a>
|
|
72
|
+
<a href="/about.html">About</a>
|
|
73
|
+
<a href="/contact.html">Contact</a>
|
|
74
|
+
</nav>
|
|
75
|
+
<main id="main"></main>
|
|
76
|
+
<script src="/spa.js"></script>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Client-Side Router (`spa.js`)
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
const main = document.getElementById("main");
|
|
85
|
+
|
|
86
|
+
const loadPage = path => {
|
|
87
|
+
const page = (path === "/" || path === "/index.html")
|
|
88
|
+
? "/pages/index.html"
|
|
89
|
+
: `/pages${path}`;
|
|
90
|
+
fetch(page)
|
|
91
|
+
.then(r => {
|
|
92
|
+
if(!r.ok) throw new Error(r.status);
|
|
93
|
+
return r.text();
|
|
94
|
+
})
|
|
95
|
+
.then(html => {
|
|
96
|
+
main.innerHTML = html;
|
|
97
|
+
})
|
|
98
|
+
.catch(() => {
|
|
99
|
+
main.innerHTML = "<h1>Page Not Found</h1>";
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
document.addEventListener("click", e => {
|
|
104
|
+
const a = e.target.closest("a");
|
|
105
|
+
if(!a || a.origin !== location.origin) return;
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
history.pushState(null, "", a.href);
|
|
108
|
+
loadPage(a.pathname);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
window.addEventListener("popstate", () => {
|
|
112
|
+
loadPage(location.pathname);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
loadPage(location.pathname);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### How it works
|
|
119
|
+
|
|
120
|
+
- `loadPage` maps the URL pathname to a fragment file under `/pages/` and fetches it. The root path `/` maps to `/pages/index.html`.
|
|
121
|
+
- The `click` listener intercepts same-origin link clicks, prevents the default navigation, pushes the new URL into the browser history, and loads the content.
|
|
122
|
+
- The `popstate` listener handles back/forward button presses.
|
|
123
|
+
- On initial load, `loadPage(location.pathname)` renders the correct content for any URL — including direct navigation and browser refreshes.
|
|
124
|
+
|
|
125
|
+
## Page Fragments
|
|
126
|
+
|
|
127
|
+
Each file inside `pages/` is a plain HTML fragment — just the content, no `<html>` or `<body>` wrapper needed:
|
|
128
|
+
|
|
129
|
+
```html
|
|
130
|
+
<!-- pages/about.html -->
|
|
131
|
+
<h1>About</h1>
|
|
132
|
+
<p>This is an example single-page application powered by Kempo-Server.</p>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Running the SPA
|
|
136
|
+
|
|
137
|
+
Add a script to your `package.json`:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"scripts": {
|
|
142
|
+
"spa": "kempo-server --root ./spa"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Then run:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npm run spa
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The server starts on `http://localhost:3000` by default. All `.html` URLs load `app.html` and client-side routing takes over from there.
|
package/llm.txt
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# kempo-server
|
|
2
|
+
|
|
3
|
+
Zero-dependency, file-based routing Node.js HTTP server.
|
|
4
|
+
|
|
5
|
+
## Install & Run
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install kempo-server
|
|
9
|
+
npx kempo-server -r ./my-root -p 3000
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Flags: `--root`/`-r` (default `./`), `--port`/`-p` (default `3000`), `--logging`/`-l` (0-4 or silent/minimal/verbose/debug, default 2), `--config`/`-c` path to config file (default `.config.json`).
|
|
13
|
+
|
|
14
|
+
## File-Based Routing
|
|
15
|
+
|
|
16
|
+
Files inside the root directory map directly to URL paths.
|
|
17
|
+
|
|
18
|
+
| File | URL |
|
|
19
|
+
|------|-----|
|
|
20
|
+
| `index.html` | `/` |
|
|
21
|
+
| `about.html` | `/about` or `/about.html` |
|
|
22
|
+
| `api/GET.js` | `GET /api` |
|
|
23
|
+
| `api/POST.js` | `POST /api` |
|
|
24
|
+
| `users/[id]/GET.js` | `GET /users/:id` |
|
|
25
|
+
|
|
26
|
+
Route files checked in order for a directory request: `METHOD.js`, `METHOD.html`, `index.js`, `index.html`.
|
|
27
|
+
|
|
28
|
+
Allowed route file names: `GET.js POST.js PUT.js DELETE.js HEAD.js OPTIONS.js PATCH.js CONNECT.js TRACE.js index.js`
|
|
29
|
+
|
|
30
|
+
## Route Files
|
|
31
|
+
|
|
32
|
+
A route file exports a default async function:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
export default async (req, res) => {
|
|
36
|
+
res.json({ id: req.params.id, search: req.query.q });
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Request Object
|
|
41
|
+
|
|
42
|
+
| Property/Method | Description |
|
|
43
|
+
|-----------------|-------------|
|
|
44
|
+
| `req.params` | Dynamic path params from `[param]` segments |
|
|
45
|
+
| `req.query` | Parsed query string as plain object |
|
|
46
|
+
| `req.path` | URL path string |
|
|
47
|
+
| `req.cookies` | Parsed cookies as plain object |
|
|
48
|
+
| `req.body()` | Promise→string of request body |
|
|
49
|
+
| `req.json()` | Promise→parsed JSON body |
|
|
50
|
+
| `req.text()` | Alias for `body()` |
|
|
51
|
+
| `req.buffer()` | Promise→Buffer of request body |
|
|
52
|
+
| `req.get(name)` | Get request header (case-insensitive) |
|
|
53
|
+
| `req.is(type)` | Check Content-Type (e.g. `'json'`) |
|
|
54
|
+
|
|
55
|
+
## Response Object
|
|
56
|
+
|
|
57
|
+
| Method | Description |
|
|
58
|
+
|--------|-------------|
|
|
59
|
+
| `res.status(code)` | Set status code, chainable |
|
|
60
|
+
| `res.set(field, value)` | Set header(s), chainable; accepts object |
|
|
61
|
+
| `res.get(field)` | Get response header |
|
|
62
|
+
| `res.type(t)` | Set Content-Type; shortcuts: html/json/xml/text/css/js |
|
|
63
|
+
| `res.json(obj)` | Send JSON response |
|
|
64
|
+
| `res.send(data)` | Auto-detect: Buffer/object→json, string→text |
|
|
65
|
+
| `res.html(str)` | Send HTML response |
|
|
66
|
+
| `res.text(str)` | Send plain text response |
|
|
67
|
+
| `res.redirect(url, code=302)` | Redirect |
|
|
68
|
+
| `res.cookie(name, val, opts)` | Set cookie; opts: maxAge, domain, path, secure, httpOnly, sameSite |
|
|
69
|
+
| `res.clearCookie(name, opts)` | Clear cookie |
|
|
70
|
+
|
|
71
|
+
## Configuration (.config.json)
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"allowedMimes": { ".ext": { "mime": "type/subtype", "encoding": "utf8" } },
|
|
76
|
+
"disallowedRegex": ["pattern"],
|
|
77
|
+
"customRoutes": { "/old": "/new", "/spa/*": "/app.html" },
|
|
78
|
+
"routeFiles": ["GET.js", "index.js"],
|
|
79
|
+
"noRescanPaths": ["pattern"],
|
|
80
|
+
"maxRescanAttempts": 3,
|
|
81
|
+
"middleware": {
|
|
82
|
+
"cors": { "enabled": false, "origin": "*", "methods": "GET,POST", "headers": "*" },
|
|
83
|
+
"compression": { "enabled": false, "threshold": 1024 },
|
|
84
|
+
"rateLimit": { "enabled": false, "windowMs": 60000, "max": 100 },
|
|
85
|
+
"security": { "enabled": true, "headers": { "X-Frame-Options": "DENY" } },
|
|
86
|
+
"logging": { "enabled": true },
|
|
87
|
+
"custom": ["/middleware/auth.js"]
|
|
88
|
+
},
|
|
89
|
+
"cache": {
|
|
90
|
+
"enabled": false,
|
|
91
|
+
"maxSize": 500,
|
|
92
|
+
"maxMemoryMB": 256,
|
|
93
|
+
"ttlMs": 300000,
|
|
94
|
+
"watchFiles": true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`customRoutes` values: exact path string (redirect) or `{ "file": "/path.js", "params": {} }` (custom handler file). `*` matches one path segment; `**` matches multiple.
|
|
100
|
+
|
|
101
|
+
## Custom Middleware
|
|
102
|
+
|
|
103
|
+
Middleware files export a default factory:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
export default config => async (req, res, next) => {
|
|
107
|
+
// req/res are the enhanced wrappers
|
|
108
|
+
await next();
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Dynamic Routes
|
|
113
|
+
|
|
114
|
+
Use `[param]` in directory names:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
users/[id]/GET.js → GET /users/123 (req.params.id === '123')
|
|
118
|
+
posts/[slug]/index.js → /posts/my-title (req.params.slug === 'my-title')
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## SPA Mode
|
|
122
|
+
|
|
123
|
+
Use `customRoutes` to redirect all page paths to the SPA entry:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
"customRoutes": {
|
|
127
|
+
"/": "/app.html",
|
|
128
|
+
"/*.html": "/app.html"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Utility Modules
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
import { getArgs } from 'kempo-server/utils/cli';
|
|
136
|
+
// getArgs(mapping?) → parsed argv object
|
|
137
|
+
|
|
138
|
+
import { ensureDir, copyDir, emptyDir } from 'kempo-server/utils/fs-utils';
|
|
139
|
+
```
|
|
140
|
+
|
package/package.json
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.10.
|
|
4
|
+
"version": "1.10.7",
|
|
5
5
|
"description": "A lightweight, zero-dependency, file based routing server.",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
6
|
"exports": {
|
|
8
|
-
".": "./dist/index.js",
|
|
9
7
|
"./utils/cli": "./dist/utils/cli.js",
|
|
10
8
|
"./utils/fs-utils": "./dist/utils/fs-utils.js"
|
|
11
9
|
},
|
|
@@ -15,6 +13,7 @@
|
|
|
15
13
|
"scripts": {
|
|
16
14
|
"build": "node scripts/build.js",
|
|
17
15
|
"docs": "node dist/index.js -r ./docs",
|
|
16
|
+
"spa": "node dist/index.js -r ./spa",
|
|
18
17
|
"test": "npx kempo-test",
|
|
19
18
|
"test:gui": "npx kempo-test --gui",
|
|
20
19
|
"test:browser": "npx kempo-test -b",
|
package/spa/.config.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"customRoutes": {
|
|
3
|
+
"/kempo.css": "../node_modules/kempo-css/dist/kempo.min.css",
|
|
4
|
+
"/*.html": "./app.html",
|
|
5
|
+
"/": "./app.html"
|
|
6
|
+
},
|
|
7
|
+
"middleware": {
|
|
8
|
+
"security": {
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"headers": {
|
|
11
|
+
"Cache-Control": "public, max-age=3600"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
package/spa/app.html
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Kempo-Server SPA</title>
|
|
7
|
+
<link rel="stylesheet" href="/kempo.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<nav>
|
|
11
|
+
<a href="/">Home</a>
|
|
12
|
+
<a href="/page1.html">Page 1</a>
|
|
13
|
+
<a href="/about.html">About</a>
|
|
14
|
+
<a href="/contact.html">Contact</a>
|
|
15
|
+
<a href="/settings.html">Settings</a>
|
|
16
|
+
</nav>
|
|
17
|
+
<main id="main"></main>
|
|
18
|
+
<script src="/spa.js"></script>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>Hello <code>index.html</code></h1>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>Hello <code>page1.html</code></h1>
|
package/spa/spa.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const main = document.getElementById("main");
|
|
2
|
+
|
|
3
|
+
const loadPage = path => {
|
|
4
|
+
const page = (path === "/" || path === "/index.html")
|
|
5
|
+
? "/pages/index.html"
|
|
6
|
+
: `/pages${path}`;
|
|
7
|
+
fetch(page)
|
|
8
|
+
.then(r => {
|
|
9
|
+
if(!r.ok) throw new Error(r.status);
|
|
10
|
+
return r.text();
|
|
11
|
+
})
|
|
12
|
+
.then(html => {
|
|
13
|
+
main.innerHTML = html;
|
|
14
|
+
})
|
|
15
|
+
.catch(() => {
|
|
16
|
+
main.innerHTML = "<h1>Page Not Found</h1>";
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
document.addEventListener("click", e => {
|
|
21
|
+
const a = e.target.closest("a");
|
|
22
|
+
if(!a || a.origin !== location.origin) return;
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
history.pushState(null, "", a.href);
|
|
25
|
+
loadPage(a.pathname);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
window.addEventListener("popstate", () => {
|
|
29
|
+
loadPage(location.pathname);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
loadPage(location.pathname);
|