e22m4u 7 часов назад
Родитель
Сommit
17b016960e
3 измененных файлов с 228 добавлено и 10 удалено
  1. 1 1
      README.md
  2. 218 0
      dist/cjs/index.cjs
  3. 9 9
      package.json

+ 1 - 1
README.md

@@ -64,7 +64,7 @@ server.listen(3000, () => {
   console.log('Server is running on http://localhost:3000');
   console.log('Try to open:');
   console.log('http://localhost:3000/static/');
-  console.log('http://localhost:3000/favicon.ico');
+  console.log('http://localhost:3000/file.txt');
 });
 ```
 

+ 218 - 0
dist/cjs/index.cjs

@@ -0,0 +1,218 @@
+"use strict";
+var __create = Object.create;
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __getProtoOf = Object.getPrototypeOf;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
+var __export = (target, all) => {
+  for (var name in all)
+    __defProp(target, name, { get: all[name], enumerable: true });
+};
+var __copyProps = (to, from, except, desc) => {
+  if (from && typeof from === "object" || typeof from === "function") {
+    for (let key of __getOwnPropNames(from))
+      if (!__hasOwnProp.call(to, key) && key !== except)
+        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+  }
+  return to;
+};
+var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
+  // If the importer is in node compatibility mode or this is not an ESM
+  // file that has been converted to a CommonJS file using a Babel-
+  // compatible transform (i.e. "__esModule" has not been set), then set
+  // "default" to the CommonJS "module.exports" for node compatibility.
+  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
+  mod
+));
+var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
+
+// src/index.js
+var index_exports = {};
+__export(index_exports, {
+  HttpStaticRouter: () => HttpStaticRouter
+});
+module.exports = __toCommonJS(index_exports);
+
+// src/http-static-router.js
+var import_path = __toESM(require("path"), 1);
+var import_mime_types = __toESM(require("mime-types"), 1);
+var import_fs = __toESM(require("fs"), 1);
+
+// src/utils/escape-regexp.js
+function escapeRegexp(input) {
+  return String(input).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+__name(escapeRegexp, "escapeRegexp");
+
+// src/utils/normalize-path.js
+function normalizePath(value, noStartingSlash = false) {
+  if (typeof value !== "string") {
+    return "/";
+  }
+  const res = value.trim().replace(/\/+/g, "/").replace(/(^\/|\/$)/g, "");
+  return noStartingSlash ? res : "/" + res;
+}
+__name(normalizePath, "normalizePath");
+
+// src/http-static-router.js
+var import_js_service = require("@e22m4u/js-service");
+var import_js_format = require("@e22m4u/js-format");
+var _HttpStaticRouter = class _HttpStaticRouter extends import_js_service.DebuggableService {
+  /**
+   * Routes.
+   *
+   * @protected
+   */
+  _routes = [];
+  /**
+   * Constructor.
+   *
+   * @param {import('@e22m4u/js-service').ServiceContainer} container
+   */
+  constructor(container) {
+    super(container, {
+      noEnvironmentNamespace: true,
+      namespace: "jsHttpStaticRouter"
+    });
+  }
+  /**
+   * Add route.
+   *
+   * @param {string} remotePath
+   * @param {string} resourcePath
+   * @returns {object}
+   */
+  addRoute(remotePath, resourcePath) {
+    const debug = this.getDebuggerFor(this.addRoute);
+    resourcePath = import_path.default.resolve(resourcePath);
+    debug("Adding a new route.");
+    debug("Resource path is %v.", resourcePath);
+    debug("Remote path is %v.", remotePath);
+    let stats;
+    try {
+      stats = import_fs.default.statSync(resourcePath);
+    } catch (error) {
+      console.error(error);
+      throw new import_js_format.InvalidArgumentError(
+        "Static resource path does not exist %v.",
+        resourcePath
+      );
+    }
+    const isFile = stats.isFile();
+    debug("Resource type is %s.", isFile ? "File" : "Folder");
+    const normalizedRemotePath = normalizePath(remotePath);
+    const escapedRemotePath = escapeRegexp(normalizedRemotePath);
+    const regexp = isFile ? new RegExp(`^${escapedRemotePath}$`) : new RegExp(`^${escapedRemotePath}(?:$|\\/)`);
+    const route = { remotePath, resourcePath, regexp, isFile };
+    this._routes.push(route);
+    this._routes.sort((a, b) => b.remotePath.length - a.remotePath.length);
+    return this;
+  }
+  /**
+   * Match route.
+   *
+   * @param {import('http').IncomingMessage} req
+   * @returns {object|undefined}
+   */
+  matchRoute(req) {
+    const debug = this.getDebuggerFor(this.matchRoute);
+    debug("Matching routes with incoming request.");
+    const url = (req.url || "/").replace(/\?.*$/, "");
+    debug("Incoming request is %s %v.", req.method, url);
+    if (req.method !== "GET" && req.method !== "HEAD") {
+      debug("Method not allowed.");
+      return;
+    }
+    debug("Walking through %v routes.", this._routes.length);
+    const route = this._routes.find((route2) => {
+      const res = route2.regexp.test(url);
+      const phrase = res ? "matched" : "not matched";
+      debug("Resource %v %s.", route2.resourcePath, phrase);
+      return res;
+    });
+    route ? debug("Resource %v matched.", route.resourcePath) : debug("No route matched.");
+    return route;
+  }
+  /**
+   * Send file by route.
+   *
+   * @param {object} route
+   * @param {import('http').IncomingMessage} req
+   * @param {import('http').ServerResponse} res
+   */
+  sendFileByRoute(route, req, res) {
+    const reqUrl = req.url || "/";
+    const reqPath = reqUrl.replace(/\?.*$/, "");
+    let targetPath = route.resourcePath;
+    if (!route.isFile) {
+      const relativePath = reqPath.replace(route.regexp, "");
+      targetPath = import_path.default.join(route.resourcePath, relativePath);
+    }
+    targetPath = import_path.default.resolve(targetPath);
+    const resourceRoot = import_path.default.resolve(route.resourcePath);
+    if (!targetPath.startsWith(resourceRoot)) {
+      res.writeHead(403, { "content-type": "text/plain" });
+      res.end("403 Forbidden");
+      return;
+    }
+    import_fs.default.stat(targetPath, (statsError, stats) => {
+      if (statsError) {
+        return _handleFsError(statsError, res);
+      }
+      if (stats.isDirectory()) {
+        if (/[^/]$/.test(reqPath)) {
+          const searchMatch = reqUrl.match(/\?.*$/);
+          const search = searchMatch ? searchMatch[0] : "";
+          const normalizedPath = reqUrl.replace(/\/{2,}/g, "/");
+          res.writeHead(302, { location: `${normalizedPath}/${search}` });
+          res.end();
+          return;
+        }
+        if (/\/{2,}/.test(reqUrl)) {
+          const normalizedUrl = reqUrl.replace(/\/{2,}/g, "/");
+          res.writeHead(302, { location: normalizedUrl });
+          res.end();
+          return;
+        }
+        targetPath = import_path.default.join(targetPath, "index.html");
+      }
+      const extname = import_path.default.extname(targetPath);
+      const contentType = import_mime_types.default.contentType(extname) || "application/octet-stream";
+      const fileStream = (0, import_fs.createReadStream)(targetPath);
+      fileStream.on("error", (error) => {
+        _handleFsError(error, res);
+      });
+      fileStream.on("open", () => {
+        res.writeHead(200, { "content-type": contentType });
+        if (req.method === "HEAD") {
+          res.end();
+          return;
+        }
+        fileStream.pipe(res);
+      });
+    });
+  }
+};
+__name(_HttpStaticRouter, "HttpStaticRouter");
+var HttpStaticRouter = _HttpStaticRouter;
+function _handleFsError(error, res) {
+  if (res.headersSent) {
+    return;
+  }
+  if ("code" in error && error.code === "ENOENT") {
+    res.writeHead(404, { "content-type": "text/plain" });
+    res.write("404 Not Found");
+    res.end();
+  } else {
+    res.writeHead(500, { "content-type": "text/plain" });
+    res.write("500 Internal Server Error");
+    res.end();
+  }
+}
+__name(_handleFsError, "_handleFsError");
+// Annotate the CommonJS export names for ESM import in node:
+0 && (module.exports = {
+  HttpStaticRouter
+});

+ 9 - 9
package.json

@@ -37,29 +37,29 @@
     "prepare": "husky"
   },
   "dependencies": {
-    "@e22m4u/js-format": "~0.2.1",
-    "@e22m4u/js-service": "~0.4.6",
-    "mime-types": "~3.0.1"
+    "@e22m4u/js-format": "~0.3.2",
+    "@e22m4u/js-service": "~0.5.1",
+    "mime-types": "~3.0.2"
   },
   "devDependencies": {
-    "@commitlint/cli": "~20.3.1",
-    "@commitlint/config-conventional": "~20.3.1",
+    "@commitlint/cli": "~20.4.1",
+    "@commitlint/config-conventional": "~20.4.1",
     "@eslint/js": "~9.39.2",
     "@types/chai": "~5.2.3",
     "@types/mocha": "~10.0.10",
     "c8": "~10.1.3",
     "chai": "~6.2.2",
-    "esbuild": "~0.27.2",
+    "esbuild": "~0.27.3",
     "eslint": "~9.39.2",
     "eslint-config-prettier": "~10.1.8",
     "eslint-plugin-chai-expect": "~3.1.0",
     "eslint-plugin-import": "~2.32.0",
-    "eslint-plugin-jsdoc": "~62.0.0",
+    "eslint-plugin-jsdoc": "~62.5.2",
     "eslint-plugin-mocha": "~11.2.0",
-    "globals": "~17.0.0",
+    "globals": "~17.3.0",
     "husky": "~9.1.7",
     "mocha": "~11.7.5",
-    "prettier": "~3.7.4",
+    "prettier": "~3.8.1",
     "rimraf": "~6.1.2",
     "typescript": "~5.9.3"
   }