koa-classic-server 2.1.4 → 2.3.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/README.md +79 -34
- package/__tests__/deprecation-warnings.test.js +217 -0
- package/__tests__/publicWwwTest/test-page.html +1 -0
- package/__tests__/useOriginalUrl.test.js +213 -0
- package/docs/CHANGELOG.md +125 -0
- package/index.cjs +49 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,37 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/koa-classic-server)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[]()
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
## 🎉 Version 2.
|
|
11
|
+
## 🎉 Version 2.X - Production-Ready Release
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
The 2.X series brings major performance improvements, enhanced security, and powerful new features while maintaining full backward compatibility.
|
|
14
14
|
|
|
15
|
-
###
|
|
16
|
-
|
|
17
|
-
✅ **Development-Friendly Defaults** - `enableCaching` now defaults to `false` for easier development
|
|
18
|
-
✅ **Production Guidance** - Clear documentation on enabling caching for production environments
|
|
19
|
-
✅ **Enhanced Documentation** - Comprehensive notes on caching configuration and recommendations
|
|
20
|
-
|
|
21
|
-
### What's New in 2.1.2
|
|
15
|
+
### Key Features in Version 2.X
|
|
22
16
|
|
|
17
|
+
✅ **URL Rewriting Support** - Compatible with i18n and routing middleware via `useOriginalUrl` option
|
|
18
|
+
✅ **Improved Caching Controls** - Clear `browserCacheEnabled` and `browserCacheMaxAge` options
|
|
19
|
+
✅ **Development-Friendly Defaults** - Caching disabled by default for easier development
|
|
20
|
+
✅ **Production Optimized** - Enable caching in production for 80-95% bandwidth reduction
|
|
23
21
|
✅ **Sortable Directory Columns** - Click Name/Type/Size to sort (Apache2-like)
|
|
24
|
-
✅ **Navigation Bug Fixed** - Directory navigation now works correctly after sorting
|
|
25
22
|
✅ **File Size Display** - Human-readable file sizes (B, KB, MB, GB, TB)
|
|
26
|
-
✅ **HTTP Caching** -
|
|
23
|
+
✅ **HTTP Caching** - ETag and Last-Modified headers with 304 responses
|
|
27
24
|
✅ **Async/Await** - Non-blocking I/O for high performance
|
|
28
|
-
✅ **
|
|
29
|
-
✅ **Flow Documentation** - Complete execution flow diagrams
|
|
30
|
-
✅ **Code Review** - Standardized operators and best practices
|
|
31
|
-
|
|
32
|
-
### What's New in 2.0
|
|
33
|
-
|
|
34
|
-
✅ **Performance Optimizations** - 50-70% faster directory listings
|
|
25
|
+
✅ **Performance Optimized** - 50-70% faster directory listings
|
|
35
26
|
✅ **Enhanced Index Option** - Array format with RegExp support
|
|
36
|
-
✅ **Template Engine
|
|
37
|
-
✅ **Security
|
|
27
|
+
✅ **Template Engine Support** - EJS, Pug, Handlebars, Nunjucks, and more
|
|
28
|
+
✅ **Enterprise Security** - Path traversal, XSS, race condition protection
|
|
29
|
+
✅ **Comprehensive Testing** - 197 tests passing with extensive coverage
|
|
30
|
+
✅ **Complete Documentation** - Detailed guides and examples
|
|
38
31
|
|
|
39
32
|
[See full changelog →](./docs/CHANGELOG.md)
|
|
40
33
|
|
|
@@ -101,8 +94,8 @@ app.use(koaClassicServer(__dirname + '/public', {
|
|
|
101
94
|
showDirContents: true,
|
|
102
95
|
index: ['index.html', 'index.htm'],
|
|
103
96
|
urlPrefix: '/static',
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
browserCacheMaxAge: 3600,
|
|
98
|
+
browserCacheEnabled: true
|
|
106
99
|
}));
|
|
107
100
|
|
|
108
101
|
app.listen(3000);
|
|
@@ -229,14 +222,14 @@ Enable aggressive caching for static files:
|
|
|
229
222
|
|
|
230
223
|
```javascript
|
|
231
224
|
app.use(koaClassicServer(__dirname + '/public', {
|
|
232
|
-
|
|
233
|
-
|
|
225
|
+
browserCacheEnabled: true, // Enable ETag and Last-Modified
|
|
226
|
+
browserCacheMaxAge: 86400, // Cache for 24 hours (in seconds)
|
|
234
227
|
}));
|
|
235
228
|
```
|
|
236
229
|
|
|
237
230
|
**⚠️ Important: Production Recommendation**
|
|
238
231
|
|
|
239
|
-
The default value for `
|
|
232
|
+
The default value for `browserCacheEnabled` is `false` to facilitate development (where you want changes to be immediately visible). **For production environments, it is strongly recommended to set `browserCacheEnabled: true`** to benefit from:
|
|
240
233
|
|
|
241
234
|
- 80-95% bandwidth reduction
|
|
242
235
|
- 304 Not Modified responses for unchanged files
|
|
@@ -281,8 +274,8 @@ app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
|
281
274
|
index: ['index.html', 'index.htm'],
|
|
282
275
|
urlPrefix: '/assets',
|
|
283
276
|
urlsReserved: ['/admin', '/api', '/.git'],
|
|
284
|
-
|
|
285
|
-
|
|
277
|
+
browserCacheEnabled: true,
|
|
278
|
+
browserCacheMaxAge: 86400, // 24 hours
|
|
286
279
|
}));
|
|
287
280
|
|
|
288
281
|
// Serve dynamic templates
|
|
@@ -366,10 +359,18 @@ Creates a Koa middleware for serving static files.
|
|
|
366
359
|
ext: ['ejs', 'pug', 'hbs']
|
|
367
360
|
},
|
|
368
361
|
|
|
369
|
-
// HTTP caching configuration
|
|
362
|
+
// Browser HTTP caching configuration
|
|
370
363
|
// NOTE: Default is false for development. Set to true in production for better performance!
|
|
371
|
-
|
|
372
|
-
|
|
364
|
+
browserCacheEnabled: false, // Enable ETag & Last-Modified (default: false)
|
|
365
|
+
browserCacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 3600 = 1 hour)
|
|
366
|
+
|
|
367
|
+
// URL resolution
|
|
368
|
+
useOriginalUrl: true, // Use ctx.originalUrl (default) or ctx.url
|
|
369
|
+
// Set false for URL rewriting middleware (i18n, routing)
|
|
370
|
+
|
|
371
|
+
// DEPRECATED (use new names above):
|
|
372
|
+
// enableCaching: use browserCacheEnabled instead
|
|
373
|
+
// cacheMaxAge: use browserCacheMaxAge instead
|
|
373
374
|
}
|
|
374
375
|
```
|
|
375
376
|
|
|
@@ -384,8 +385,52 @@ Creates a Koa middleware for serving static files.
|
|
|
384
385
|
| `urlsReserved` | Array | `[]` | Reserved directory paths (first-level only) |
|
|
385
386
|
| `template.render` | Function | `undefined` | Template rendering function |
|
|
386
387
|
| `template.ext` | Array | `[]` | Extensions for template rendering |
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
388
|
+
| `browserCacheEnabled` | Boolean | `false` | Enable browser HTTP caching headers (recommended: `true` in production) |
|
|
389
|
+
| `browserCacheMaxAge` | Number | `3600` | Browser cache duration in seconds |
|
|
390
|
+
| `useOriginalUrl` | Boolean | `true` | Use `ctx.originalUrl` (default) or `ctx.url` for URL resolution |
|
|
391
|
+
| ~~`enableCaching`~~ | Boolean | `false` | **DEPRECATED**: Use `browserCacheEnabled` instead |
|
|
392
|
+
| ~~`cacheMaxAge`~~ | Number | `3600` | **DEPRECATED**: Use `browserCacheMaxAge` instead |
|
|
393
|
+
|
|
394
|
+
#### useOriginalUrl (Boolean, default: true)
|
|
395
|
+
|
|
396
|
+
Controls which URL property is used for file resolution:
|
|
397
|
+
- **`true` (default)**: Uses `ctx.originalUrl` (immutable, reflects the original request)
|
|
398
|
+
- **`false`**: Uses `ctx.url` (mutable, can be modified by middleware)
|
|
399
|
+
|
|
400
|
+
**When to use `false`:**
|
|
401
|
+
|
|
402
|
+
Set `useOriginalUrl: false` when using URL rewriting middleware such as i18n routers or path rewriters that modify `ctx.url`. This allows koa-classic-server to serve files based on the rewritten URL instead of the original request URL.
|
|
403
|
+
|
|
404
|
+
**Example with i18n middleware:**
|
|
405
|
+
|
|
406
|
+
```javascript
|
|
407
|
+
const Koa = require('koa');
|
|
408
|
+
const koaClassicServer = require('koa-classic-server');
|
|
409
|
+
|
|
410
|
+
const app = new Koa();
|
|
411
|
+
|
|
412
|
+
// i18n middleware that rewrites URLs
|
|
413
|
+
app.use(async (ctx, next) => {
|
|
414
|
+
if (ctx.path.match(/^\/it\//)) {
|
|
415
|
+
ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html → /page.html
|
|
416
|
+
}
|
|
417
|
+
await next();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Serve files using rewritten URL
|
|
421
|
+
app.use(koaClassicServer(__dirname + '/www', {
|
|
422
|
+
useOriginalUrl: false // Use ctx.url (rewritten) instead of ctx.originalUrl
|
|
423
|
+
}));
|
|
424
|
+
|
|
425
|
+
app.listen(3000);
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**How it works:**
|
|
429
|
+
- Request: `GET /it/page.html`
|
|
430
|
+
- `ctx.originalUrl`: `/it/page.html` (unchanged)
|
|
431
|
+
- `ctx.url`: `/page.html` (rewritten by middleware)
|
|
432
|
+
- With `useOriginalUrl: false`: Server looks for `/www/page.html` ✅
|
|
433
|
+
- With `useOriginalUrl: true` (default): Server looks for `/www/it/page.html` ❌ 404
|
|
389
434
|
|
|
390
435
|
---
|
|
391
436
|
|
|
@@ -511,7 +556,7 @@ npm run test:performance
|
|
|
511
556
|
```
|
|
512
557
|
|
|
513
558
|
**Test Coverage:**
|
|
514
|
-
- ✅
|
|
559
|
+
- ✅ 197 tests passing
|
|
515
560
|
- ✅ Security tests (path traversal, XSS, race conditions)
|
|
516
561
|
- ✅ EJS template integration tests
|
|
517
562
|
- ✅ Index option tests (strings, arrays, RegExp)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
//
|
|
3
|
+
// TEST FOR DEPRECATED OPTION NAMES (enableCaching, cacheMaxAge)
|
|
4
|
+
// This test verifies backward compatibility and deprecation warnings
|
|
5
|
+
//
|
|
6
|
+
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
7
|
+
|
|
8
|
+
const supertest = require('supertest');
|
|
9
|
+
const koaClassicServer = require('../index.cjs');
|
|
10
|
+
const Koa = require('koa');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const rootDir = path.join(__dirname, 'publicWwwTest');
|
|
14
|
+
|
|
15
|
+
describe('Deprecated option names (backward compatibility)', () => {
|
|
16
|
+
|
|
17
|
+
describe('Using deprecated enableCaching option', () => {
|
|
18
|
+
let app;
|
|
19
|
+
let server;
|
|
20
|
+
let consoleWarnSpy;
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
// Spy on console.warn to capture deprecation warnings
|
|
24
|
+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
25
|
+
|
|
26
|
+
app = new Koa();
|
|
27
|
+
|
|
28
|
+
// Use deprecated option name
|
|
29
|
+
app.use(koaClassicServer(rootDir, {
|
|
30
|
+
enableCaching: true // DEPRECATED: should use browserCacheEnabled
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
server = app.listen();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
server.close();
|
|
38
|
+
consoleWarnSpy.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('should display deprecation warning for enableCaching', () => {
|
|
42
|
+
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
43
|
+
const warningMessage = consoleWarnSpy.mock.calls[0][1];
|
|
44
|
+
expect(warningMessage).toContain('DEPRECATION WARNING');
|
|
45
|
+
expect(warningMessage).toContain('enableCaching');
|
|
46
|
+
expect(warningMessage).toContain('browserCacheEnabled');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should still work with deprecated option', async () => {
|
|
50
|
+
const response = await supertest(server).get('/test-page.html');
|
|
51
|
+
|
|
52
|
+
// Should return the file
|
|
53
|
+
expect(response.status).toBe(200);
|
|
54
|
+
|
|
55
|
+
// Should have caching headers (because enableCaching: true was set)
|
|
56
|
+
expect(response.headers['etag']).toBeDefined();
|
|
57
|
+
expect(response.headers['last-modified']).toBeDefined();
|
|
58
|
+
expect(response.headers['cache-control']).toContain('public');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('Using deprecated cacheMaxAge option', () => {
|
|
63
|
+
let app;
|
|
64
|
+
let server;
|
|
65
|
+
let consoleWarnSpy;
|
|
66
|
+
|
|
67
|
+
beforeAll(() => {
|
|
68
|
+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
69
|
+
|
|
70
|
+
app = new Koa();
|
|
71
|
+
|
|
72
|
+
// Use deprecated option name
|
|
73
|
+
app.use(koaClassicServer(rootDir, {
|
|
74
|
+
enableCaching: true, // Also deprecated
|
|
75
|
+
cacheMaxAge: 7200 // DEPRECATED: should use browserCacheMaxAge
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
server = app.listen();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterAll(() => {
|
|
82
|
+
server.close();
|
|
83
|
+
consoleWarnSpy.mockRestore();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should display deprecation warning for cacheMaxAge', () => {
|
|
87
|
+
const warningCalls = consoleWarnSpy.mock.calls;
|
|
88
|
+
const cacheMaxAgeWarning = warningCalls.find(call =>
|
|
89
|
+
call[1] && call[1].includes('cacheMaxAge')
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(cacheMaxAgeWarning).toBeDefined();
|
|
93
|
+
expect(cacheMaxAgeWarning[1]).toContain('DEPRECATION WARNING');
|
|
94
|
+
expect(cacheMaxAgeWarning[1]).toContain('browserCacheMaxAge');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('should use the deprecated cacheMaxAge value', async () => {
|
|
98
|
+
const response = await supertest(server).get('/test-page.html');
|
|
99
|
+
|
|
100
|
+
expect(response.status).toBe(200);
|
|
101
|
+
expect(response.headers['cache-control']).toContain('max-age=7200');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Using new option names (no warnings)', () => {
|
|
106
|
+
let app;
|
|
107
|
+
let server;
|
|
108
|
+
let consoleWarnSpy;
|
|
109
|
+
|
|
110
|
+
beforeAll(() => {
|
|
111
|
+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
112
|
+
|
|
113
|
+
app = new Koa();
|
|
114
|
+
|
|
115
|
+
// Use NEW option names
|
|
116
|
+
app.use(koaClassicServer(rootDir, {
|
|
117
|
+
browserCacheEnabled: true,
|
|
118
|
+
browserCacheMaxAge: 3600
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
server = app.listen();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
afterAll(() => {
|
|
125
|
+
server.close();
|
|
126
|
+
consoleWarnSpy.mockRestore();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should NOT display deprecation warnings', () => {
|
|
130
|
+
// Filter out warnings from other tests (like index option deprecation)
|
|
131
|
+
const cachingWarnings = consoleWarnSpy.mock.calls.filter(call =>
|
|
132
|
+
call[1] && (call[1].includes('enableCaching') || call[1].includes('cacheMaxAge'))
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(cachingWarnings.length).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should work correctly with new option names', async () => {
|
|
139
|
+
const response = await supertest(server).get('/test-page.html');
|
|
140
|
+
|
|
141
|
+
expect(response.status).toBe(200);
|
|
142
|
+
expect(response.headers['etag']).toBeDefined();
|
|
143
|
+
expect(response.headers['cache-control']).toContain('max-age=3600');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Using both old and new names (new takes precedence)', () => {
|
|
148
|
+
let app;
|
|
149
|
+
let server;
|
|
150
|
+
let consoleWarnSpy;
|
|
151
|
+
|
|
152
|
+
beforeAll(() => {
|
|
153
|
+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
154
|
+
|
|
155
|
+
app = new Koa();
|
|
156
|
+
|
|
157
|
+
// Use BOTH old and new names - new should take precedence
|
|
158
|
+
app.use(koaClassicServer(rootDir, {
|
|
159
|
+
enableCaching: true, // OLD (deprecated)
|
|
160
|
+
browserCacheEnabled: false, // NEW (should take precedence)
|
|
161
|
+
cacheMaxAge: 7200, // OLD (deprecated)
|
|
162
|
+
browserCacheMaxAge: 9999 // NEW (should take precedence)
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
server = app.listen();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
afterAll(() => {
|
|
169
|
+
server.close();
|
|
170
|
+
consoleWarnSpy.mockRestore();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('should NOT display warnings when new names are also provided', () => {
|
|
174
|
+
// When both old and new names are provided, no warning should be shown
|
|
175
|
+
const cachingWarnings = consoleWarnSpy.mock.calls.filter(call =>
|
|
176
|
+
call[1] && (call[1].includes('enableCaching') || call[1].includes('cacheMaxAge'))
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(cachingWarnings.length).toBe(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('new option values should take precedence over old ones', async () => {
|
|
183
|
+
const response = await supertest(server).get('/test-page.html');
|
|
184
|
+
|
|
185
|
+
expect(response.status).toBe(200);
|
|
186
|
+
|
|
187
|
+
// browserCacheEnabled: false should take precedence over enableCaching: true
|
|
188
|
+
// So there should be NO caching headers
|
|
189
|
+
expect(response.headers['etag']).toBeUndefined();
|
|
190
|
+
expect(response.headers['cache-control']).toContain('no-cache');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('Default behavior (no caching options specified)', () => {
|
|
195
|
+
let app;
|
|
196
|
+
let server;
|
|
197
|
+
|
|
198
|
+
beforeAll(() => {
|
|
199
|
+
app = new Koa();
|
|
200
|
+
app.use(koaClassicServer(rootDir));
|
|
201
|
+
server = app.listen();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
afterAll(() => {
|
|
205
|
+
server.close();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('should default to browserCacheEnabled: false', async () => {
|
|
209
|
+
const response = await supertest(server).get('/test-page.html');
|
|
210
|
+
|
|
211
|
+
expect(response.status).toBe(200);
|
|
212
|
+
|
|
213
|
+
// Default is caching disabled
|
|
214
|
+
expect(response.headers['cache-control']).toContain('no-cache');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Test Page</h1></body></html>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
//
|
|
3
|
+
// TEST FOR useOriginalUrl OPTION
|
|
4
|
+
// This test verifies that the useOriginalUrl option works correctly with URL rewriting middleware
|
|
5
|
+
//
|
|
6
|
+
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
7
|
+
|
|
8
|
+
const supertest = require('supertest');
|
|
9
|
+
const koaClassicServer = require('../index.cjs');
|
|
10
|
+
const Koa = require('koa');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const rootDir = path.join(__dirname, 'publicWwwTest');
|
|
15
|
+
|
|
16
|
+
// Create a simple test file if it doesn't exist
|
|
17
|
+
const testFilePath = path.join(rootDir, 'test-page.html');
|
|
18
|
+
const testFileContent = '<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Test Page</h1></body></html>';
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
// Ensure test directory exists
|
|
22
|
+
if (!fs.existsSync(rootDir)) {
|
|
23
|
+
fs.mkdirSync(rootDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Create test file
|
|
27
|
+
if (!fs.existsSync(testFilePath)) {
|
|
28
|
+
fs.writeFileSync(testFilePath, testFileContent, 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('useOriginalUrl option tests', () => {
|
|
33
|
+
|
|
34
|
+
describe('Default behavior (useOriginalUrl: true)', () => {
|
|
35
|
+
let app;
|
|
36
|
+
let server;
|
|
37
|
+
|
|
38
|
+
beforeAll(() => {
|
|
39
|
+
app = new Koa();
|
|
40
|
+
|
|
41
|
+
// i18n middleware that rewrites URLs
|
|
42
|
+
app.use(async (ctx, next) => {
|
|
43
|
+
if (ctx.path.match(/^\/it\//)) {
|
|
44
|
+
// Rewrite /it/page.html to /page.html
|
|
45
|
+
ctx.url = ctx.path.replace(/^\/it/, '');
|
|
46
|
+
}
|
|
47
|
+
await next();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Serve files with default useOriginalUrl: true
|
|
51
|
+
app.use(koaClassicServer(rootDir, {
|
|
52
|
+
useOriginalUrl: true // Default behavior
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
server = app.listen();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterAll(() => {
|
|
59
|
+
server.close();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should use ctx.originalUrl (original request path)', async () => {
|
|
63
|
+
// Request /it/test-page.html
|
|
64
|
+
// ctx.originalUrl = /it/test-page.html (unchanged)
|
|
65
|
+
// ctx.url = /test-page.html (rewritten by middleware)
|
|
66
|
+
// With useOriginalUrl: true, server looks for /it/test-page.html (which doesn't exist)
|
|
67
|
+
const response = await supertest(server).get('/it/test-page.html');
|
|
68
|
+
|
|
69
|
+
// Should return 404 because /it/test-page.html doesn't exist
|
|
70
|
+
expect(response.status).toBe(404);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should serve file without rewriting', async () => {
|
|
74
|
+
// Request /test-page.html directly (no rewriting)
|
|
75
|
+
const response = await supertest(server).get('/test-page.html');
|
|
76
|
+
|
|
77
|
+
// Should return 200 and the file content
|
|
78
|
+
expect(response.status).toBe(200);
|
|
79
|
+
expect(response.text).toContain('Test Page');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('URL rewriting support (useOriginalUrl: false)', () => {
|
|
84
|
+
let app;
|
|
85
|
+
let server;
|
|
86
|
+
|
|
87
|
+
beforeAll(() => {
|
|
88
|
+
app = new Koa();
|
|
89
|
+
|
|
90
|
+
// i18n middleware that rewrites URLs
|
|
91
|
+
app.use(async (ctx, next) => {
|
|
92
|
+
if (ctx.path.match(/^\/it\//)) {
|
|
93
|
+
// Rewrite /it/page.html to /page.html
|
|
94
|
+
ctx.url = ctx.path.replace(/^\/it/, '');
|
|
95
|
+
}
|
|
96
|
+
await next();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Serve files with useOriginalUrl: false to use rewritten URL
|
|
100
|
+
app.use(koaClassicServer(rootDir, {
|
|
101
|
+
useOriginalUrl: false // Use ctx.url (rewritten)
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
server = app.listen();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterAll(() => {
|
|
108
|
+
server.close();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('should use ctx.url (rewritten path)', async () => {
|
|
112
|
+
// Request /it/test-page.html
|
|
113
|
+
// ctx.originalUrl = /it/test-page.html (unchanged)
|
|
114
|
+
// ctx.url = /test-page.html (rewritten by middleware)
|
|
115
|
+
// With useOriginalUrl: false, server looks for /test-page.html (which exists)
|
|
116
|
+
const response = await supertest(server).get('/it/test-page.html');
|
|
117
|
+
|
|
118
|
+
// Should return 200 and the file content
|
|
119
|
+
expect(response.status).toBe(200);
|
|
120
|
+
expect(response.text).toContain('Test Page');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('should still serve file without rewriting', async () => {
|
|
124
|
+
// Request /test-page.html directly (no rewriting)
|
|
125
|
+
const response = await supertest(server).get('/test-page.html');
|
|
126
|
+
|
|
127
|
+
// Should return 200 and the file content
|
|
128
|
+
expect(response.status).toBe(200);
|
|
129
|
+
expect(response.text).toContain('Test Page');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('Complex i18n routing scenario', () => {
|
|
134
|
+
let app;
|
|
135
|
+
let server;
|
|
136
|
+
|
|
137
|
+
beforeAll(() => {
|
|
138
|
+
app = new Koa();
|
|
139
|
+
|
|
140
|
+
// More complex i18n middleware
|
|
141
|
+
app.use(async (ctx, next) => {
|
|
142
|
+
const langPattern = /^\/(it|fr|de|es)\//;
|
|
143
|
+
const match = ctx.path.match(langPattern);
|
|
144
|
+
if (match) {
|
|
145
|
+
// Store language in state
|
|
146
|
+
ctx.state.lang = match[1];
|
|
147
|
+
// Strip language prefix
|
|
148
|
+
ctx.url = ctx.path.replace(langPattern, '/');
|
|
149
|
+
}
|
|
150
|
+
await next();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
app.use(koaClassicServer(rootDir, {
|
|
154
|
+
useOriginalUrl: false
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
server = app.listen();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
afterAll(() => {
|
|
161
|
+
server.close();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should work with Italian locale (/it/)', async () => {
|
|
165
|
+
const response = await supertest(server).get('/it/test-page.html');
|
|
166
|
+
expect(response.status).toBe(200);
|
|
167
|
+
expect(response.text).toContain('Test Page');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('should work with French locale (/fr/)', async () => {
|
|
171
|
+
const response = await supertest(server).get('/fr/test-page.html');
|
|
172
|
+
expect(response.status).toBe(200);
|
|
173
|
+
expect(response.text).toContain('Test Page');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('should work with German locale (/de/)', async () => {
|
|
177
|
+
const response = await supertest(server).get('/de/test-page.html');
|
|
178
|
+
expect(response.status).toBe(200);
|
|
179
|
+
expect(response.text).toContain('Test Page');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('should work with Spanish locale (/es/)', async () => {
|
|
183
|
+
const response = await supertest(server).get('/es/test-page.html');
|
|
184
|
+
expect(response.status).toBe(200);
|
|
185
|
+
expect(response.text).toContain('Test Page');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Backward compatibility', () => {
|
|
190
|
+
let app;
|
|
191
|
+
let server;
|
|
192
|
+
|
|
193
|
+
beforeAll(() => {
|
|
194
|
+
app = new Koa();
|
|
195
|
+
|
|
196
|
+
// No URL rewriting middleware
|
|
197
|
+
// Default useOriginalUrl (should be true)
|
|
198
|
+
app.use(koaClassicServer(rootDir));
|
|
199
|
+
|
|
200
|
+
server = app.listen();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
afterAll(() => {
|
|
204
|
+
server.close();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('should work with default options (backward compatible)', async () => {
|
|
208
|
+
const response = await supertest(server).get('/test-page.html');
|
|
209
|
+
expect(response.status).toBe(200);
|
|
210
|
+
expect(response.text).toContain('Test Page');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,131 @@ All notable changes to koa-classic-server will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.3.0] - 2026-01-03
|
|
9
|
+
|
|
10
|
+
### 🔄 Renamed Options (with Backward Compatibility)
|
|
11
|
+
|
|
12
|
+
#### Renamed Caching Options for Clarity
|
|
13
|
+
- **Old Names** (DEPRECATED): `enableCaching`, `cacheMaxAge`
|
|
14
|
+
- **New Names**: `browserCacheEnabled`, `browserCacheMaxAge`
|
|
15
|
+
- **Reason**: Improved clarity - these options specifically control browser-side HTTP caching
|
|
16
|
+
- **Backward Compatible**: Old names still work but display deprecation warnings
|
|
17
|
+
|
|
18
|
+
#### Deprecation Warnings
|
|
19
|
+
When using deprecated option names, a warning is displayed on the terminal:
|
|
20
|
+
```
|
|
21
|
+
[koa-classic-server] DEPRECATION WARNING: The "enableCaching" option is deprecated and will be removed in future versions.
|
|
22
|
+
Current usage: enableCaching: true
|
|
23
|
+
Recommended: browserCacheEnabled: true
|
|
24
|
+
Please update your configuration to use the new option name.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 📝 Documentation Updates
|
|
28
|
+
|
|
29
|
+
- Updated README.md with new option names
|
|
30
|
+
- Updated JSDoc comments in index.cjs
|
|
31
|
+
- Added deprecation notes in Options table
|
|
32
|
+
- All examples updated to use new names
|
|
33
|
+
|
|
34
|
+
### 🔧 Changes
|
|
35
|
+
|
|
36
|
+
- **index.cjs**: Lines 109-135 - Added backward compatibility logic with deprecation warnings
|
|
37
|
+
- **index.cjs**: Lines 47-58 - Updated JSDoc comments
|
|
38
|
+
- **index.cjs**: Lines 350, 361 - Updated code to use new option names
|
|
39
|
+
- **README.md**: Updated all references to use new names, added deprecation notes
|
|
40
|
+
- **package.json**: Version bumped from `2.2.0` to `2.3.0`
|
|
41
|
+
|
|
42
|
+
### ⚠️ Migration Guide
|
|
43
|
+
|
|
44
|
+
**No immediate changes required** - old option names still work.
|
|
45
|
+
|
|
46
|
+
**Recommended migration:**
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// Old (still works, but deprecated)
|
|
50
|
+
app.use(koaClassicServer('/public', {
|
|
51
|
+
enableCaching: true,
|
|
52
|
+
cacheMaxAge: 3600
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// New (recommended)
|
|
56
|
+
app.use(koaClassicServer('/public', {
|
|
57
|
+
browserCacheEnabled: true,
|
|
58
|
+
browserCacheMaxAge: 3600
|
|
59
|
+
}));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Timeline:**
|
|
63
|
+
- **v2.3.0**: Old names work with deprecation warnings
|
|
64
|
+
- **Future versions**: Old names may be removed (will be announced in advance)
|
|
65
|
+
|
|
66
|
+
### 📦 Package Changes
|
|
67
|
+
|
|
68
|
+
- **Version**: `2.2.0` → `2.3.0`
|
|
69
|
+
- **Semver**: Minor version bump (new feature names, backward compatible)
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## [2.2.0] - 2026-01-03
|
|
74
|
+
|
|
75
|
+
### ✨ Features
|
|
76
|
+
|
|
77
|
+
#### Added useOriginalUrl Option
|
|
78
|
+
- **New Option**: `useOriginalUrl` (Boolean, default: `true`)
|
|
79
|
+
- **Purpose**: Controls URL resolution for file serving - use `ctx.originalUrl` (immutable) or `ctx.url` (mutable)
|
|
80
|
+
- **Use Case**: Compatibility with URL rewriting middleware (i18n, routing)
|
|
81
|
+
- **Backward Compatible**: Default value `true` maintains existing behavior
|
|
82
|
+
|
|
83
|
+
#### URL Rewriting Middleware Support
|
|
84
|
+
- **Problem Solved**: koa-classic-server previously used `ctx.href` (based on `ctx.originalUrl`), which caused 404 errors when middleware rewrites URLs by modifying `ctx.url`
|
|
85
|
+
- **Solution**: Set `useOriginalUrl: false` to use the rewritten URL from `ctx.url` instead
|
|
86
|
+
- **Example**: i18n middleware that strips language prefixes (`/it/page.html` → `/page.html`)
|
|
87
|
+
|
|
88
|
+
### 📝 Documentation
|
|
89
|
+
|
|
90
|
+
- Added comprehensive `useOriginalUrl` documentation in README.md
|
|
91
|
+
- Added JSDoc comments in index.cjs
|
|
92
|
+
- Included practical i18n middleware example
|
|
93
|
+
- Added option to API reference table
|
|
94
|
+
|
|
95
|
+
### 🔧 Changes
|
|
96
|
+
|
|
97
|
+
- **index.cjs**: Line 108 - Added `useOriginalUrl` option initialization
|
|
98
|
+
- **index.cjs**: Lines 117-125 - Modified URL construction logic to support both `ctx.originalUrl` and `ctx.url`
|
|
99
|
+
- **README.md**: Added detailed section explaining `useOriginalUrl` with examples
|
|
100
|
+
- **package.json**: Version bumped from `2.1.4` to `2.2.0`
|
|
101
|
+
|
|
102
|
+
### 💡 Usage Example
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
// i18n middleware example
|
|
106
|
+
app.use(async (ctx, next) => {
|
|
107
|
+
if (ctx.path.match(/^\/it\//)) {
|
|
108
|
+
ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html → /page.html
|
|
109
|
+
}
|
|
110
|
+
await next();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
app.use(koaClassicServer('/www', {
|
|
114
|
+
useOriginalUrl: false // Use rewritten URL
|
|
115
|
+
}));
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### ⚠️ Migration Notes
|
|
119
|
+
|
|
120
|
+
**No breaking changes** - this is a backward-compatible release.
|
|
121
|
+
|
|
122
|
+
- **Default behavior unchanged**: `useOriginalUrl` defaults to `true`
|
|
123
|
+
- **No code changes required** for existing implementations
|
|
124
|
+
- **New feature**: Set `useOriginalUrl: false` if you use URL rewriting middleware
|
|
125
|
+
|
|
126
|
+
### 📦 Package Changes
|
|
127
|
+
|
|
128
|
+
- **Version**: `2.1.4` → `2.2.0`
|
|
129
|
+
- **Semver**: Minor version bump (new feature, backward compatible)
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
8
133
|
## [2.1.3] - 2025-11-25
|
|
9
134
|
|
|
10
135
|
### 🔧 Configuration Changes
|
package/index.cjs
CHANGED
|
@@ -44,11 +44,17 @@ module.exports = function koaClassicServer(
|
|
|
44
44
|
render: undefined, // Template rendering function: async (ctx, next, filePath) => {}
|
|
45
45
|
ext: [], // File extensions to process with template.render
|
|
46
46
|
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
browserCacheMaxAge: 3600, // Browser Cache-Control max-age in seconds (default: 1 hour)
|
|
48
|
+
browserCacheEnabled: false, // Enable browser HTTP caching headers (ETag, Last-Modified)
|
|
49
|
+
// NOTE: Default is false for development.
|
|
50
|
+
// In production, it's recommended to set browserCacheEnabled: true
|
|
51
|
+
// to reduce bandwidth usage and improve performance.
|
|
52
|
+
useOriginalUrl: true, // Use ctx.originalUrl (default) or ctx.url
|
|
53
|
+
// Set false for URL rewriting middleware (i18n, routing)
|
|
54
|
+
|
|
55
|
+
// DEPRECATED OPTIONS (maintained for backward compatibility):
|
|
56
|
+
// cacheMaxAge: use browserCacheMaxAge instead
|
|
57
|
+
// enableCaching: use browserCacheEnabled instead
|
|
52
58
|
}
|
|
53
59
|
*/
|
|
54
60
|
) {
|
|
@@ -100,11 +106,37 @@ module.exports = function koaClassicServer(
|
|
|
100
106
|
options.template.ext = Array.isArray(options.template.ext) ? options.template.ext : [];
|
|
101
107
|
|
|
102
108
|
// OPTIMIZATION: HTTP Caching options
|
|
103
|
-
// NOTE: Default
|
|
109
|
+
// NOTE: Default browserCacheEnabled is false for development environments.
|
|
104
110
|
// For production deployments, it's strongly recommended to enable caching
|
|
105
|
-
// by setting
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
// by setting browserCacheEnabled: true to benefit from reduced bandwidth and improved performance.
|
|
112
|
+
|
|
113
|
+
// DEPRECATION: Handle legacy option names for backward compatibility
|
|
114
|
+
if ('cacheMaxAge' in opts && !('browserCacheMaxAge' in opts)) {
|
|
115
|
+
console.warn(
|
|
116
|
+
'\x1b[33m%s\x1b[0m',
|
|
117
|
+
'[koa-classic-server] DEPRECATION WARNING: The "cacheMaxAge" option is deprecated and will be removed in future versions.\n' +
|
|
118
|
+
' Current usage: cacheMaxAge: ' + opts.cacheMaxAge + '\n' +
|
|
119
|
+
' Recommended: browserCacheMaxAge: ' + opts.cacheMaxAge + '\n' +
|
|
120
|
+
' Please update your configuration to use the new option name.'
|
|
121
|
+
);
|
|
122
|
+
options.browserCacheMaxAge = opts.cacheMaxAge;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if ('enableCaching' in opts && !('browserCacheEnabled' in opts)) {
|
|
126
|
+
console.warn(
|
|
127
|
+
'\x1b[33m%s\x1b[0m',
|
|
128
|
+
'[koa-classic-server] DEPRECATION WARNING: The "enableCaching" option is deprecated and will be removed in future versions.\n' +
|
|
129
|
+
' Current usage: enableCaching: ' + opts.enableCaching + '\n' +
|
|
130
|
+
' Recommended: browserCacheEnabled: ' + opts.enableCaching + '\n' +
|
|
131
|
+
' Please update your configuration to use the new option name.'
|
|
132
|
+
);
|
|
133
|
+
options.browserCacheEnabled = opts.enableCaching;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Set new option names (with defaults)
|
|
137
|
+
options.browserCacheMaxAge = typeof options.browserCacheMaxAge === 'number' && options.browserCacheMaxAge >= 0 ? options.browserCacheMaxAge : 3600;
|
|
138
|
+
options.browserCacheEnabled = typeof options.browserCacheEnabled === 'boolean' ? options.browserCacheEnabled : false;
|
|
139
|
+
options.useOriginalUrl = typeof options.useOriginalUrl === 'boolean' ? options.useOriginalUrl : true;
|
|
108
140
|
|
|
109
141
|
return async (ctx, next) => {
|
|
110
142
|
// Check if method is allowed
|
|
@@ -113,12 +145,14 @@ module.exports = function koaClassicServer(
|
|
|
113
145
|
return;
|
|
114
146
|
}
|
|
115
147
|
|
|
116
|
-
//
|
|
148
|
+
// Construct full URL based on useOriginalUrl option
|
|
149
|
+
const urlToUse = options.useOriginalUrl ? ctx.originalUrl : ctx.url;
|
|
150
|
+
const fullUrl = ctx.protocol + '://' + ctx.host + urlToUse;
|
|
117
151
|
let pageHref = '';
|
|
118
|
-
if (
|
|
119
|
-
pageHref = new URL(
|
|
152
|
+
if (fullUrl.charAt(fullUrl.length - 1) === '/') {
|
|
153
|
+
pageHref = new URL(fullUrl.slice(0, -1));
|
|
120
154
|
} else {
|
|
121
|
-
pageHref = new URL(
|
|
155
|
+
pageHref = new URL(fullUrl);
|
|
122
156
|
}
|
|
123
157
|
|
|
124
158
|
// Check URL prefix
|
|
@@ -317,7 +351,7 @@ module.exports = function koaClassicServer(
|
|
|
317
351
|
}
|
|
318
352
|
|
|
319
353
|
// OPTIMIZATION: HTTP Caching Headers
|
|
320
|
-
if (options.
|
|
354
|
+
if (options.browserCacheEnabled) {
|
|
321
355
|
// Generate ETag from mtime timestamp + file size
|
|
322
356
|
// This ensures ETag changes when file is modified or resized
|
|
323
357
|
const etag = `"${fileStat.mtime.getTime()}-${fileStat.size}"`;
|
|
@@ -328,7 +362,7 @@ module.exports = function koaClassicServer(
|
|
|
328
362
|
// Set caching headers
|
|
329
363
|
ctx.set('ETag', etag);
|
|
330
364
|
ctx.set('Last-Modified', lastModified);
|
|
331
|
-
ctx.set('Cache-Control', `public, max-age=${options.
|
|
365
|
+
ctx.set('Cache-Control', `public, max-age=${options.browserCacheMaxAge}, must-revalidate`);
|
|
332
366
|
|
|
333
367
|
// OPTIMIZATION: Handle conditional requests (304 Not Modified)
|
|
334
368
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa-classic-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "High-performance Koa middleware for serving static files with Apache-like directory listing, HTTP caching, template engine support, and comprehensive security fixes",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"exports": {
|