koa-classic-server 2.3.0 → 2.5.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 +92 -7
- package/__tests__/hideExtension.test.js +550 -0
- package/__tests__/publicWwwTest/hideext-test/about/index.html +1 -0
- package/__tests__/publicWwwTest/hideext-test/about.EJS +1 -0
- package/__tests__/publicWwwTest/hideext-test/about.ejs +2 -0
- package/__tests__/publicWwwTest/hideext-test/blog/articolo.ejs +2 -0
- package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina +1 -0
- package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina.ejs +1 -0
- package/__tests__/publicWwwTest/hideext-test/file.ejs.bak +1 -0
- package/__tests__/publicWwwTest/hideext-test/index.ejs +2 -0
- package/__tests__/publicWwwTest/hideext-test/photo.txt +1 -0
- package/__tests__/publicWwwTest/hideext-test/sezione/index.ejs +1 -0
- package/__tests__/publicWwwTest/hideext-test/style.css +1 -0
- package/__tests__/symlink.test.js +438 -0
- package/docs/CHANGELOG.md +87 -0
- package/docs/DOCUMENTATION.md +53 -5
- package/index.cjs +186 -23
- package/package.json +1 -1
- package/.vscode/OLD_launch.json +0 -26
- package/.vscode/launch.json +0 -41
- package/.vscode/settings.json +0 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symlink support tests for koa-classic-server
|
|
3
|
+
*
|
|
4
|
+
* Context: On NixOS with buildFHSEnv (chroot-like environment used for Playwright tests),
|
|
5
|
+
* files in the www/ directory appear as symlinks to the Nix store instead of regular files.
|
|
6
|
+
* This caused two failures:
|
|
7
|
+
* - GET / returned a directory listing ("Index of /") instead of rendering index.ejs
|
|
8
|
+
* - GET /index.ejs returned 404 instead of 200
|
|
9
|
+
*
|
|
10
|
+
* Root cause: fs.readdir({ withFileTypes: true }) classifies symlinks as
|
|
11
|
+
* isSymbolicLink()=true, isFile()=false. The findIndexFile() function filtered
|
|
12
|
+
* with dirent.isFile(), excluding all symlinks from index file discovery.
|
|
13
|
+
*
|
|
14
|
+
* The fix introduces isFileOrSymlinkToFile() / isDirOrSymlinkToDir() helpers
|
|
15
|
+
* that follow symlinks via fs.promises.stat() only when dirent.isSymbolicLink()
|
|
16
|
+
* is true, adding zero overhead for regular files.
|
|
17
|
+
*
|
|
18
|
+
* These tests also cover: Docker bind mounts, npm link, Capistrano-style deploys,
|
|
19
|
+
* and any other scenario where files are served through symbolic links.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const Koa = require('koa');
|
|
26
|
+
const supertest = require('supertest');
|
|
27
|
+
const koaClassicServer = require('../index.cjs');
|
|
28
|
+
|
|
29
|
+
// Detect if the OS supports symlinks (Windows without dev mode may not)
|
|
30
|
+
let symlinkSupported = true;
|
|
31
|
+
try {
|
|
32
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'symlink-check-'));
|
|
33
|
+
const testFile = path.join(testDir, 'target');
|
|
34
|
+
const testLink = path.join(testDir, 'link');
|
|
35
|
+
fs.writeFileSync(testFile, 'test');
|
|
36
|
+
fs.symlinkSync(testFile, testLink);
|
|
37
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
38
|
+
} catch {
|
|
39
|
+
symlinkSupported = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const describeIfSymlinks = symlinkSupported ? describe : describe.skip;
|
|
43
|
+
|
|
44
|
+
describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
45
|
+
let tmpDir;
|
|
46
|
+
|
|
47
|
+
beforeAll(() => {
|
|
48
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kcs-symlink-test-'));
|
|
49
|
+
|
|
50
|
+
// --- Regular files ---
|
|
51
|
+
fs.writeFileSync(
|
|
52
|
+
path.join(tmpDir, 'index.html'),
|
|
53
|
+
'<html><head><title>Regular Index</title></head><body>Hello</body></html>'
|
|
54
|
+
);
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(tmpDir, 'style.css'),
|
|
57
|
+
'body { color: red; }'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// --- Real file that will be the symlink target for index.ejs ---
|
|
61
|
+
fs.writeFileSync(
|
|
62
|
+
path.join(tmpDir, 'real-index.ejs'),
|
|
63
|
+
'<html><head><title>EJS via Symlink</title></head><body><h1>Works</h1></body></html>'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// --- Symlink to file (the core bug scenario) ---
|
|
67
|
+
fs.symlinkSync(
|
|
68
|
+
path.join(tmpDir, 'real-index.ejs'),
|
|
69
|
+
path.join(tmpDir, 'index.ejs')
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// --- Symlink to regular file (non-index) ---
|
|
73
|
+
fs.symlinkSync(
|
|
74
|
+
path.join(tmpDir, 'style.css'),
|
|
75
|
+
path.join(tmpDir, 'linked-style.css')
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// --- Real subdirectory with a file ---
|
|
79
|
+
const realSubdir = path.join(tmpDir, 'real-subdir');
|
|
80
|
+
fs.mkdirSync(realSubdir);
|
|
81
|
+
fs.writeFileSync(
|
|
82
|
+
path.join(realSubdir, 'file.txt'),
|
|
83
|
+
'content inside real-subdir'
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// --- Symlink to directory ---
|
|
87
|
+
fs.symlinkSync(
|
|
88
|
+
realSubdir,
|
|
89
|
+
path.join(tmpDir, 'linked-subdir')
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// --- Broken symlink (target does not exist) ---
|
|
93
|
+
fs.symlinkSync(
|
|
94
|
+
path.join(tmpDir, 'non-existent-file.html'),
|
|
95
|
+
path.join(tmpDir, 'broken-link.html')
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// --- Circular symlinks ---
|
|
99
|
+
try {
|
|
100
|
+
fs.symlinkSync(
|
|
101
|
+
path.join(tmpDir, 'circular-b'),
|
|
102
|
+
path.join(tmpDir, 'circular-a')
|
|
103
|
+
);
|
|
104
|
+
fs.symlinkSync(
|
|
105
|
+
path.join(tmpDir, 'circular-a'),
|
|
106
|
+
path.join(tmpDir, 'circular-b')
|
|
107
|
+
);
|
|
108
|
+
} catch {
|
|
109
|
+
// Some systems may not support circular symlinks creation
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterAll(() => {
|
|
114
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// =========================================================================
|
|
118
|
+
// 1. REGRESSION - Regular file as index
|
|
119
|
+
// =========================================================================
|
|
120
|
+
describe('regular file as index (regression)', () => {
|
|
121
|
+
let server, request;
|
|
122
|
+
|
|
123
|
+
beforeAll(() => {
|
|
124
|
+
const app = new Koa();
|
|
125
|
+
app.use(koaClassicServer(tmpDir, {
|
|
126
|
+
index: ['index.html'],
|
|
127
|
+
showDirContents: true
|
|
128
|
+
}));
|
|
129
|
+
server = app.listen();
|
|
130
|
+
request = supertest(server);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterAll(() => { server?.close(); });
|
|
134
|
+
|
|
135
|
+
test('GET / serves regular index.html, not directory listing', async () => {
|
|
136
|
+
const res = await request.get('/');
|
|
137
|
+
expect(res.status).toBe(200);
|
|
138
|
+
expect(res.text).toContain('Regular Index');
|
|
139
|
+
expect(res.text).not.toContain('Index of');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// =========================================================================
|
|
144
|
+
// 2. BUG FIX - Symlink to file as index
|
|
145
|
+
// =========================================================================
|
|
146
|
+
describe('symlink to file as index (bug fix)', () => {
|
|
147
|
+
let server, request;
|
|
148
|
+
|
|
149
|
+
beforeAll(() => {
|
|
150
|
+
const app = new Koa();
|
|
151
|
+
app.use(koaClassicServer(tmpDir, {
|
|
152
|
+
index: ['index.ejs'],
|
|
153
|
+
showDirContents: true
|
|
154
|
+
}));
|
|
155
|
+
server = app.listen();
|
|
156
|
+
request = supertest(server);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
afterAll(() => { server?.close(); });
|
|
160
|
+
|
|
161
|
+
test('GET / serves symlinked index.ejs, not directory listing', async () => {
|
|
162
|
+
const res = await request.get('/');
|
|
163
|
+
expect(res.status).toBe(200);
|
|
164
|
+
expect(res.text).toContain('EJS via Symlink');
|
|
165
|
+
expect(res.text).not.toContain('Index of');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// =========================================================================
|
|
170
|
+
// 3. BUG FIX - Direct GET to symlinked file returns 200
|
|
171
|
+
// =========================================================================
|
|
172
|
+
describe('direct GET to symlinked file (bug fix)', () => {
|
|
173
|
+
let server, request;
|
|
174
|
+
|
|
175
|
+
beforeAll(() => {
|
|
176
|
+
const app = new Koa();
|
|
177
|
+
app.use(koaClassicServer(tmpDir, {
|
|
178
|
+
index: [],
|
|
179
|
+
showDirContents: true
|
|
180
|
+
}));
|
|
181
|
+
server = app.listen();
|
|
182
|
+
request = supertest(server);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
afterAll(() => { server?.close(); });
|
|
186
|
+
|
|
187
|
+
test('GET /index.ejs via symlink returns 200', async () => {
|
|
188
|
+
const res = await request.get('/index.ejs');
|
|
189
|
+
expect(res.status).toBe(200);
|
|
190
|
+
expect(res.text).toContain('EJS via Symlink');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('GET /linked-style.css via symlink returns 200 with correct mime', async () => {
|
|
194
|
+
const res = await request.get('/linked-style.css');
|
|
195
|
+
expect(res.status).toBe(200);
|
|
196
|
+
expect(res.headers['content-type']).toMatch(/css/);
|
|
197
|
+
expect(res.text).toContain('body { color: red; }');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// =========================================================================
|
|
202
|
+
// 4. BUG FIX - Symlink to file with template engine
|
|
203
|
+
// =========================================================================
|
|
204
|
+
describe('EJS template via symlink (bug fix)', () => {
|
|
205
|
+
let server, request;
|
|
206
|
+
|
|
207
|
+
beforeAll(() => {
|
|
208
|
+
const app = new Koa();
|
|
209
|
+
app.use(koaClassicServer(tmpDir, {
|
|
210
|
+
index: ['index.ejs'],
|
|
211
|
+
showDirContents: true,
|
|
212
|
+
template: {
|
|
213
|
+
ext: ['ejs'],
|
|
214
|
+
render: async (ctx, next, filePath) => {
|
|
215
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
216
|
+
ctx.type = 'text/html';
|
|
217
|
+
ctx.body = content;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}));
|
|
221
|
+
server = app.listen();
|
|
222
|
+
request = supertest(server);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
afterAll(() => { server?.close(); });
|
|
226
|
+
|
|
227
|
+
test('GET / renders EJS template through symlink', async () => {
|
|
228
|
+
const res = await request.get('/');
|
|
229
|
+
expect(res.status).toBe(200);
|
|
230
|
+
expect(res.headers['content-type']).toMatch(/html/);
|
|
231
|
+
expect(res.text).toContain('EJS via Symlink');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// =========================================================================
|
|
236
|
+
// 5. Directory as symlink
|
|
237
|
+
// =========================================================================
|
|
238
|
+
describe('symlink to directory', () => {
|
|
239
|
+
let server, request;
|
|
240
|
+
|
|
241
|
+
beforeAll(() => {
|
|
242
|
+
const app = new Koa();
|
|
243
|
+
app.use(koaClassicServer(tmpDir, {
|
|
244
|
+
index: [],
|
|
245
|
+
showDirContents: true
|
|
246
|
+
}));
|
|
247
|
+
server = app.listen();
|
|
248
|
+
request = supertest(server);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
afterAll(() => { server?.close(); });
|
|
252
|
+
|
|
253
|
+
test('GET /linked-subdir/ lists directory contents', async () => {
|
|
254
|
+
const res = await request.get('/linked-subdir/');
|
|
255
|
+
expect(res.status).toBe(200);
|
|
256
|
+
expect(res.text).toContain('file.txt');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('GET /linked-subdir/file.txt serves file inside symlinked dir', async () => {
|
|
260
|
+
const res = await request.get('/linked-subdir/file.txt');
|
|
261
|
+
expect(res.status).toBe(200);
|
|
262
|
+
expect(res.text).toContain('content inside real-subdir');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// =========================================================================
|
|
267
|
+
// 6. Broken symlink
|
|
268
|
+
// =========================================================================
|
|
269
|
+
describe('broken symlink', () => {
|
|
270
|
+
let server, request;
|
|
271
|
+
|
|
272
|
+
beforeAll(() => {
|
|
273
|
+
const app = new Koa();
|
|
274
|
+
app.use(koaClassicServer(tmpDir, {
|
|
275
|
+
index: [],
|
|
276
|
+
showDirContents: true
|
|
277
|
+
}));
|
|
278
|
+
server = app.listen();
|
|
279
|
+
request = supertest(server);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
afterAll(() => { server?.close(); });
|
|
283
|
+
|
|
284
|
+
test('GET /broken-link.html returns 404, not crash', async () => {
|
|
285
|
+
const res = await request.get('/broken-link.html');
|
|
286
|
+
expect(res.status).toBe(404);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// =========================================================================
|
|
291
|
+
// 7. Circular symlink
|
|
292
|
+
// =========================================================================
|
|
293
|
+
describe('circular symlink', () => {
|
|
294
|
+
let server, request;
|
|
295
|
+
let circularExists = false;
|
|
296
|
+
|
|
297
|
+
beforeAll(() => {
|
|
298
|
+
circularExists = fs.existsSync(path.join(tmpDir, 'circular-a'));
|
|
299
|
+
const app = new Koa();
|
|
300
|
+
app.use(koaClassicServer(tmpDir, {
|
|
301
|
+
index: [],
|
|
302
|
+
showDirContents: true
|
|
303
|
+
}));
|
|
304
|
+
server = app.listen();
|
|
305
|
+
request = supertest(server);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
afterAll(() => { server?.close(); });
|
|
309
|
+
|
|
310
|
+
test('GET /circular-a does not cause infinite loop', async () => {
|
|
311
|
+
if (!circularExists) {
|
|
312
|
+
// Skip if circular symlinks could not be created on this OS
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const res = await request.get('/circular-a');
|
|
316
|
+
// Should return an error status, not hang
|
|
317
|
+
expect([404, 500]).toContain(res.status);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// =========================================================================
|
|
322
|
+
// 8. REGRESSION - Regular non-index file unchanged
|
|
323
|
+
// =========================================================================
|
|
324
|
+
describe('regular non-index file (regression)', () => {
|
|
325
|
+
let server, request;
|
|
326
|
+
|
|
327
|
+
beforeAll(() => {
|
|
328
|
+
const app = new Koa();
|
|
329
|
+
app.use(koaClassicServer(tmpDir, {
|
|
330
|
+
index: ['index.html'],
|
|
331
|
+
showDirContents: true
|
|
332
|
+
}));
|
|
333
|
+
server = app.listen();
|
|
334
|
+
request = supertest(server);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
afterAll(() => { server?.close(); });
|
|
338
|
+
|
|
339
|
+
test('GET /style.css serves regular file correctly', async () => {
|
|
340
|
+
const res = await request.get('/style.css');
|
|
341
|
+
expect(res.status).toBe(200);
|
|
342
|
+
expect(res.headers['content-type']).toMatch(/css/);
|
|
343
|
+
expect(res.text).toContain('body { color: red; }');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// =========================================================================
|
|
348
|
+
// 9. Directory listing shows symlink indicators
|
|
349
|
+
// =========================================================================
|
|
350
|
+
describe('directory listing symlink indicators', () => {
|
|
351
|
+
let server, request;
|
|
352
|
+
|
|
353
|
+
beforeAll(() => {
|
|
354
|
+
const app = new Koa();
|
|
355
|
+
app.use(koaClassicServer(tmpDir, {
|
|
356
|
+
index: [],
|
|
357
|
+
showDirContents: true
|
|
358
|
+
}));
|
|
359
|
+
server = app.listen();
|
|
360
|
+
request = supertest(server);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
afterAll(() => { server?.close(); });
|
|
364
|
+
|
|
365
|
+
test('symlink to file shows ( Symlink ) indicator', async () => {
|
|
366
|
+
const res = await request.get('/');
|
|
367
|
+
expect(res.status).toBe(200);
|
|
368
|
+
// index.ejs is a symlink to a file
|
|
369
|
+
expect(res.text).toContain('index.ejs');
|
|
370
|
+
expect(res.text).toMatch(/index\.ejs<\/a>\s*\( Symlink \)/);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('symlink to directory shows ( Symlink ) indicator', async () => {
|
|
374
|
+
const res = await request.get('/');
|
|
375
|
+
expect(res.status).toBe(200);
|
|
376
|
+
expect(res.text).toContain('linked-subdir');
|
|
377
|
+
expect(res.text).toMatch(/linked-subdir<\/a>\s*\( Symlink \)/);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('broken symlink shows ( Broken Symlink ) indicator without link', async () => {
|
|
381
|
+
const res = await request.get('/');
|
|
382
|
+
expect(res.status).toBe(200);
|
|
383
|
+
expect(res.text).toContain('broken-link.html');
|
|
384
|
+
expect(res.text).toContain('( Broken Symlink )');
|
|
385
|
+
// Broken symlink name should NOT be wrapped in <a> tag
|
|
386
|
+
expect(res.text).not.toMatch(/<a[^>]*>broken-link\.html<\/a>/);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('regular file does NOT show symlink indicator', async () => {
|
|
390
|
+
const res = await request.get('/');
|
|
391
|
+
expect(res.status).toBe(200);
|
|
392
|
+
expect(res.text).toContain('style.css');
|
|
393
|
+
// style.css (not linked-style.css) should not have any symlink indicator
|
|
394
|
+
expect(res.text).not.toMatch(/>style\.css<\/a>\s*\( Symlink \)/);
|
|
395
|
+
expect(res.text).not.toMatch(/>style\.css<\/a>\s*\( Broken Symlink \)/);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('symlink to file shows target mime type, not "unknown"', async () => {
|
|
399
|
+
const res = await request.get('/');
|
|
400
|
+
expect(res.status).toBe(200);
|
|
401
|
+
// linked-style.css is a symlink to style.css - should show text/css mime
|
|
402
|
+
expect(res.text).toMatch(/linked-style\.css[\s\S]*?text\/css/);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('symlink to directory shows DIR type', async () => {
|
|
406
|
+
const res = await request.get('/');
|
|
407
|
+
expect(res.status).toBe(200);
|
|
408
|
+
// linked-subdir is a symlink to a directory - should show DIR
|
|
409
|
+
expect(res.text).toMatch(/linked-subdir[\s\S]*?DIR/);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// =========================================================================
|
|
414
|
+
// 10. Symlink as index with RegExp pattern
|
|
415
|
+
// =========================================================================
|
|
416
|
+
describe('symlink as index with RegExp pattern', () => {
|
|
417
|
+
let server, request;
|
|
418
|
+
|
|
419
|
+
beforeAll(() => {
|
|
420
|
+
const app = new Koa();
|
|
421
|
+
app.use(koaClassicServer(tmpDir, {
|
|
422
|
+
index: [/index\.[eE][jJ][sS]/],
|
|
423
|
+
showDirContents: true
|
|
424
|
+
}));
|
|
425
|
+
server = app.listen();
|
|
426
|
+
request = supertest(server);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
afterAll(() => { server?.close(); });
|
|
430
|
+
|
|
431
|
+
test('GET / finds symlinked index.ejs via RegExp pattern', async () => {
|
|
432
|
+
const res = await request.get('/');
|
|
433
|
+
expect(res.status).toBe(200);
|
|
434
|
+
expect(res.text).toContain('EJS via Symlink');
|
|
435
|
+
expect(res.text).not.toContain('Index of');
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
});
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,93 @@ 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.5.0] - 2026-02-28
|
|
9
|
+
|
|
10
|
+
### ✨ New Feature
|
|
11
|
+
|
|
12
|
+
#### hideExtension - Clean URLs (mod_rewrite-like)
|
|
13
|
+
- **New Option**: `hideExtension: { ext: '.ejs', redirect: 301 }`
|
|
14
|
+
- **Purpose**: Hide file extensions from URLs for SEO-friendly clean URLs
|
|
15
|
+
- **Clean URL Resolution**: `/about` → serves `about.ejs` (when file exists)
|
|
16
|
+
- **Extension Redirect**: `/about.ejs` → 301 redirect to `/about` (preserves query string)
|
|
17
|
+
- **Index File Redirect**: `/index.ejs` → redirect to `/`, `/section/index.ejs` → redirect to `/section/`
|
|
18
|
+
- **Conflict Resolution**: `.ejs` file wins over both directories and extensionless files with same base name
|
|
19
|
+
- **Case-Sensitive**: Extension matching is case-sensitive (`.ejs` ≠ `.EJS`)
|
|
20
|
+
- **No Interference**: URLs with other extensions (`.css`, `.png`, etc.) pass through normally
|
|
21
|
+
- **Trailing Slash**: `/about/` always means directory, never resolves to file
|
|
22
|
+
- **Redirect uses `ctx.originalUrl`**: Preserves URL prefixes from upstream middleware (i18n, routing)
|
|
23
|
+
|
|
24
|
+
#### Input Validation
|
|
25
|
+
- `hideExtension: true` → throws Error (must be an object)
|
|
26
|
+
- `hideExtension: {}` → throws Error (missing `ext`)
|
|
27
|
+
- `hideExtension: { ext: '' }` → throws Error (empty ext)
|
|
28
|
+
- `hideExtension: { ext: 'ejs' }` → warning + auto-normalizes to `.ejs`
|
|
29
|
+
- `hideExtension: { ext: '.ejs', redirect: 'abc' }` → throws Error (redirect must be number)
|
|
30
|
+
|
|
31
|
+
#### Integration with Existing Options
|
|
32
|
+
- **urlsReserved**: Checked before `hideExtension`, no interference
|
|
33
|
+
- **urlPrefix**: `hideExtension` works on path after prefix removal
|
|
34
|
+
- **useOriginalUrl**: Resolution follows setting; redirect always uses `ctx.originalUrl`
|
|
35
|
+
- **template**: Resolved files pass through template engine normally
|
|
36
|
+
- **method**: `hideExtension` only applies to allowed HTTP methods
|
|
37
|
+
|
|
38
|
+
### 🧪 Testing
|
|
39
|
+
- Added `__tests__/hideExtension.test.js` with 31 tests covering:
|
|
40
|
+
- Clean URL resolution (single and multi-level paths)
|
|
41
|
+
- Extension redirect (301/302, query string preservation)
|
|
42
|
+
- Directory/file conflict resolution
|
|
43
|
+
- Trailing slash behavior
|
|
44
|
+
- Extensionless file conflict
|
|
45
|
+
- Index file redirect (`/index.ejs` → `/`)
|
|
46
|
+
- `urlsReserved` interaction
|
|
47
|
+
- `useOriginalUrl` interaction (redirect preserves prefix)
|
|
48
|
+
- Case-sensitive matching
|
|
49
|
+
- No interference with other extensions
|
|
50
|
+
- Template engine integration
|
|
51
|
+
- Input validation (7 validation tests)
|
|
52
|
+
- All 278 existing tests still pass (zero regressions)
|
|
53
|
+
|
|
54
|
+
### 📦 Package Changes
|
|
55
|
+
- **Version**: `2.4.0` → `2.5.0`
|
|
56
|
+
- **Semver**: Minor version bump (new feature, backward compatible)
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## [2.4.0] - 2026-02-28
|
|
61
|
+
|
|
62
|
+
### 🐛 Bug Fix
|
|
63
|
+
|
|
64
|
+
#### Fixed Symlink Support in Index File Discovery and Directory Listing
|
|
65
|
+
- **Issue**: On systems where served files are symbolic links (NixOS buildFHSEnv, Docker bind mounts, `npm link`, Capistrano-style deploys), `findIndexFile()` failed because `dirent.isFile()` returns `false` for symlinks. This caused `GET /` to show directory listing instead of rendering the index file, and `GET /index.ejs` to return 404.
|
|
66
|
+
- **Impact**: HIGH - Server unusable on NixOS/buildFHSEnv and similar environments
|
|
67
|
+
- **Fix**: Added `isFileOrSymlinkToFile()` / `isDirOrSymlinkToDir()` helpers that follow symlinks via `fs.promises.stat()` only when `dirent.isSymbolicLink()` is true, adding zero overhead for regular files.
|
|
68
|
+
- **Code**: `index.cjs` - new helpers + `findIndexFile()` + `show_dir()`
|
|
69
|
+
|
|
70
|
+
### ✨ Improvements
|
|
71
|
+
|
|
72
|
+
#### Directory Listing Symlink Indicators
|
|
73
|
+
- Symlinks to files/directories show `( Symlink )` label next to the name
|
|
74
|
+
- Broken/circular symlinks show `( Broken Symlink )` label (name visible but not clickable)
|
|
75
|
+
- Symlinks resolved to effective type for MIME and size display (e.g. symlink to dir shows `DIR`)
|
|
76
|
+
- Sorting uses effective type (symlink-to-dir sorts with directories)
|
|
77
|
+
|
|
78
|
+
### 🧪 Testing
|
|
79
|
+
- Added `__tests__/symlink.test.js` with 17 tests covering:
|
|
80
|
+
- Regular file as index (regression)
|
|
81
|
+
- Symlink to file as index (string and RegExp patterns)
|
|
82
|
+
- Direct GET to symlinked file
|
|
83
|
+
- EJS template via symlink
|
|
84
|
+
- Symlink to directory (listing and file access)
|
|
85
|
+
- Broken and circular symlinks
|
|
86
|
+
- Directory listing indicators (`( Symlink )`, `( Broken Symlink )`)
|
|
87
|
+
- Regular file regression (no false symlink indicator)
|
|
88
|
+
- All 187 existing tests still pass (zero regressions)
|
|
89
|
+
|
|
90
|
+
### 📦 Package Changes
|
|
91
|
+
- **Semver**: Minor version bump (new feature, backward compatible)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
8
95
|
## [2.3.0] - 2026-01-03
|
|
9
96
|
|
|
10
97
|
### 🔄 Renamed Options (with Backward Compatibility)
|
package/docs/DOCUMENTATION.md
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
- Navigazione parent directory con link ".. Parent Directory"
|
|
36
36
|
- Indicazione chiara del tipo di risorsa (DIR, MIME type)
|
|
37
37
|
- Gestione e visualizzazione cartelle riservate
|
|
38
|
-
- Supporto link simbolici
|
|
38
|
+
- Supporto completo link simbolici (symlink a file, directory, broken e circolari)
|
|
39
39
|
|
|
40
40
|
#### 3. Supporto Template Engine
|
|
41
41
|
- Integrazione flessibile con motori di template (es. EJS, Pug, Handlebars)
|
|
@@ -777,6 +777,49 @@ http://localhost:3000/file.txt/ → http://localhost:3000/file.txt
|
|
|
777
777
|
|
|
778
778
|
Questo assicura comportamento coerente indipendentemente dal trailing slash.
|
|
779
779
|
|
|
780
|
+
### Gestione dei Link Simbolici (Symlink)
|
|
781
|
+
|
|
782
|
+
Il middleware supporta completamente i link simbolici (symlink). Questo è fondamentale in ambienti dove i file serviti sono symlink anziché file regolari, come ad esempio:
|
|
783
|
+
|
|
784
|
+
- **NixOS con buildFHSEnv** (chroot-like): i file nella directory www/ appaiono come symlink al Nix store
|
|
785
|
+
- **Docker bind mounts**: i file montati possono risultare come symlink
|
|
786
|
+
- **npm link**: i pacchetti linkati sono symlink
|
|
787
|
+
- **Deploy Capistrano-style**: la directory `current` è un symlink alla release attiva
|
|
788
|
+
|
|
789
|
+
#### Comportamento
|
|
790
|
+
|
|
791
|
+
Il middleware segue i symlink in modo trasparente tramite `fs.promises.stat()` (che risolve il symlink al target reale), ma solo quando `dirent.isSymbolicLink()` è `true`. Per i file regolari non viene effettuata alcuna chiamata aggiuntiva (zero overhead).
|
|
792
|
+
|
|
793
|
+
#### Risoluzione Index File
|
|
794
|
+
|
|
795
|
+
Quando il middleware cerca un file index in una directory (opzione `index`), i symlink che puntano a file regolari vengono inclusi nella ricerca, sia con pattern stringa che RegExp:
|
|
796
|
+
|
|
797
|
+
```
|
|
798
|
+
Directory:
|
|
799
|
+
index.ejs → /nix/store/.../index.ejs (symlink a file)
|
|
800
|
+
style.css (file regolare)
|
|
801
|
+
|
|
802
|
+
Config: index: ['index.ejs']
|
|
803
|
+
Risultato: Serve index.ejs attraverso il symlink ✓
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
#### Directory Listing
|
|
807
|
+
|
|
808
|
+
Nel directory listing, i symlink sono identificati visivamente:
|
|
809
|
+
|
|
810
|
+
| Caso | Indicatore | Cliccabile | Tipo mostrato |
|
|
811
|
+
|------|-----------|------------|---------------|
|
|
812
|
+
| Symlink a file | `( Symlink )` | Sì | MIME type del target |
|
|
813
|
+
| Symlink a directory | `( Symlink )` | Sì | `DIR` |
|
|
814
|
+
| Symlink rotto/circolare | `( Broken Symlink )` | No | `unknown` |
|
|
815
|
+
| File/directory regolare | nessuno | Sì | tipo reale |
|
|
816
|
+
|
|
817
|
+
#### Casi Limite
|
|
818
|
+
|
|
819
|
+
- **Broken symlink** (target inesistente): il GET diretto restituisce 404; nel listing il nome appare senza link
|
|
820
|
+
- **Symlink circolare** (A → B → A): trattato come broken symlink, nessun loop infinito
|
|
821
|
+
- **Symlink a directory**: navigabile come una directory regolare, i file al suo interno sono accessibili
|
|
822
|
+
|
|
780
823
|
### MIME Types
|
|
781
824
|
|
|
782
825
|
MIME types riconosciuti automaticamente tramite estensione file:
|
|
@@ -1494,7 +1537,7 @@ app.use(koaClassicServer(__dirname + '/public', {
|
|
|
1494
1537
|
## Informazioni Aggiuntive
|
|
1495
1538
|
|
|
1496
1539
|
### Versione
|
|
1497
|
-
**
|
|
1540
|
+
**2.4.0**
|
|
1498
1541
|
|
|
1499
1542
|
### Autore
|
|
1500
1543
|
**Italo Paesano**
|
|
@@ -1558,8 +1601,13 @@ Contributi benvenuti! Per favore:
|
|
|
1558
1601
|
|
|
1559
1602
|
### Changelog
|
|
1560
1603
|
|
|
1604
|
+
#### v1.2.0
|
|
1605
|
+
- Fix: supporto completo link simbolici in `findIndexFile()` e directory listing
|
|
1606
|
+
- Nuovi helper `isFileOrSymlinkToFile()` / `isDirOrSymlinkToDir()` (zero overhead per file regolari)
|
|
1607
|
+
- Directory listing: indicatori `( Symlink )` e `( Broken Symlink )`, tipo effettivo (MIME/DIR) per symlink
|
|
1608
|
+
- 17 nuovi test per tutti gli scenari symlink (NixOS, Docker, npm link, broken, circular)
|
|
1609
|
+
|
|
1561
1610
|
#### v1.1.0
|
|
1562
|
-
- Versione attuale
|
|
1563
1611
|
- Supporto conditional exports
|
|
1564
1612
|
- Test suite completa
|
|
1565
1613
|
|
|
@@ -1580,6 +1628,6 @@ Repository con esempi completi disponibile nella cartella `customTest/` del prog
|
|
|
1580
1628
|
|
|
1581
1629
|
---
|
|
1582
1630
|
|
|
1583
|
-
**Documentazione generata per koa-classic-server
|
|
1631
|
+
**Documentazione generata per koa-classic-server v2.4.0**
|
|
1584
1632
|
|
|
1585
|
-
*Ultimo aggiornamento:
|
|
1633
|
+
*Ultimo aggiornamento: 2026-02-28*
|