koa-classic-server 4.2.0 → 5.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.
Files changed (3) hide show
  1. package/README.md +77 -32
  2. package/index.cjs +578 -162
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -6,8 +6,9 @@ traditional web server, but intentionally its own thing.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/koa-classic-server.svg)](https://www.npmjs.com/package/koa-classic-server)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
- [![Tests](https://img.shields.io/badge/tests-717%20passing-brightgreen.svg)]()
10
- [![Node](https://img.shields.io/badge/node-%3E%3D18-blue.svg)]()
9
+ [![Tests](https://img.shields.io/badge/tests-1239%20passing-brightgreen.svg)]()
10
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-blue.svg)]()
11
+ [![Koa](https://img.shields.io/badge/koa-%3E%3D3.1.2-blue.svg)]()
11
12
 
12
13
  One rule drives every default:
13
14
 
@@ -25,7 +26,7 @@ Hardening is opt-in.
25
26
  npm install koa-classic-server
26
27
  ```
27
28
 
28
- Requires **Node ≥ 18** and **Koa ≥ 2** (Koa 3 needs 3.1.2).
29
+ Requires **Node ≥ 20** and **Koa ≥ 3.1.2**. (Koa 2 and Node 18 were supported through v4.x; both were dropped in v5.0.0.)
29
30
 
30
31
  ### Import
31
32
 
@@ -92,7 +93,7 @@ const ejs = require('ejs');
92
93
  app.use(koaClassicServer(path.join(__dirname, 'views'), {
93
94
  hideExtension: { ext: '.ejs' }, // GET /about → views/about.ejs ; GET /about.ejs → 301 /about
94
95
  template: {
95
- ext: ['ejs'],
96
+ ext: ['.ejs'], // leading dot optional since v5: '.ejs' (preferred) ≡ 'ejs'
96
97
  // signature: (ctx, next, filePath, rawBuffer, signal)
97
98
  render: async (ctx, next, filePath, rawBuffer, signal) => {
98
99
  ctx.type = 'html';
@@ -134,6 +135,11 @@ app.use(koaClassicServer(root, {
134
135
  app.use(koaClassicServer(root, { compression: false }));
135
136
  ```
136
137
 
138
+ To size compression quality and the server-side caches to your host's RAM and CPU
139
+ (small VPS, weak CPU, big dedicated box, behind a CDN), see the
140
+ **[Performance Tuning Guide](./docs/PERFORMANCE_TUNING.md)** — it includes
141
+ copy-paste profiles.
142
+
137
143
  ### Hide sensitive files
138
144
 
139
145
  Dot-files are **served by default** (the operator's directory is the source of truth). For a
@@ -252,21 +258,32 @@ app.use(koaClassicServer(path.join(__dirname, 'public'), {
252
258
 
253
259
  ---
254
260
 
255
- ## What's new in v4
256
-
257
- **v4.0.0** is the robustness release. One behavior change made it a major:
258
-
259
- - **Canonical trailing slash** (default on): `GET /dir` → `301 /dir/`, and `GET /file/` → `404`,
260
- so relative links in an index page resolve correctly. Restore the old behavior with
261
- `dirListing: { trailingSlash: false }`.
262
- - **Bounded compression** — `compression.maxFileSize` (10 MB) caps the buffered high-quality path;
263
- larger files still compress, via bounded-RAM streaming. Opt out with `maxFileSize: false`.
264
- - **Stricter option validation** — a non-object `options` argument now throws instead of coercing.
265
-
266
- Plus HTTP-conformance fixes across content negotiation (`Accept-Encoding` q-values, `Vary`),
267
- conditional requests (`If-None-Match` lists/`*`/weak, precedence over `Range`), and safer
268
- config validation (malformed `urlPrefix` / `urlsReserved` / `browserCacheMaxAge` now warn, and
269
- will throw in the next major). Full details in the **[changelog](./docs/CHANGELOG.md)**.
261
+ ## What's new in v5
262
+
263
+ **v5.0.0** is the configuration-correctness release: config mistakes that used to
264
+ misbehave silently at request time now fail fast at startup, and the two extension
265
+ options finally share one coherent, forgiving syntax.
266
+
267
+ - **`hideExtension.redirect` is validated at startup.** Accepted values are the real
268
+ redirect codes — `300, 301, 302, 303, 305, 307, 308` and anything else **throws at
269
+ factory time** with the valid list in the message. Until v4 a wrong value failed
270
+ silently in production: a non-redirect integer (`200`, `404`, `999`, …) was quietly
271
+ sent as `302`, and a non-integer value produced a **500 on every redirect**.
272
+ - **The leading dot is optional in `template.ext` and `hideExtension.ext`** — `'.ejs'`
273
+ and `'ejs'` are equivalent everywhere; the preferred, documented form is **`'.ejs'`**.
274
+ This also fixes a long-standing trap: a dotted `template.ext` entry (`['.ejs']`) used
275
+ to never match, so the render never ran and the **template source was served raw**.
276
+ It now works as intended.
277
+ - **`template.ext` matches by suffix** (as `hideExtension.ext` always did), so compound
278
+ extensions are supported: `['.html.ejs']` targets only `*.html.ejs` files. Entries
279
+ that cannot name a suffix (empty, `'.'`, non-string) are dropped with a one-time
280
+ warning.
281
+ - **A non-function `template.render` now warns** (one-time) instead of silently
282
+ disabling template rendering.
283
+
284
+ The v4 highlights — canonical trailing slash, bounded compression with streamed-output
285
+ caching, configurable compression quality, custom error pages — are all still here.
286
+ Full details in the **[changelog](./docs/CHANGELOG.md)**.
270
287
 
271
288
  ---
272
289
 
@@ -291,7 +308,10 @@ koaClassicServer(rootDir, {
291
308
  urlsReserved: [], // e.g. ['/api'] — first-level, passed to next()
292
309
  useOriginalUrl: true, // false when an upstream middleware rewrites ctx.url
293
310
 
294
- hideExtension: { ext: '.ejs', redirect: 301 }, // clean URLs
311
+ hideExtension: { // clean URLs
312
+ ext: '.ejs', // suffix to hide — leading dot optional (v5), compound ('.tar.gz') ok
313
+ redirect: 301, // v5: must be 300|301|302|303|305|307|308, else throws at startup
314
+ },
295
315
 
296
316
  hidden: { // everything visible by default
297
317
  dotFiles: { default: 'visible', whitelist: [], blacklist: [] },
@@ -308,11 +328,27 @@ koaClassicServer(rootDir, {
308
328
  minFileSize: 1024,
309
329
  maxFileSize: 10485760, // 10 MB buffered-path cap (false = no cap)
310
330
  mimeTypes: [], // override the compressible-type list
331
+ buffered: { brotliQuality: 11, gzipLevel: 9 }, // v4.3: quality when compressing once, then caching
332
+ streaming: { brotliQuality: 4, gzipLevel: 6 }, // v4.3: quality when compressing per request (above maxFileSize)
311
333
  },
312
334
 
313
335
  serverCache: { // in-memory server-side caches
314
- rawFile: { enabled: false }, // cache raw file buffers
315
- compressedFile: { enabled: true }, // cache br/gzip responses
336
+ rawFile: { // cache raw file buffers (skip disk reads)
337
+ enabled: false,
338
+ maxSize: 52428800, // total RAM cap, bytes (50 MB)
339
+ maxFileSize: 1048576, // larger files are never cached, bytes (1 MB)
340
+ maxAge: 0, // ms before an entry counts as stale (0 = off)
341
+ warnInterval: 60000, // ms between "maxSize reached" warnings (0 = always, false = never)
342
+ },
343
+ compressedFile: { // cache br/gzip responses (compress once)
344
+ enabled: true,
345
+ maxSize: 104857600, // total RAM cap, bytes (100 MB)
346
+ maxEntrySize: undefined, // v4.3: per-entry cap on the compressed output, bytes
347
+ // default: maxSize / 4; false = no per-entry cap;
348
+ // oversized entries are served, just never cached
349
+ maxAge: 0, // ms before an entry counts as stale (0 = off)
350
+ warnInterval: 60000, // ms between "maxSize reached" warnings (0 = always, false = never)
351
+ },
316
352
  },
317
353
 
318
354
  symlinks: 'follow', // 'follow' | 'follow-within-root' | 'deny'
@@ -325,7 +361,8 @@ koaClassicServer(rootDir, {
325
361
  },
326
362
 
327
363
  template: {
328
- ext: [], // e.g. ['ejs']
364
+ ext: [], // e.g. ['.ejs'] — dot optional (v5); suffix match, so
365
+ // compound entries ('.html.ejs') target *.html.ejs only
329
366
  renderTimeout: 30000, // ms; on timeout the request fails closed (0 = off)
330
367
  render: async (ctx, next, filePath, rawBuffer, signal) => { /* ... */ },
331
368
  },
@@ -351,24 +388,31 @@ are intentionally left to a reverse proxy or an upstream middleware.
351
388
 
352
389
  ---
353
390
 
354
- ## Migrating from v3
391
+ ## Migrating from v4
392
+
393
+ If your `hideExtension.redirect` is already a real redirect code (or unset) and your
394
+ `template.ext` entries are dot-less, **v5 changes nothing** — everything below is either a
395
+ config bug surfacing earlier or a previously broken form starting to work.
355
396
 
356
- | What | v3 | v4 |
397
+ | What | v4 | v5 |
357
398
  |---|---|---|
358
- | `GET /dir` (directory) | serves the index/listing | **301 `/dir/`** (set `dirListing.trailingSlash: false` to keep v3) |
359
- | `GET /file/` (file + slash) | serves the file | **404** |
360
- | Huge compressible file | buffered whole into RAM | streamed above `compression.maxFileSize` (10 MB) |
361
- | `options` not an object | coerced / crashed | **throws** at construction |
362
- | Malformed `urlPrefix` / `urlsReserved` / negative `browserCacheMaxAge` | silent | **deprecation warning** (throws next major) |
399
+ | `hideExtension.redirect: 200` (any non-redirect integer) | silently sent as `302` | **throws at startup** valid: `300, 301, 302, 303, 305, 307, 308` |
400
+ | `hideExtension.redirect: '301'` (non-integer) | **500 on every redirect** | **throws at startup** |
401
+ | `template.ext: ['.ejs']` (with dot) | never matched template source served raw | **renders** `'.ejs'` `'ejs'`, dotted form preferred |
402
+ | `template.ext: ['.html.ejs']` (compound) | never matched | targets `*.html.ejs` files only |
403
+ | `hideExtension.ext: 'ejs'` (no dot) | worked, with a warning | works warning gone, both forms legal |
404
+ | `template.render: <non-function>` | silently dropped (sources served raw) | one-time warning (startup error planned for v6) |
363
405
 
364
- The v2v3 notes remain in the **[changelog](./docs/CHANGELOG.md)**.
406
+ The v3v4 notes (trailing slash, `compression.maxFileSize`, strict `options` validation)
407
+ remain in the **[changelog](./docs/CHANGELOG.md)**.
365
408
 
366
409
  ---
367
410
 
368
411
  ## Testing
369
412
 
370
413
  ```bash
371
- npm test # full suite (lints first) — 717 tests
414
+ npm test # full suite (lints first)
415
+ npm run test:ci # suite without benchmarks — 1239 tests
372
416
  npm run test:security # security tests only
373
417
  npm run test:performance # benchmarks
374
418
  ```
@@ -379,6 +423,7 @@ npm run test:performance # benchmarks
379
423
 
380
424
  - **[DOCUMENTATION.md](./docs/DOCUMENTATION.md)** — full API reference
381
425
  - **[SECURITY_HARDENING.md](./docs/SECURITY_HARDENING.md)** — hardening guide (canonical)
426
+ - **[PERFORMANCE_TUNING.md](./docs/PERFORMANCE_TUNING.md)** — sizing caches & compression for your host's RAM/CPU
382
427
  - **[CHANGELOG.md](./docs/CHANGELOG.md)** — version history
383
428
  - **[TEMPLATE_ENGINE_GUIDE.md](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)** — EJS/Pug/Handlebars/Nunjucks
384
429