koa-classic-server 2.1.4 โ†’ 2.4.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/docs/CHANGELOG.md CHANGED
@@ -5,6 +5,166 @@ 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.4.0] - 2026-02-28
9
+
10
+ ### ๐Ÿ› Bug Fix
11
+
12
+ #### Fixed Symlink Support in Index File Discovery and Directory Listing
13
+ - **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.
14
+ - **Impact**: HIGH - Server unusable on NixOS/buildFHSEnv and similar environments
15
+ - **Fix**: Added `isFileOrSymlinkToFile()` / `isDirOrSymlinkToDir()` helpers that follow symlinks via `fs.promises.stat()` only when `dirent.isSymbolicLink()` is true, adding zero overhead for regular files.
16
+ - **Code**: `index.cjs` - new helpers + `findIndexFile()` + `show_dir()`
17
+
18
+ ### โœจ Improvements
19
+
20
+ #### Directory Listing Symlink Indicators
21
+ - Symlinks to files/directories show `( Symlink )` label next to the name
22
+ - Broken/circular symlinks show `( Broken Symlink )` label (name visible but not clickable)
23
+ - Symlinks resolved to effective type for MIME and size display (e.g. symlink to dir shows `DIR`)
24
+ - Sorting uses effective type (symlink-to-dir sorts with directories)
25
+
26
+ ### ๐Ÿงช Testing
27
+ - Added `__tests__/symlink.test.js` with 17 tests covering:
28
+ - Regular file as index (regression)
29
+ - Symlink to file as index (string and RegExp patterns)
30
+ - Direct GET to symlinked file
31
+ - EJS template via symlink
32
+ - Symlink to directory (listing and file access)
33
+ - Broken and circular symlinks
34
+ - Directory listing indicators (`( Symlink )`, `( Broken Symlink )`)
35
+ - Regular file regression (no false symlink indicator)
36
+ - All 187 existing tests still pass (zero regressions)
37
+
38
+ ### ๐Ÿ“ฆ Package Changes
39
+ - **Semver**: Minor version bump (new feature, backward compatible)
40
+
41
+ ---
42
+
43
+ ## [2.3.0] - 2026-01-03
44
+
45
+ ### ๐Ÿ”„ Renamed Options (with Backward Compatibility)
46
+
47
+ #### Renamed Caching Options for Clarity
48
+ - **Old Names** (DEPRECATED): `enableCaching`, `cacheMaxAge`
49
+ - **New Names**: `browserCacheEnabled`, `browserCacheMaxAge`
50
+ - **Reason**: Improved clarity - these options specifically control browser-side HTTP caching
51
+ - **Backward Compatible**: Old names still work but display deprecation warnings
52
+
53
+ #### Deprecation Warnings
54
+ When using deprecated option names, a warning is displayed on the terminal:
55
+ ```
56
+ [koa-classic-server] DEPRECATION WARNING: The "enableCaching" option is deprecated and will be removed in future versions.
57
+ Current usage: enableCaching: true
58
+ Recommended: browserCacheEnabled: true
59
+ Please update your configuration to use the new option name.
60
+ ```
61
+
62
+ ### ๐Ÿ“ Documentation Updates
63
+
64
+ - Updated README.md with new option names
65
+ - Updated JSDoc comments in index.cjs
66
+ - Added deprecation notes in Options table
67
+ - All examples updated to use new names
68
+
69
+ ### ๐Ÿ”ง Changes
70
+
71
+ - **index.cjs**: Lines 109-135 - Added backward compatibility logic with deprecation warnings
72
+ - **index.cjs**: Lines 47-58 - Updated JSDoc comments
73
+ - **index.cjs**: Lines 350, 361 - Updated code to use new option names
74
+ - **README.md**: Updated all references to use new names, added deprecation notes
75
+ - **package.json**: Version bumped from `2.2.0` to `2.3.0`
76
+
77
+ ### โš ๏ธ Migration Guide
78
+
79
+ **No immediate changes required** - old option names still work.
80
+
81
+ **Recommended migration:**
82
+
83
+ ```javascript
84
+ // Old (still works, but deprecated)
85
+ app.use(koaClassicServer('/public', {
86
+ enableCaching: true,
87
+ cacheMaxAge: 3600
88
+ }));
89
+
90
+ // New (recommended)
91
+ app.use(koaClassicServer('/public', {
92
+ browserCacheEnabled: true,
93
+ browserCacheMaxAge: 3600
94
+ }));
95
+ ```
96
+
97
+ **Timeline:**
98
+ - **v2.3.0**: Old names work with deprecation warnings
99
+ - **Future versions**: Old names may be removed (will be announced in advance)
100
+
101
+ ### ๐Ÿ“ฆ Package Changes
102
+
103
+ - **Version**: `2.2.0` โ†’ `2.3.0`
104
+ - **Semver**: Minor version bump (new feature names, backward compatible)
105
+
106
+ ---
107
+
108
+ ## [2.2.0] - 2026-01-03
109
+
110
+ ### โœจ Features
111
+
112
+ #### Added useOriginalUrl Option
113
+ - **New Option**: `useOriginalUrl` (Boolean, default: `true`)
114
+ - **Purpose**: Controls URL resolution for file serving - use `ctx.originalUrl` (immutable) or `ctx.url` (mutable)
115
+ - **Use Case**: Compatibility with URL rewriting middleware (i18n, routing)
116
+ - **Backward Compatible**: Default value `true` maintains existing behavior
117
+
118
+ #### URL Rewriting Middleware Support
119
+ - **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`
120
+ - **Solution**: Set `useOriginalUrl: false` to use the rewritten URL from `ctx.url` instead
121
+ - **Example**: i18n middleware that strips language prefixes (`/it/page.html` โ†’ `/page.html`)
122
+
123
+ ### ๐Ÿ“ Documentation
124
+
125
+ - Added comprehensive `useOriginalUrl` documentation in README.md
126
+ - Added JSDoc comments in index.cjs
127
+ - Included practical i18n middleware example
128
+ - Added option to API reference table
129
+
130
+ ### ๐Ÿ”ง Changes
131
+
132
+ - **index.cjs**: Line 108 - Added `useOriginalUrl` option initialization
133
+ - **index.cjs**: Lines 117-125 - Modified URL construction logic to support both `ctx.originalUrl` and `ctx.url`
134
+ - **README.md**: Added detailed section explaining `useOriginalUrl` with examples
135
+ - **package.json**: Version bumped from `2.1.4` to `2.2.0`
136
+
137
+ ### ๐Ÿ’ก Usage Example
138
+
139
+ ```javascript
140
+ // i18n middleware example
141
+ app.use(async (ctx, next) => {
142
+ if (ctx.path.match(/^\/it\//)) {
143
+ ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html โ†’ /page.html
144
+ }
145
+ await next();
146
+ });
147
+
148
+ app.use(koaClassicServer('/www', {
149
+ useOriginalUrl: false // Use rewritten URL
150
+ }));
151
+ ```
152
+
153
+ ### โš ๏ธ Migration Notes
154
+
155
+ **No breaking changes** - this is a backward-compatible release.
156
+
157
+ - **Default behavior unchanged**: `useOriginalUrl` defaults to `true`
158
+ - **No code changes required** for existing implementations
159
+ - **New feature**: Set `useOriginalUrl: false` if you use URL rewriting middleware
160
+
161
+ ### ๐Ÿ“ฆ Package Changes
162
+
163
+ - **Version**: `2.1.4` โ†’ `2.2.0`
164
+ - **Semver**: Minor version bump (new feature, backward compatible)
165
+
166
+ ---
167
+
8
168
  ## [2.1.3] - 2025-11-25
9
169
 
10
170
  ### ๐Ÿ”ง Configuration Changes
@@ -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*
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
- cacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 1 hour)
48
- enableCaching: false, // Enable HTTP caching headers (ETag, Last-Modified)
49
- // NOTE: Default is false for development.
50
- // In production, it's recommended to set enableCaching: true
51
- // to reduce bandwidth usage and improve performance.
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,71 @@ 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 enableCaching is false for development environments.
109
+ // NOTE: Default browserCacheEnabled is false for development environments.
104
110
  // For production deployments, it's strongly recommended to enable caching
105
- // by setting enableCaching: true to benefit from reduced bandwidth and improved performance.
106
- options.cacheMaxAge = typeof options.cacheMaxAge === 'number' && options.cacheMaxAge >= 0 ? options.cacheMaxAge : 3600;
107
- options.enableCaching = typeof options.enableCaching === 'boolean' ? options.enableCaching : false;
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;
140
+
141
+ /**
142
+ * Returns true if dirent is a regular file or a symlink pointing to a regular file.
143
+ * Uses fs.promises.stat (which follows symlinks) only when dirent.isSymbolicLink() is true.
144
+ */
145
+ async function isFileOrSymlinkToFile(dirent, dirPath) {
146
+ if (dirent.isFile()) return true;
147
+ if (dirent.isSymbolicLink()) {
148
+ try {
149
+ const realStat = await fs.promises.stat(path.join(dirPath, dirent.name));
150
+ return realStat.isFile();
151
+ } catch {
152
+ return false; // Broken or circular symlink
153
+ }
154
+ }
155
+ return false;
156
+ }
157
+
158
+ /**
159
+ * Returns true if dirent is a directory or a symlink pointing to a directory.
160
+ * Uses fs.promises.stat (which follows symlinks) only when dirent.isSymbolicLink() is true.
161
+ */
162
+ async function isDirOrSymlinkToDir(dirent, dirPath) {
163
+ if (dirent.isDirectory()) return true;
164
+ if (dirent.isSymbolicLink()) {
165
+ try {
166
+ const realStat = await fs.promises.stat(path.join(dirPath, dirent.name));
167
+ return realStat.isDirectory();
168
+ } catch {
169
+ return false; // Broken or circular symlink
170
+ }
171
+ }
172
+ return false;
173
+ }
108
174
 
109
175
  return async (ctx, next) => {
110
176
  // Check if method is allowed
@@ -113,12 +179,14 @@ module.exports = function koaClassicServer(
113
179
  return;
114
180
  }
115
181
 
116
- // Normalize URL (remove trailing slash)
182
+ // Construct full URL based on useOriginalUrl option
183
+ const urlToUse = options.useOriginalUrl ? ctx.originalUrl : ctx.url;
184
+ const fullUrl = ctx.protocol + '://' + ctx.host + urlToUse;
117
185
  let pageHref = '';
118
- if (ctx.href.charAt(ctx.href.length - 1) === '/') {
119
- pageHref = new URL(ctx.href.slice(0, -1));
186
+ if (fullUrl.charAt(fullUrl.length - 1) === '/') {
187
+ pageHref = new URL(fullUrl.slice(0, -1));
120
188
  } else {
121
- pageHref = new URL(ctx.href);
189
+ pageHref = new URL(fullUrl);
122
190
  }
123
191
 
124
192
  // Check URL prefix
@@ -225,10 +293,16 @@ module.exports = function koaClassicServer(
225
293
  // Read directory contents
226
294
  const files = await fs.promises.readdir(dirPath, { withFileTypes: true });
227
295
 
228
- // Filter only files (not directories)
229
- const fileNames = files
230
- .filter(dirent => dirent.isFile())
231
- .map(dirent => dirent.name);
296
+ // Filter files, following symlinks to determine effective type
297
+ const fileCheckResults = await Promise.all(
298
+ files.map(async dirent => ({
299
+ name: dirent.name,
300
+ isFile: await isFileOrSymlinkToFile(dirent, dirPath)
301
+ }))
302
+ );
303
+ const fileNames = fileCheckResults
304
+ .filter(entry => entry.isFile)
305
+ .map(entry => entry.name);
232
306
 
233
307
  // Search with priority order (first pattern wins)
234
308
  for (const pattern of indexPatterns) {
@@ -317,7 +391,7 @@ module.exports = function koaClassicServer(
317
391
  }
318
392
 
319
393
  // OPTIMIZATION: HTTP Caching Headers
320
- if (options.enableCaching) {
394
+ if (options.browserCacheEnabled) {
321
395
  // Generate ETag from mtime timestamp + file size
322
396
  // This ensures ETag changes when file is modified or resized
323
397
  const etag = `"${fileStat.mtime.getTime()}-${fileStat.size}"`;
@@ -328,7 +402,7 @@ module.exports = function koaClassicServer(
328
402
  // Set caching headers
329
403
  ctx.set('ETag', etag);
330
404
  ctx.set('Last-Modified', lastModified);
331
- ctx.set('Cache-Control', `public, max-age=${options.cacheMaxAge}, must-revalidate`);
405
+ ctx.set('Cache-Control', `public, max-age=${options.browserCacheMaxAge}, must-revalidate`);
332
406
 
333
407
  // OPTIMIZATION: Handle conditional requests (304 Not Modified)
334
408
 
@@ -508,27 +582,45 @@ module.exports = function koaClassicServer(
508
582
  itemUri = `${baseUrl}/${encodeURIComponent(s_name)}`;
509
583
  }
510
584
 
585
+ // Resolve symlinks to their effective type
586
+ let effectiveType = type;
587
+ let isBrokenSymlink = false;
588
+ if (type === 3) {
589
+ try {
590
+ const realStat = await fs.promises.stat(itemPath);
591
+ if (realStat.isFile()) effectiveType = 1;
592
+ else if (realStat.isDirectory()) effectiveType = 2;
593
+ } catch {
594
+ isBrokenSymlink = true; // Broken or circular symlink
595
+ }
596
+ }
597
+
511
598
  // Get file size
512
599
  let sizeStr = '-';
513
600
  let sizeBytes = 0;
514
- try {
515
- const itemStat = await fs.promises.stat(itemPath);
516
- if (type === 1) {
517
- sizeBytes = itemStat.size;
518
- sizeStr = formatSize(sizeBytes);
519
- } else {
601
+ if (!isBrokenSymlink) {
602
+ try {
603
+ const itemStat = await fs.promises.stat(itemPath);
604
+ if (effectiveType === 1) {
605
+ sizeBytes = itemStat.size;
606
+ sizeStr = formatSize(sizeBytes);
607
+ } else {
608
+ sizeStr = '-';
609
+ }
610
+ } catch (error) {
520
611
  sizeStr = '-';
521
612
  }
522
- } catch (error) {
523
- sizeStr = '-';
524
613
  }
525
614
 
526
- const mimeType = type === 2 ? "DIR" : (mime.lookup(itemPath) || 'unknown');
527
- const isReserved = pageHrefOutPrefix.pathname === '/' && options.urlsReserved.includes('/' + s_name) && (type === 2 || type === 3);
615
+ const mimeType = effectiveType === 2 ? "DIR" : (mime.lookup(itemPath) || 'unknown');
616
+ const isReserved = pageHrefOutPrefix.pathname === '/' && options.urlsReserved.includes('/' + s_name) && (effectiveType === 2 || type === 3);
528
617
 
529
618
  items.push({
530
619
  name: s_name,
531
620
  type: type,
621
+ effectiveType: effectiveType,
622
+ isSymlink: type === 3,
623
+ isBrokenSymlink: isBrokenSymlink,
532
624
  mimeType: mimeType,
533
625
  sizeStr: sizeStr,
534
626
  sizeBytes: sizeBytes,
@@ -544,19 +636,19 @@ module.exports = function koaClassicServer(
544
636
  if (sortBy === 'name') {
545
637
  comparison = a.name.localeCompare(b.name);
546
638
  } else if (sortBy === 'type') {
547
- // Sort directories first, then by mime type
548
- if (a.type === 2 && b.type !== 2) {
639
+ // Sort directories first, then by mime type (using effectiveType for symlinks)
640
+ if (a.effectiveType === 2 && b.effectiveType !== 2) {
549
641
  comparison = -1;
550
- } else if (a.type !== 2 && b.type === 2) {
642
+ } else if (a.effectiveType !== 2 && b.effectiveType === 2) {
551
643
  comparison = 1;
552
644
  } else {
553
645
  comparison = a.mimeType.localeCompare(b.mimeType);
554
646
  }
555
647
  } else if (sortBy === 'size') {
556
- // Directories always at top when sorting by size
557
- if (a.type === 2 && b.type !== 2) {
648
+ // Directories always at top when sorting by size (using effectiveType for symlinks)
649
+ if (a.effectiveType === 2 && b.effectiveType !== 2) {
558
650
  comparison = -1;
559
- } else if (a.type !== 2 && b.type === 2) {
651
+ } else if (a.effectiveType !== 2 && b.effectiveType === 2) {
560
652
  comparison = 1;
561
653
  } else {
562
654
  comparison = a.sizeBytes - b.sizeBytes;
@@ -570,16 +662,26 @@ module.exports = function koaClassicServer(
570
662
  // Generate HTML for sorted items
571
663
  for (const item of items) {
572
664
  let rowStart = '';
573
- if (item.type === 1) {
665
+ if (item.effectiveType === 1) {
574
666
  rowStart = `<tr><td> FILE `;
575
667
  } else {
576
668
  rowStart = `<tr><td>`;
577
669
  }
578
670
 
671
+ // Symlink indicator label
672
+ const symlinkLabel = item.isBrokenSymlink
673
+ ? ' ( Broken Symlink )'
674
+ : item.isSymlink
675
+ ? ' ( Symlink )'
676
+ : '';
677
+
579
678
  if (item.isReserved) {
580
- parts.push(`${rowStart} ${escapeHtml(item.name)}</td> <td> DIR BUT RESERVED</td><td>${item.sizeStr}</td></tr>`);
679
+ parts.push(`${rowStart} ${escapeHtml(item.name)}${symlinkLabel}</td> <td> DIR BUT RESERVED</td><td>${item.sizeStr}</td></tr>`);
680
+ } else if (item.isBrokenSymlink) {
681
+ // Broken symlink: name visible but not clickable
682
+ parts.push(`${rowStart} ${escapeHtml(item.name)}${symlinkLabel}</td> <td> ${escapeHtml(item.mimeType)} </td><td>${item.sizeStr}</td></tr>`);
581
683
  } else {
582
- parts.push(`${rowStart} <a href="${escapeHtml(item.itemUri)}">${escapeHtml(item.name)}</a> </td> <td> ${escapeHtml(item.mimeType)} </td><td>${item.sizeStr}</td></tr>`);
684
+ parts.push(`${rowStart} <a href="${escapeHtml(item.itemUri)}">${escapeHtml(item.name)}</a>${symlinkLabel} </td> <td> ${escapeHtml(item.mimeType)} </td><td>${item.sizeStr}</td></tr>`);
583
685
  }
584
686
  }
585
687
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koa-classic-server",
3
- "version": "2.1.4",
3
+ "version": "2.4.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": {