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 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.5",
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",
@@ -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,2 @@
1
+ <h1>About</h1>
2
+ <p>This is an example single-page application powered by Kempo-Server.</p>
@@ -0,0 +1,2 @@
1
+ <h1>Contact</h1>
2
+ <p>Get in touch at <a href="mailto:hello@example.com">hello@example.com</a>.</p>
@@ -0,0 +1 @@
1
+ <h1>Hello <code>index.html</code></h1>
@@ -0,0 +1 @@
1
+ <h1>Hello <code>page1.html</code></h1>
@@ -0,0 +1,2 @@
1
+ <h1>Settings</h1>
2
+ <p>Configure your preferences here.</p>
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);