chore: init
This commit is contained in:
commit
acbf714fc4
15 changed files with 8734 additions and 0 deletions
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# ESLint cache
|
||||
.eslintcache
|
||||
|
||||
# Build directories
|
||||
/dist/
|
||||
|
||||
# Editor directories and files
|
||||
*.swa-p
|
2
.husky/commit-msg
Executable file
2
.husky/commit-msg
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
npx --no -- commitlint --edit "$1"
|
2
.husky/pre-commit
Executable file
2
.husky/pre-commit
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
npx lint-staged
|
14
.swcrc
Normal file
14
.swcrc
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "ecmascript",
|
||||
"dynamicImport": true
|
||||
},
|
||||
"loose": true,
|
||||
"target": "es2017"
|
||||
},
|
||||
"minify": false,
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
}
|
||||
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 SVR.JS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
100
README.md
Normal file
100
README.md
Normal file
|
@ -0,0 +1,100 @@
|
|||
# SVRouter
|
||||
|
||||
SVRouter is a router library built for use in building web applications as SVR.JS 4.x mods.
|
||||
|
||||
Example SVR.JS mod that uses SVRouter (you will need to install the `svrouter` npm package either in the SVR.JS installation root or globally):
|
||||
|
||||
```js
|
||||
const svrouter = require("svrouter");
|
||||
const router = svrouter();
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
router.get("/hello", (req, res, logFacilities, config, next) => {
|
||||
res.writeHead(200, "OK", {
|
||||
"Content-Type": "text/plain"
|
||||
});
|
||||
res.write("Hello World!");
|
||||
res.end();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
module.exports.modInfo = {
|
||||
name: "Example mod with SVRouter",
|
||||
version: "0.0.0"
|
||||
};
|
||||
```
|
||||
|
||||
## Methods (exported by the `svrouter` package)
|
||||
|
||||
### _svrouter()_
|
||||
|
||||
Returns: the SVRouter router, which is a SVR.JS mod function with additional functions for adding routes and middleware.
|
||||
|
||||
## Methods (provided by the SVRouter router)
|
||||
|
||||
### _router(req, res, logFacilities, config, next)_
|
||||
|
||||
Parameters:
|
||||
- _req_ - the SVR.JS request object
|
||||
- _res_ - the SVR.JS response object
|
||||
- _logFacilities_ - the SVR.JS log facilities object
|
||||
- _config_ - the SVR.JS configuration object
|
||||
- _next_ - the callback which passes the execution to SVR.JS mods and SVR.JS internal handlers.
|
||||
|
||||
The function is a SVR.JS mod callback. You can read more about the SVR.JS mod callbacks in the [SVR.JS mod API documentation](https://svrjs.org/docs/api/svrjs-api).
|
||||
|
||||
### _router.route(method, path, callback)_
|
||||
|
||||
Parameters:
|
||||
- _method_ - the HTTP method, for which the route applies (_String_)
|
||||
- _path_ - the route path, for which the route applies. The route paths are process via the [`path-to-regexp` library](https://www.npmjs.com/package/path-to-regexp) (_String_)
|
||||
- _callback_ - the SVR.JS mod callback applied for the route (_Function_)
|
||||
|
||||
The function adds a route to the SVRouter router.
|
||||
|
||||
The _callback_ parameter has these arguments of the SVR.JS mod callback:
|
||||
- _req_ - the SVR.JS request object
|
||||
- _res_ - the SVR.JS response object
|
||||
- _logFacilities_ - the SVR.JS log facilities object
|
||||
- _config_ - the SVR.JS configuration object
|
||||
- _next_ - the callback which passes the execution to other routes, SVR.JS mods and SVR.JS internal handlers.
|
||||
|
||||
The _req_ object has an additional _params_ parameter, which contains request parameters, for example if the request URL is `/api/task/1`, and the route path is `/api/task/:id`, then the _req.params_ object is a `null` prototype object with the `id` property set to `"1"`.
|
||||
|
||||
You can read more about the SVR.JS mod callbacks in the [SVR.JS mod API documentation](https://svrjs.org/docs/api/svrjs-api).
|
||||
|
||||
### _router.pass([path, ]callback)_
|
||||
|
||||
Parameters:
|
||||
- _path_ - the path, for which the route applies. (optional, _String_)
|
||||
- _callback_ - the SVR.JS mod callback, which the SVRouter router will pass to (_Function_)
|
||||
|
||||
The function adds middleware to the SVRouter router. The middleware can be an another SVRouter router (the absolute request URLs need to be provided for the _router.route_ function in the SVRouter router).
|
||||
|
||||
The _callback_ parameter has these arguments of the SVR.JS mod callback:
|
||||
- _req_ - the SVR.JS request object
|
||||
- _res_ - the SVR.JS response object
|
||||
- _logFacilities_ - the SVR.JS log facilities object
|
||||
- _config_ - the SVR.JS configuration object
|
||||
- _next_ - the callback which passes the execution to other routes, SVR.JS mods and SVR.JS internal handlers.
|
||||
|
||||
You can read more about the SVR.JS mod callbacks in the [SVR.JS mod API documentation](https://svrjs.org/docs/api/svrjs-api).
|
||||
|
||||
### _router.get(path, callback)_
|
||||
An alias to the _router.route("GET", path, callback)_ function
|
||||
|
||||
### _router.post(path, callback)_
|
||||
An alias to the _router.route("POST", path, callback)_ function
|
||||
|
||||
### _router.put(path, callback)_
|
||||
An alias to the _router.route("PUT", path, callback)_ function
|
||||
|
||||
### _router.patch(path, callback)_
|
||||
An alias to the _router.route("PATCH", path, callback)_ function
|
||||
|
||||
### _router.delete(path, callback)_
|
||||
An alias to the _router.route("DELETE", path, callback)_ function
|
||||
|
||||
### _router.head(path, callback)_
|
||||
An alias to the _router.route("HEAD", path, callback)_ function
|
3
commitlint.config.js
Normal file
3
commitlint.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"]
|
||||
};
|
30
eslint.config.js
Normal file
30
eslint.config.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
const globals = require("globals");
|
||||
const pluginJs = require("@eslint/js");
|
||||
const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended");
|
||||
const jest = require("eslint-plugin-jest");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
sourceType: "commonjs"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["tests/*.test.js", "tests/**/*.test.js"],
|
||||
...jest.configs['flat/recommended'],
|
||||
rules: {
|
||||
...jest.configs['flat/recommended'].rules,
|
||||
'jest/prefer-expect-assertions': 'off',
|
||||
}
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
eslintPluginPrettierRecommended
|
||||
];
|
5
jest.config.js
Normal file
5
jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/tests/**/*.test.js'],
|
||||
verbose: true,
|
||||
};
|
6
lint-staged.config.js
Normal file
6
lint-staged.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
"tests/**/*.js": "eslint --cache --fix",
|
||||
"src/**/*.js": "eslint --cache --fix",
|
||||
"src/**/*.d.ts": "eslint --cache --fix",
|
||||
"utils/**/*.js": "eslint --cache --fix"
|
||||
};
|
8196
package-lock.json
generated
Normal file
8196
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
56
package.json
Normal file
56
package.json
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "svrouter",
|
||||
"version": "0.0.0",
|
||||
"description": "A router library for SVR.JS 4.x mods",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "rimraf dist/* && swc -d dist src --strip-leading-paths",
|
||||
"cz": "cz",
|
||||
"lint": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js tests/**/*.test.js tests/**/*.js",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"prepare": "husky",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.svrjs.org/svrjs/svrouter"
|
||||
},
|
||||
"keywords": [
|
||||
"routing",
|
||||
"router",
|
||||
"svrjs",
|
||||
"http"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.6.1",
|
||||
"@commitlint/config-conventional": "^19.6.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@swc/cli": "^0.5.2",
|
||||
"commitizen": "^4.3.1",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jest": "^28.10.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.3.0",
|
||||
"prettier": "^3.4.2",
|
||||
"rimraf": "^5.0.10"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"dist/"
|
||||
],
|
||||
"dependencies": {
|
||||
"path-to-regexp": "^8.2.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
15
prettier.config.js
Normal file
15
prettier.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
// prettier.config.js, .prettierrc.js, prettier.config.cjs, or .prettierrc.cjs
|
||||
|
||||
/**
|
||||
* @see https://prettier.io/docs/en/configuration.html
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
trailingComma: "none",
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
endOfLine: "lf"
|
||||
};
|
||||
|
||||
module.exports = config;
|
105
src/index.js
Normal file
105
src/index.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
const { match } = require("path-to-regexp");
|
||||
const http = require("http");
|
||||
|
||||
function svrouter() {
|
||||
const routes = [];
|
||||
|
||||
const addRoute = (method, path, callback) => {
|
||||
if (typeof method !== "string") {
|
||||
throw new Error("The HTTP method must be a string.");
|
||||
} else if (typeof path !== "string") {
|
||||
throw new Error("The route path must be a string.");
|
||||
} else if (typeof callback !== "function") {
|
||||
throw new Error("The route callback must be a function.");
|
||||
}
|
||||
|
||||
routes.push({
|
||||
method: method.toUpperCase(),
|
||||
pathFunction: match(path),
|
||||
callback: callback
|
||||
});
|
||||
};
|
||||
|
||||
const passRoute = (path, callback) => {
|
||||
const realCallback = callback ? callback : path;
|
||||
if (typeof realCallback !== "function") {
|
||||
throw new Error("The passed callback must be a function.");
|
||||
} else if (callback && typeof path !== "string") {
|
||||
throw new Error("The path must be a function");
|
||||
}
|
||||
|
||||
routes.push({
|
||||
method: null,
|
||||
pathFunction: callback
|
||||
? (checkedPath) =>
|
||||
checkedPath == path ||
|
||||
checkedPath.substring(0, path.length + 1) == path + "/"
|
||||
? {
|
||||
path: checkedPath,
|
||||
params: null
|
||||
}
|
||||
: false
|
||||
: () => true,
|
||||
callback: realCallback
|
||||
});
|
||||
};
|
||||
|
||||
const router = (req, res, logFacilities, config, next) => {
|
||||
let index = 0;
|
||||
let previousReqParams = req.params;
|
||||
let paramsPresent = false;
|
||||
|
||||
const nextRoute = () => {
|
||||
if (paramsPresent) req.params = previousReqParams;
|
||||
let currentRoute = routes[index++];
|
||||
let currentMatch =
|
||||
currentRoute && currentRoute.pathFunction
|
||||
? currentRoute.pathFunction(req.parsedURL.pathname)
|
||||
: false;
|
||||
while (
|
||||
currentRoute &&
|
||||
((currentRoute.method && req.method != currentRoute.method) ||
|
||||
!currentMatch)
|
||||
) {
|
||||
currentRoute = routes[index++];
|
||||
currentMatch =
|
||||
currentRoute && currentRoute.pathFunction
|
||||
? currentRoute.pathFunction(req.parsedURL.pathname)
|
||||
: false;
|
||||
}
|
||||
if (currentRoute && currentRoute.callback) {
|
||||
try {
|
||||
paramsPresent = Boolean(currentMatch.params);
|
||||
if (paramsPresent)
|
||||
req.params =
|
||||
currentMatch && currentMatch.params
|
||||
? currentMatch.params
|
||||
: Object.create(null);
|
||||
currentRoute.callback(req, res, logFacilities, config, nextRoute);
|
||||
} catch (err) {
|
||||
res.error(500, err);
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
nextRoute();
|
||||
};
|
||||
|
||||
const methods = http.METHODS
|
||||
? http.METHODS
|
||||
: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"];
|
||||
|
||||
methods.forEach((method) => {
|
||||
router[method.toLowerCase()] = (path, callback) =>
|
||||
addRoute(method, path, callback);
|
||||
});
|
||||
|
||||
router.route = addRoute;
|
||||
router.pass = passRoute;
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports = svrouter;
|
168
tests/index.test.js
Normal file
168
tests/index.test.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
const svrouter = require("../src/index.js");
|
||||
|
||||
describe("SVRouter", () => {
|
||||
let router;
|
||||
|
||||
beforeEach(() => {
|
||||
router = svrouter();
|
||||
});
|
||||
|
||||
test("should add and handle a GET route", () => {
|
||||
const req = {
|
||||
method: "GET",
|
||||
parsedURL: { pathname: "/test" },
|
||||
params: null
|
||||
};
|
||||
const res = {
|
||||
end: jest.fn()
|
||||
};
|
||||
|
||||
router.get("/test", (req, res) => {
|
||||
res.end("GET route matched");
|
||||
});
|
||||
|
||||
router(req, res, null, null, () => {
|
||||
res.end("No route matched");
|
||||
});
|
||||
|
||||
expect(res.end).toHaveBeenCalledWith("GET route matched");
|
||||
});
|
||||
|
||||
test("should add and handle a POST route", () => {
|
||||
const req = {
|
||||
method: "POST",
|
||||
parsedURL: { pathname: "/submit" },
|
||||
params: null
|
||||
};
|
||||
const res = {
|
||||
end: jest.fn()
|
||||
};
|
||||
|
||||
router.post("/submit", (req, res) => {
|
||||
res.end("POST route matched");
|
||||
});
|
||||
|
||||
router(req, res, null, null, () => {
|
||||
res.end("No route matched");
|
||||
});
|
||||
|
||||
expect(res.end).toHaveBeenCalledWith("POST route matched");
|
||||
});
|
||||
|
||||
test("should correctly parse parameters in the route", () => {
|
||||
const req = {
|
||||
method: "GET",
|
||||
parsedURL: { pathname: "/user/42" },
|
||||
params: null
|
||||
};
|
||||
const res = {
|
||||
end: jest.fn()
|
||||
};
|
||||
|
||||
router.get("/user/:id", (req, res) => {
|
||||
res.end(`User ID is ${req.params.id}`);
|
||||
});
|
||||
|
||||
router(req, res, null, null, () => {
|
||||
res.end("No route matched");
|
||||
});
|
||||
|
||||
expect(res.end).toHaveBeenCalledWith("User ID is 42");
|
||||
});
|
||||
|
||||
test("should pass to next middleware when no route matches", () => {
|
||||
const req = {
|
||||
method: "GET",
|
||||
parsedURL: { pathname: "/nonexistent" },
|
||||
params: null
|
||||
};
|
||||
const res = {
|
||||
end: jest.fn()
|
||||
};
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
router(req, res, null, null, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should add a route using router.route", () => {
|
||||
const req = {
|
||||
method: "PUT",
|
||||
parsedURL: { pathname: "/update" },
|
||||
params: null
|
||||
};
|
||||
const res = {
|
||||
end: jest.fn()
|
||||
};
|
||||
|
||||
router.route("PUT", "/update", (req, res) => {
|
||||
res.end("PUT route matched");
|
||||
});
|
||||
|
||||
router(req, res, null, null, () => {
|
||||
res.end("No route matched");
|
||||
});
|
||||
|
||||
expect(res.end).toHaveBeenCalledWith("PUT route matched");
|
||||
});
|
||||
|
||||
test("should add a pass-through route with router.pass", () => {
|
||||
const req = {
|
||||
method: "DELETE",
|
||||
parsedURL: { pathname: "/anything" },
|
||||
params: null
|
||||
};
|
||||
const res = {
|
||||
end: jest.fn()
|
||||
};
|
||||
|
||||
router.pass((req, res) => {
|
||||
res.end("Pass-through matched");
|
||||
});
|
||||
|
||||
router(req, res, null, null, () => {
|
||||
res.end("No route matched");
|
||||
});
|
||||
|
||||
expect(res.end).toHaveBeenCalledWith("Pass-through matched");
|
||||
});
|
||||
|
||||
test("should throw an error if method is not a string in route", () => {
|
||||
expect(() => {
|
||||
router.route(123, "/path", () => {});
|
||||
}).toThrow("The HTTP method must be a string.");
|
||||
});
|
||||
|
||||
test("should throw an error if callback is not a function in route", () => {
|
||||
expect(() => {
|
||||
router.route("GET", "/path", "not a function");
|
||||
}).toThrow("The route callback must be a function.");
|
||||
});
|
||||
|
||||
test("should throw an error if path is not a string in passRoute", () => {
|
||||
expect(() => {
|
||||
router.pass(123, () => {});
|
||||
}).toThrow("The path must be a function");
|
||||
});
|
||||
|
||||
test("should handle errors thrown in route callbacks gracefully", () => {
|
||||
const req = {
|
||||
method: "GET",
|
||||
parsedURL: { pathname: "/error" },
|
||||
params: null
|
||||
};
|
||||
const res = {
|
||||
error: jest.fn()
|
||||
};
|
||||
|
||||
router.get("/error", () => {
|
||||
throw new Error("Test error");
|
||||
});
|
||||
|
||||
router(req, res, null, null, () => {});
|
||||
|
||||
expect(res.error).toHaveBeenCalledWith(500, expect.any(Error));
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue