minimalistic-server 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.mjs +111 -13
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -1194,6 +1194,7 @@ export class FileResponse extends Response {
1194
1194
  #maxChunkSize = 4 * 1024 * 1024;
1195
1195
  #fragmentRequestMap = new WeakMap();
1196
1196
  #makeNotFoundResponse = null;
1197
+ #urlPathForDirectory = null;
1197
1198
 
1198
1199
  constructor(filePath, code = 200, contentType = null, cookies = null) {
1199
1200
  super();
@@ -1255,6 +1256,11 @@ export class FileResponse extends Response {
1255
1256
  this.#dataPromise = null;
1256
1257
  }
1257
1258
 
1259
+ setUrlPathForDirectory(urlPathForDirectory) {
1260
+ this.#urlPathForDirectory = urlPathForDirectory;
1261
+ this.#dataPromise = null;
1262
+ }
1263
+
1258
1264
  async #getFragmentRequest(headers) {
1259
1265
  let result = this.#fragmentRequestMap.get(headers);
1260
1266
 
@@ -1314,6 +1320,45 @@ export class FileResponse extends Response {
1314
1320
  return result ?? null;
1315
1321
  }
1316
1322
 
1323
+ async * #getDirectoryStream() {
1324
+ const data = await this.#retreiveData();
1325
+
1326
+ if (typeof data.body !== 'function') {
1327
+ yield data.body;
1328
+ return;
1329
+ }
1330
+
1331
+ const urlPath = this.#urlPathForDirectory;
1332
+ const parentUrlPath = urlPath.split('/').filter((x, i, arr) => x && i !== arr.length - 1).join('/');
1333
+ const filePath = this.#filePath;
1334
+
1335
+ yield `<!DOCTYPE html>
1336
+ <html lang="en">
1337
+ <head>
1338
+ <title>${escapeHtml(urlPath)}</title>
1339
+ </head>
1340
+ <body>
1341
+ ${urlPath ? `<a href="/${parentUrlPath}">Up</a><hr>` : ''}
1342
+ `;
1343
+
1344
+ const files = await fs.readdir(filePath);
1345
+ let counter = 0;
1346
+
1347
+ for (const file of files) {
1348
+ if (counter === 100) {
1349
+ await new Promise(resolve => setTimeout(resolve, 50));
1350
+ }
1351
+
1352
+ ++counter;
1353
+
1354
+ yield `<a href="/${urlPath}/${encodeURIComponent(file)}">${escapeHtml(file)}</a><br>`;
1355
+ }
1356
+
1357
+
1358
+ yield `</body>`
1359
+
1360
+ }
1361
+
1317
1362
  async * #getBodyStream(fragmentRequest) {
1318
1363
  const data = await this.#retreiveData();
1319
1364
 
@@ -1347,14 +1392,23 @@ export class FileResponse extends Response {
1347
1392
 
1348
1393
  try {
1349
1394
  filehandle = await fs.open(this.#filePath, 'r');
1350
- const size = (await filehandle.stat()).size;
1351
- const body = size > this.#maxChunkSize ? (fragmentRequest => this.#getBodyStream(fragmentRequest)) : await filehandle.readFile();
1395
+ const stat = await filehandle.stat();
1396
+ const size = stat.size;
1397
+ const readAsDirectory = stat.isDirectory() && this.#urlPathForDirectory !== null;
1398
+ const body = size > this.#maxChunkSize ? (fragmentRequest => this.#getBodyStream(fragmentRequest)) : (readAsDirectory ? (() => this.#getDirectoryStream()) : await filehandle.readFile());
1399
+
1400
+ if (readAsDirectory) {
1401
+ this.addCustomHeaders({ 'Cache-Control': 'no-store, no-cache, must-revalidate' });
1402
+ }
1352
1403
 
1353
1404
  resolve({
1354
1405
  code: this.#code,
1355
1406
  headers: this.getMergedWithOtherHeaders({
1356
- 'Content-Type': this.#contentType ?? mimeTypes[this.#filePath.split('.').at(-1).toLowerCase()] ?? 'application/octet-stream',
1357
- 'Content-Length': size,
1407
+ 'Content-Type': this.#contentType ??
1408
+ (readAsDirectory ? 'text/html; charset=utf-8' : null) ??
1409
+ mimeTypes[this.#filePath.split('.').at(-1).toLowerCase()] ??
1410
+ 'application/octet-stream',
1411
+ 'Content-Length': readAsDirectory ? null : size,
1358
1412
  }
1359
1413
  ),
1360
1414
  body,
@@ -1494,21 +1548,19 @@ export class RedirectResponse extends Response {
1494
1548
  }
1495
1549
  }
1496
1550
 
1497
- export function serve(routes, port = 80, staticFileDirectory = null, handleNotFoundError = null, handleServerError = null) {
1551
+ export function serve(routes, port = 80, staticFileDirectoryOrDirectories = null, handleNotFoundError = null, handleServerError = null) {
1498
1552
  port = +port;
1499
1553
  routes = normalizeRoutes(routes, handleServerError);
1554
+ staticFileDirectoryOrDirectories = normalizeStaticFileDirectories(staticFileDirectoryOrDirectories);
1500
1555
  safePrint(routes);
1501
-
1502
- if (staticFileDirectory !== null && staticFileDirectory !== undefined) {
1503
- staticFileDirectory = `${staticFileDirectory}`.split('/').filter(x => x).join('/');
1504
- }
1556
+ safePrint(staticFileDirectoryOrDirectories);
1505
1557
 
1506
1558
  try {
1507
1559
  unserve(port);
1508
1560
 
1509
1561
  const server = http.createServer(async (req, res) => {
1510
1562
  try {
1511
- const [code, headers, body] = await handleRequest(req, routes, staticFileDirectory, handleNotFoundError);
1563
+ const [code, headers, body] = await handleRequest(req, routes, staticFileDirectoryOrDirectories, handleNotFoundError);
1512
1564
  res.writeHead(code, headers);
1513
1565
 
1514
1566
  if (typeof body === 'function') {
@@ -1573,6 +1625,39 @@ export function unserve(port = 80) {
1573
1625
  servers.delete(port);
1574
1626
  }
1575
1627
 
1628
+ function normalizeStaticFileDirectories(staticFileDirectoryOrDirectories) {
1629
+ if (staticFileDirectoryOrDirectories !== null && staticFileDirectoryOrDirectories !== undefined) {
1630
+ if (!Array.isArray(staticFileDirectoryOrDirectories)) {
1631
+ staticFileDirectoryOrDirectories = [staticFileDirectoryOrDirectories];
1632
+ }
1633
+
1634
+ staticFileDirectoryOrDirectories = staticFileDirectoryOrDirectories.filter(x => x !== null && typeof x === 'object' || typeof x === 'string').map(x => {
1635
+ let serverFilePath = '', urlPath = '', showWholeDirectory = false;;
1636
+
1637
+ if (typeof x === 'string') {
1638
+ serverFilePath = urlPath = x;
1639
+ showWholeDirectory = false;
1640
+ } else {
1641
+ ({ serverPath: serverFilePath, urlPath, showWholeDirectory } = x);
1642
+ }
1643
+
1644
+ urlPath = `${urlPath}`.split('/').filter(x => x).join('/');
1645
+ serverFilePath = `${serverFilePath}`.split('/').filter(x => x).join('/');
1646
+ showWholeDirectory = !!showWholeDirectory;
1647
+
1648
+ return {
1649
+ urlPath,
1650
+ serverFilePath,
1651
+ showWholeDirectory,
1652
+ };
1653
+ });
1654
+ } else {
1655
+ staticFileDirectoryOrDirectories = [];
1656
+ }
1657
+
1658
+ return staticFileDirectoryOrDirectories;
1659
+ }
1660
+
1576
1661
  function normalizeRoutes(routes, handleServerError) {
1577
1662
  const flatten = {};
1578
1663
 
@@ -1698,7 +1783,7 @@ export function clearStaticCache(path = null) {
1698
1783
  }
1699
1784
  }
1700
1785
 
1701
- async function handleRequest(req, routes, staticFileDirectory, handleNotFoundError) {
1786
+ async function handleRequest(req, routes, staticFileDirectories, handleNotFoundError) {
1702
1787
  let response = new JsonResponse({
1703
1788
  message: 'Invalid data'
1704
1789
  }, 400);
@@ -1716,9 +1801,11 @@ async function handleRequest(req, routes, staticFileDirectory, handleNotFoundErr
1716
1801
  let routeHandler = routes;
1717
1802
  let handleOptions = false;
1718
1803
 
1719
- if (staticFileDirectory && method === 'GET' && path.startsWith(staticFileDirectory)) {
1804
+ const staticFileOrDirectory = method === 'GET' ? staticFileDirectories.find(x => x.urlPath === path || path.startsWith(x.urlPath + '/')) : null;
1805
+
1806
+ if (staticFileOrDirectory) {
1720
1807
  routeHandler = () => {
1721
- const filePath = decodeURI(path);
1808
+ const filePath = decodeURI(path).replace(staticFileOrDirectory.urlPath, staticFileOrDirectory.serverFilePath);
1722
1809
 
1723
1810
  let resp = staticCache.get(filePath);
1724
1811
 
@@ -1732,7 +1819,18 @@ async function handleRequest(req, routes, staticFileDirectory, handleNotFoundErr
1732
1819
  resp.setNotFoundErrorCustomResponseHandler(() => handleNotFoundError(request));
1733
1820
  }
1734
1821
 
1822
+ if (staticFileOrDirectory.showWholeDirectory) {
1823
+ resp.setUrlPathForDirectory(path);
1824
+ }
1825
+
1735
1826
  staticCache.set(filePath, resp);
1827
+
1828
+ if (staticCache.size > 500) {
1829
+ for (const k of staticCache.keys()) {
1830
+ staticCache.delete(k);
1831
+ break;
1832
+ }
1833
+ }
1736
1834
  }
1737
1835
 
1738
1836
  return resp;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimalistic-server",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "engines" : {
5
5
  "npm" : ">=8.6.0",
6
6
  "node" : ">=22.0.0"