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.
@@ -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)
@@ -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
- **1.1.0**
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 v1.1.0**
1631
+ **Documentazione generata per koa-classic-server v2.4.0**
1584
1632
 
1585
- *Ultimo aggiornamento: 2025-11-17*
1633
+ *Ultimo aggiornamento: 2026-02-28*