├── AGENTS.md ├── .github ├── CODE_OF_CONDUCT.md └── workflows │ └── nodejs.yml ├── .vscode └── launch.json ├── LICENSE ├── package.json ├── .gitignore ├── test ├── routegroup.test.js ├── router.test.js ├── integration.test.js └── tree.test.js ├── routegroup.js ├── router.js ├── readme.md ├── index.d.ts ├── tree.js └── pnpm-lock.yaml /AGENTS.md: -------------------------------------------------------------------------------- 1 | A high performance router for Koa framework. 2 | 3 | ## Setup commands 4 | 5 | - Install deps: `pnpm install` 6 | - Run tests `node --test` 7 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct/](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [20.x, 24.x] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: install dependencies 18 | run: yarn 19 | env: 20 | CI: true 21 | - name: run tests 22 | run: node --test 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "-u", 14 | "tdd", 15 | "--timeout", 16 | "999999", 17 | "--colors", 18 | "${workspaceFolder}/test" 19 | ], 20 | "internalConsoleOptions": "openOnSessionStart" 21 | }, 22 | { 23 | "type": "node", 24 | "request": "launch", 25 | "name": "Launch Program", 26 | "program": "${workspaceFolder}/router.js" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - 2023 Weilin Shi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-tree-router", 3 | "version": "0.13.1", 4 | "description": "A high performance koa router", 5 | "main": "router.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "node --test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/steambap/koa-tree-router.git" 13 | }, 14 | "engines": { 15 | "node": ">=20.0" 16 | }, 17 | "files": [ 18 | "*.js", 19 | "index.d.ts" 20 | ], 21 | "keywords": [ 22 | "koa", 23 | "router", 24 | "middleware", 25 | "fast", 26 | "radix", 27 | "tree" 28 | ], 29 | "author": "Weilin Shi <934587911@qq.com>", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/steambap/koa-tree-router/issues" 33 | }, 34 | "homepage": "https://github.com/steambap/koa-tree-router#readme", 35 | "typings": "index.d.ts", 36 | "devDependencies": { 37 | "koa": "^3.0.1", 38 | "supertest": "^7.1.4" 39 | }, 40 | "dependencies": { 41 | "@types/koa": "^3.0.0", 42 | "koa-compose": "^4.1.0" 43 | }, 44 | "pnpm": { 45 | "overrides": { 46 | "formidable@<3.2.4": ">=3.2.4" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Only apps need lock file 61 | yarn.lock 62 | -------------------------------------------------------------------------------- /test/routegroup.test.js: -------------------------------------------------------------------------------- 1 | import { test, describe } from "node:test"; 2 | import assert, { strictEqual } from "node:assert"; 3 | import Router from "../router.js"; 4 | import RouteGroup from "../routegroup.js"; 5 | 6 | const noOp = function () {}; 7 | 8 | describe("Route Group", () => { 9 | test("works!", () => { 10 | const r = new Router(); 11 | const group = new RouteGroup(r, "/foo"); 12 | group.get("/bar", noOp); 13 | assert(r.find("GET", "/foo/bar").handle); 14 | }); 15 | 16 | test("works in router", () => { 17 | const r = new Router(); 18 | const group = r.newGroup("/bar"); 19 | group.post("/", noOp); 20 | group.get("/foo", noOp); 21 | assert(r.find("POST", "/bar").handle); 22 | assert(r.find("GET", "/bar/foo").handle); 23 | }); 24 | 25 | test("uses middleware from `use` in `on`", () => { 26 | const r = new Router(); 27 | const group = new RouteGroup(r, "/foo"); 28 | group.use(noOp); 29 | group.get("/bar", noOp); 30 | assert(r.find("GET", "/foo/bar").handle); 31 | strictEqual(r.find("GET", "/foo/bar").handle.length, 2); 32 | }); 33 | 34 | test("works with multiple handle", () => { 35 | const r = new Router(); 36 | const group = new RouteGroup(r, "/foo"); 37 | group.get("/bar", noOp, noOp, noOp); 38 | strictEqual(r.find("GET", "/foo/bar").handle.length, 3); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /routegroup.js: -------------------------------------------------------------------------------- 1 | import { METHODS } from "node:http"; 2 | 3 | class RouteGroup { 4 | /** 5 | * 6 | * @param {*} router 7 | * @param {string} path 8 | */ 9 | constructor(router, path, handlers = []) { 10 | if (path[0] !== "/") { 11 | throw new Error("path must begin with '/' in path '" + path + "'"); 12 | } 13 | //Strip trailing / (if present) as all added sub paths must start with a / 14 | if (path[path.length - 1] === "/") { 15 | path = path.substr(0, path.length - 1); 16 | } 17 | this.handlers = [...handlers]; 18 | this.r = router; 19 | this.p = path; 20 | } 21 | /** 22 | * @param {string} path 23 | */ 24 | subpath(path) { 25 | if (path[0] !== "/") { 26 | throw new Error("path must start with a '/'"); 27 | } 28 | if (path === "/") { 29 | return this.p; 30 | } 31 | return this.p + path; 32 | } 33 | /** 34 | * 35 | * @param {string} path 36 | */ 37 | newGroup(path) { 38 | return new RouteGroup(this.r, this.subpath(path), this.handlers); 39 | } 40 | on(method, path, ...handle) { 41 | handle.unshift(...this.handlers); 42 | this.r.on(method, this.subpath(path), ...handle); 43 | return this; 44 | } 45 | get(...arg) { 46 | return this.on("GET", ...arg); 47 | } 48 | put(...arg) { 49 | return this.on("PUT", ...arg); 50 | } 51 | post(...arg) { 52 | return this.on("POST", ...arg); 53 | } 54 | delete(...arg) { 55 | return this.on("DELETE", ...arg); 56 | } 57 | head(...arg) { 58 | return this.on("HEAD", ...arg); 59 | } 60 | patch(...arg) { 61 | return this.on("PATCH", ...arg); 62 | } 63 | options(...arg) { 64 | return this.on("OPTIONS", ...arg); 65 | } 66 | trace(...arg) { 67 | return this.on("TRACE", ...arg); 68 | } 69 | connect(...arg) { 70 | return this.on("CONNECT", ...arg); 71 | } 72 | all(...arg) { 73 | METHODS.forEach((method) => { 74 | this.on(method, ...arg); 75 | }); 76 | return this; 77 | } 78 | use(...handle) { 79 | this.handlers.push(...handle); 80 | } 81 | routes() { 82 | return this.r.routes(); 83 | } 84 | } 85 | 86 | export default RouteGroup; 87 | -------------------------------------------------------------------------------- /test/router.test.js: -------------------------------------------------------------------------------- 1 | import { test, describe } from "node:test"; 2 | import assert, { throws, strictEqual } from "node:assert"; 3 | import Router from "../router.js"; 4 | 5 | const noOp = function() {}; 6 | 7 | describe("Router", () => { 8 | test("works!", () => { 9 | assert(new Router() instanceof Router); 10 | }); 11 | 12 | test("throws with invalid input", () => { 13 | const router = new Router(); 14 | throws(() => router.on("GET", "invalid", noOp)); 15 | }); 16 | 17 | test("support `get`", () => { 18 | const router = new Router(); 19 | router.get("/", noOp); 20 | assert(router.find("GET", "/").handle); 21 | }); 22 | 23 | test("support `post`", () => { 24 | const router = new Router(); 25 | router.post("/", noOp); 26 | assert(router.find("POST", "/").handle); 27 | }); 28 | 29 | test("support `put`", () => { 30 | const router = new Router(); 31 | router.put("/", noOp); 32 | assert(router.find("PUT", "/").handle); 33 | }); 34 | 35 | test("support `delete`", () => { 36 | const router = new Router(); 37 | router.delete("/", noOp); 38 | assert(router.find("DELETE", "/").handle); 39 | }); 40 | 41 | test("support `head`", () => { 42 | const router = new Router(); 43 | router.head("/", noOp); 44 | assert(router.find("HEAD", "/").handle); 45 | }); 46 | 47 | test("support `patch`", () => { 48 | const router = new Router(); 49 | router.patch("/", noOp); 50 | assert(router.find("PATCH", "/").handle); 51 | }); 52 | 53 | test("support `options`", () => { 54 | const router = new Router(); 55 | router.options("/", noOp); 56 | assert(router.find("OPTIONS", "/").handle); 57 | }); 58 | 59 | test("support `trace`", () => { 60 | const router = new Router(); 61 | router.trace("/", noOp); 62 | assert(router.find("TRACE", "/").handle); 63 | }); 64 | 65 | test("support `connect`", () => { 66 | const router = new Router(); 67 | router.connect("/", noOp); 68 | assert(router.find("CONNECT", "/").handle); 69 | }); 70 | 71 | test("support wildcard `all`", () => { 72 | const router = new Router(); 73 | router.all("/", noOp); 74 | assert(router.find("DELETE", "/").handle); 75 | assert(router.find("GET", "/").handle); 76 | assert(router.find("HEAD", "/").handle); 77 | assert(router.find("PATCH", "/").handle); 78 | assert(router.find("POST", "/").handle); 79 | assert(router.find("PUT", "/").handle); 80 | assert(router.find("OPTIONS", "/").handle); 81 | assert(router.find("TRACE", "/").handle); 82 | assert(router.find("CONNECT", "/").handle); 83 | }); 84 | 85 | test("uses middleware from `use` in `on`", () => { 86 | const router = new Router(); 87 | router.use(noOp); 88 | router.on("GET", "/", noOp); 89 | assert(router.find("GET", "/").handle); 90 | strictEqual(router.find("GET", "/").handle.length, 2); 91 | }); 92 | 93 | test("works with multiple handle", () => { 94 | const router = new Router(); 95 | router.get("/", noOp, noOp, noOp); 96 | strictEqual(router.find("GET", "/").handle.length, 3); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | import { test, describe } from "node:test"; 2 | import { strictEqual, deepStrictEqual } from "node:assert"; 3 | import Koa from "koa"; 4 | import request from "supertest"; 5 | import Router from "../router.js"; 6 | 7 | describe("Router", () => { 8 | test("should work", (t, done) => { 9 | const app = new Koa(); 10 | const router = new Router(); 11 | router.get("/", function (ctx) { 12 | ctx.body = "ok"; 13 | }); 14 | 15 | app.use(router.routes()); 16 | 17 | request(app.callback()).get("/").expect(200, done); 18 | }); 19 | 20 | test("support multiple middleware", (t, done) => { 21 | const app = new Koa(); 22 | const router = new Router(); 23 | router.get( 24 | "/", 25 | function (ctx, next) { 26 | ctx.body = 1; 27 | next(); 28 | }, 29 | function (ctx) { 30 | ctx.body += 1; 31 | } 32 | ); 33 | 34 | app.use(router.routes()); 35 | 36 | request(app.callback()) 37 | .get("/") 38 | .expect(200) 39 | .end(function (err, res) { 40 | strictEqual(res.body, 2); 41 | done(err); 42 | }); 43 | }); 44 | 45 | test("support 405 method not allowed", (t, done) => { 46 | const resBody = { msg: "not allowed" }; 47 | const app = new Koa(); 48 | const router = new Router({ 49 | onMethodNotAllowed(ctx) { 50 | ctx.body = resBody; 51 | }, 52 | }); 53 | router.get("/", function (ctx) { 54 | ctx.body = "ok"; 55 | }); 56 | 57 | app.use(router.routes()); 58 | 59 | request(app.callback()) 60 | .post("/") 61 | .expect(405) 62 | .end(function (err, res) { 63 | deepStrictEqual(res.body, resBody); 64 | done(err); 65 | }); 66 | }); 67 | 68 | test("respond with 405 and correct header", (t, done) => { 69 | const app = new Koa(); 70 | const router = new Router({ 71 | onMethodNotAllowed(ctx) { 72 | ctx.body = {}; 73 | }, 74 | }); 75 | 76 | router.get("/users", function () {}); 77 | router.put("/users", function () {}); 78 | 79 | app.use(router.routes()); 80 | 81 | request(app.callback()) 82 | .post("/users") 83 | .expect(405) 84 | .end(function (err, res) { 85 | strictEqual(res.header.allow, "GET, PUT"); 86 | done(err); 87 | }); 88 | }); 89 | 90 | test("handle #", (t, done) => { 91 | const app = new Koa(); 92 | const router = new Router(); 93 | router.get("/test", function (ctx) { 94 | ctx.body = "ok"; 95 | }); 96 | 97 | app.use(router.routes()); 98 | 99 | request(app.callback()).get("/test#id").expect(200, done); 100 | }); 101 | 102 | test("handle ?", (t, done) => { 103 | const app = new Koa(); 104 | const router = new Router(); 105 | router.get("/test", function (ctx) { 106 | ctx.body = "ok"; 107 | }); 108 | 109 | app.use(router.routes()); 110 | 111 | request(app.callback()).get("/test?id=test").expect(200, done); 112 | }); 113 | 114 | test("ignore trailing slash", (t, done) => { 115 | const app = new Koa(); 116 | const router = new Router({ 117 | ignoreTrailingSlash: true, 118 | }); 119 | router.get("/test", function (ctx) { 120 | ctx.body = "ok"; 121 | }); 122 | 123 | app.use(router.routes()); 124 | 125 | request(app.callback()).get("/test/").expect(200, done); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | import { METHODS } from "node:http"; 2 | import compose from "koa-compose"; 3 | import Node from "./tree.js"; 4 | import RouteGroup from "./routegroup.js"; 5 | 6 | const NOT_FOUND = { handle: null, params: [] }; 7 | 8 | function trimLastSlash(path) { 9 | if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) { 10 | return path.slice(0, -1); 11 | } 12 | return path; 13 | } 14 | 15 | class Router { 16 | constructor(opts = {}) { 17 | if (!(this instanceof Router)) { 18 | return new Router(opts); 19 | } 20 | this.handlers = []; 21 | this.trees = {}; 22 | this.opts = opts; 23 | } 24 | on(method, path, ...handle) { 25 | if (path[0] !== "/") { 26 | throw new Error("path must begin with '/' in path"); 27 | } 28 | handle.unshift(...this.handlers); 29 | if (!this.trees[method]) { 30 | this.trees[method] = new Node(); 31 | } 32 | this.trees[method].addRoute(path, handle); 33 | return this; 34 | } 35 | get(...arg) { 36 | return this.on("GET", ...arg); 37 | } 38 | put(...arg) { 39 | return this.on("PUT", ...arg); 40 | } 41 | post(...arg) { 42 | return this.on("POST", ...arg); 43 | } 44 | delete(...arg) { 45 | return this.on("DELETE", ...arg); 46 | } 47 | head(...arg) { 48 | return this.on("HEAD", ...arg); 49 | } 50 | patch(...arg) { 51 | return this.on("PATCH", ...arg); 52 | } 53 | options(...arg) { 54 | return this.on("OPTIONS", ...arg); 55 | } 56 | trace(...arg) { 57 | return this.on("TRACE", ...arg); 58 | } 59 | connect(...arg) { 60 | return this.on("CONNECT", ...arg); 61 | } 62 | all(...arg) { 63 | METHODS.forEach((method) => { 64 | this.on(method, ...arg); 65 | }); 66 | return this; 67 | } 68 | use(...handle) { 69 | this.handlers.push(...handle); 70 | } 71 | find(method, path) { 72 | const tree = this.trees[method]; 73 | if (tree) { 74 | if (this.opts.ignoreTrailingSlash) { 75 | path = trimLastSlash(path); 76 | } 77 | return tree.search(path); 78 | } 79 | return NOT_FOUND; 80 | } 81 | routes() { 82 | const router = this; 83 | const handle = function (ctx, next) { 84 | const { handle, params } = router.find(ctx.method, ctx.path); 85 | if (!handle) { 86 | const handle405 = router.opts.onMethodNotAllowed; 87 | if (handle405) { 88 | const allowList = []; 89 | // Search for allowed methods 90 | for (let key in router.trees) { 91 | if (key === ctx.method) { 92 | continue; 93 | } 94 | const tree = router.trees[key]; 95 | if (tree.search(ctx.path).handle !== null) { 96 | allowList.push(key); 97 | } 98 | } 99 | ctx.status = 405; 100 | ctx.set("Allow", allowList.join(", ")); 101 | return handle405(ctx, next); 102 | } 103 | return next(); 104 | } 105 | ctx.params = {}; 106 | params.forEach(({ key, value }) => { 107 | ctx.params[key] = value; 108 | }); 109 | return compose(handle)(ctx, next); 110 | }; 111 | return handle; 112 | } 113 | middleware() { 114 | return this.routes(); 115 | } 116 | /** 117 | * @param {string} path 118 | */ 119 | newGroup(path) { 120 | return new RouteGroup(this, path, this.handlers); 121 | } 122 | } 123 | 124 | export default Router; 125 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Koa tree router 2 | 3 | [![Build Status](https://github.com/steambap/koa-tree-router/workflows/CI/badge.svg)](https://github.com/steambap/koa-tree-router/actions?workflow=CI) 4 | [![npm](https://img.shields.io/npm/v/koa-tree-router.svg)](https://npm.im/koa-tree-router) 5 | ![npm downloads](https://img.shields.io/npm/dt/koa-tree-router.svg) 6 | 7 | Koa tree router is a high performance router for Koa. 8 | 9 | ## Features 10 | 11 | - Fast. Up to 11 times faster than Koa-router. [Benchmark](https://github.com/delvedor/router-benchmark) 12 | 13 | - Express-style routing using `router.get`, `router.put`, `router.post`, etc. 14 | 15 | - Support for `405 method not allowed` 16 | 17 | - Multiple middleware per route 18 | 19 | ## How does it work? 20 | 21 | The router relies on a tree structure which makes heavy use of *common prefixes*, it is basically a *compact* [*prefix tree*](https://en.wikipedia.org/wiki/Trie) (or just [*Radix tree*](https://en.wikipedia.org/wiki/Radix_tree)). 22 | 23 | This module's tree implementation is based on [julienschmidt/httprouter](https://github.com/julienschmidt/httprouter). 24 | 25 | ## Installation 26 | 27 | ```sh 28 | # npm 29 | npm i koa-tree-router 30 | # yarn 31 | yarn add koa-tree-router 32 | ``` 33 | 34 | ## Usage 35 | 36 | ```JS 37 | import Koa from "koa"; 38 | import Router from "koa-tree-router"; 39 | 40 | const app = new Koa(); 41 | const router = new Router(); 42 | 43 | router.get("/", (ctx) => { 44 | ctx.body = "hello, world"; 45 | }); 46 | 47 | app.use(router.routes()); 48 | 49 | app.listen(8080); 50 | ``` 51 | 52 | ## API 53 | 54 | #### Router([options]) 55 | Instance a new router. 56 | ```js 57 | import Router from "koa-tree-router"; 58 | 59 | const router = new Router({ 60 | onMethodNotAllowed(ctx) { 61 | ctx.body = "not allowed"; 62 | }, 63 | ignoreTrailingSlash: true 64 | }); 65 | ``` 66 | 67 | #### on(method, path, middleware) 68 | Register a new route. 69 | ```js 70 | router.on('GET', '/example', (ctx) => { 71 | // your code 72 | }) 73 | ``` 74 | 75 | #### Shorthand methods 76 | If you want to get expressive, here is what you can do: 77 | ```js 78 | router.get(path, middleware) 79 | router.delete(path, middleware) 80 | router.head(path, middleware) 81 | router.patch(path, middleware) 82 | router.post(path, middleware) 83 | router.put(path, middleware) 84 | router.options(path, middleware) 85 | router.trace(path, middleware) 86 | router.connect(path, middleware) 87 | ``` 88 | 89 | If you need a route that supports *all* methods you can use the `all` api. 90 | ```js 91 | router.all(path, middleware) 92 | ``` 93 | 94 | #### use(middleware) 95 | You can add middleware that is added to all future routes: 96 | ```js 97 | router.use(authMiddleware); 98 | router.get("/foo", (ctx) => { /* your code */ }); 99 | router.get("/bar", (ctx) => { /* your code */ }); 100 | router.get("/baz", (ctx) => { /* your code */ }); 101 | ``` 102 | 103 | This is equivalent to: 104 | ```js 105 | router.get("/foo", authMiddleware, (ctx) => { /* your code */ }); 106 | router.get("/bar", authMiddleware, (ctx) => { /* your code */ }); 107 | router.get("/baz", authMiddleware, (ctx) => { /* your code */ }); 108 | ``` 109 | **Caveat**: `use` must be called before register a new handler. It does not append handlers to registered routes. 110 | 111 | #### routes 112 | Returns router middleware. 113 | 114 | ```JS 115 | app.use(router.routes()); 116 | ``` 117 | 118 | #### nested routes 119 | A way to create groups of routes without incuring any per-request overhead. 120 | 121 | ```js 122 | import Koa from "koa"; 123 | import Router from "koa-tree-router"; 124 | 125 | const app = new Koa(); 126 | const router = new Router(); 127 | const group = router.newGroup("/foo"); 128 | // add a handler for /foo/bar 129 | group.get("/bar", (ctx) => { 130 | ctx.body = "hello, world"; 131 | }); 132 | 133 | app.use(router.routes()); 134 | 135 | app.listen(8080); 136 | ``` 137 | 138 | Middleware added with `use()` are also added to the nested routes. 139 | 140 | #### ctx.params 141 | This object contains key-value pairs of named route parameters. 142 | 143 | ```JS 144 | router.get("/user/:name", function() { 145 | // your code 146 | }); 147 | // GET /user/1 148 | ctx.params.name 149 | // => "1" 150 | ``` 151 | 152 | ## How to write routes 153 | There are 3 types of routes: 154 | 155 | 1.Static 156 | ``` 157 | Pattern: /static 158 | 159 | /static match 160 | /anything-else no match 161 | ``` 162 | 163 | 2.Named 164 | 165 | Named parameters have the form `:name` and only match a single path segment: 166 | ``` 167 | Pattern: /user/:user 168 | 169 | /user/gordon match 170 | /user/you match 171 | /user/gordon/profile no match 172 | /user/ no match 173 | ``` 174 | 175 | 3.Catch-all 176 | 177 | Catch-all parameters have the form `*name` and match everything. They must always be at the **end** of the pattern: 178 | 179 | ``` 180 | Pattern: /src/*filepath 181 | 182 | /src/ match 183 | /src/somefile.go match 184 | /src/subdir/somefile.go match 185 | ``` 186 | 187 | ## Typescript Support 188 | This package has its own declaration files in NPM package, you don't have to do anything extra. 189 | 190 | ## License 191 | 192 | [MIT](LICENSE) 193 | -------------------------------------------------------------------------------- /test/tree.test.js: -------------------------------------------------------------------------------- 1 | import { test, describe } from "node:test"; 2 | import assert, { strictEqual, deepStrictEqual, throws } from "node:assert"; 3 | import Tree from "../tree.js"; 4 | 5 | Tree.prototype.printTree = function(prefix = "") { 6 | console.log( 7 | " %d %s%s[%d] %s %s %d \r\n", 8 | this.priority, 9 | prefix, 10 | this.path, 11 | this.children.length, 12 | this.handle, 13 | this.wildChild, 14 | this.type 15 | ); 16 | for (let l = this.path.length; l > 0; l--) { 17 | prefix += " "; 18 | } 19 | this.children.forEach(child => { 20 | child.printTree(prefix); 21 | }); 22 | }; 23 | 24 | const noOp = [function() {}]; 25 | 26 | describe("Add and get", () => { 27 | const tree = new Tree(); 28 | const routes = [ 29 | "/hi", 30 | "/contact", 31 | "/co", 32 | "/c", 33 | "/a", 34 | "/ab", 35 | "/doc/", 36 | "/doc/node_faq.html", 37 | "/doc/node1.html", 38 | "/α", 39 | "/β" 40 | ]; 41 | 42 | routes.forEach(route => { 43 | tree.addRoute(route, noOp); 44 | }); 45 | 46 | // tree.printTree(); 47 | 48 | const testData = [ 49 | { 50 | route: "/a", 51 | found: true 52 | }, 53 | { 54 | route: "/", 55 | found: false 56 | }, 57 | { 58 | route: "/hi", 59 | found: true 60 | }, 61 | { 62 | route: "/contact", 63 | found: true 64 | }, 65 | { 66 | route: "/co", 67 | found: true 68 | }, 69 | { 70 | route: "/con", 71 | found: false 72 | }, 73 | { 74 | route: "/cona", 75 | found: false 76 | }, 77 | { 78 | route: "/no", 79 | found: false 80 | }, 81 | { 82 | route: "/ab", 83 | found: true 84 | }, 85 | { 86 | route: "/α", 87 | found: true 88 | }, 89 | { 90 | route: "/β", 91 | found: true 92 | } 93 | ]; 94 | 95 | testData.forEach(data => { 96 | test(data.route, () => { 97 | const { handle } = tree.search(data.route); 98 | if (data.found) { 99 | assert(handle); 100 | } else { 101 | strictEqual(handle, null); 102 | } 103 | }); 104 | }); 105 | }); 106 | 107 | describe("Wildcard", () => { 108 | const tree = new Tree(); 109 | const routes = [ 110 | "/", 111 | "/cmd/:tool/:sub", 112 | "/cmd/:tool/", 113 | "/src/*filepath", 114 | "/search/", 115 | "/search/:query", 116 | "/user_:name", 117 | "/user_:name/about", 118 | "/files/:dir/*filepath", 119 | "/doc/", 120 | "/doc/node_faq.html", 121 | "/doc/node1.html", 122 | "/info/:user/public", 123 | "/info/:user/project/:project" 124 | ]; 125 | 126 | routes.forEach(route => { 127 | tree.addRoute(route, noOp); 128 | }); 129 | 130 | // tree.printTree(); 131 | 132 | const foundData = [ 133 | { 134 | route: "/", 135 | params: [] 136 | }, 137 | { 138 | route: "/cmd/test/", 139 | params: [{ key: "tool", value: "test" }] 140 | }, 141 | { 142 | route: "/cmd/test/3", 143 | params: [{ key: "tool", value: "test" }, { key: "sub", value: "3" }] 144 | }, 145 | { 146 | route: "/src/", 147 | params: [{ key: "filepath", value: "/" }] 148 | }, 149 | { 150 | route: "/src/some/file.png", 151 | params: [{ key: "filepath", value: "/some/file.png" }] 152 | }, 153 | { 154 | route: "/search/", 155 | params: [] 156 | }, 157 | { 158 | route: "/search/中文", 159 | params: [{ key: "query", value: "中文" }] 160 | }, 161 | { 162 | route: "/user_noder", 163 | params: [{ key: "name", value: "noder" }] 164 | }, 165 | { 166 | route: "/user_noder/about", 167 | params: [{ key: "name", value: "noder" }] 168 | }, 169 | { 170 | route: "/files/js/inc/framework.js", 171 | params: [ 172 | { key: "dir", value: "js" }, 173 | { key: "filepath", value: "/inc/framework.js" } 174 | ] 175 | }, 176 | { 177 | route: "/info/gordon/public", 178 | params: [{ key: "user", value: "gordon" }] 179 | }, 180 | { 181 | route: "/info/gordon/project/node", 182 | params: [ 183 | { key: "user", value: "gordon" }, 184 | { key: "project", value: "node" } 185 | ] 186 | } 187 | ]; 188 | 189 | foundData.forEach(data => { 190 | test(data.route, () => { 191 | const { handle, params } = tree.search(data.route); 192 | assert(handle); 193 | deepStrictEqual(params, data.params); 194 | }); 195 | }); 196 | 197 | const noHandlerData = [ 198 | { 199 | route: "/cmd/test", 200 | params: [{ key: "tool", value: "test" }] 201 | }, 202 | { 203 | route: "/search/中文/", 204 | params: [{ key: "query", value: "中文" }] 205 | } 206 | ]; 207 | 208 | noHandlerData.forEach(data => { 209 | test(data.route, () => { 210 | const { handle, params } = tree.search(data.route); 211 | strictEqual(handle, null); 212 | deepStrictEqual(params, data.params); 213 | }); 214 | }); 215 | }); 216 | 217 | describe("Invalid", () => { 218 | test("node type", () => { 219 | const tree = new Tree(); 220 | tree.addRoute("/", noOp); 221 | tree.addRoute("/:page", noOp); 222 | 223 | tree.children[0].type = 42; 224 | 225 | throws(() => tree.search("/test")); 226 | }); 227 | 228 | test("conflict", () => { 229 | const tree = new Tree(); 230 | tree.addRoute("/src3/*filepath", noOp); 231 | 232 | throws(() => tree.addRoute("/src3/*filepath/x", noOp)); 233 | }) 234 | }); 235 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from "koa"; 2 | 3 | declare namespace Router { 4 | export interface IRouterOptions { 5 | onMethodNotAllowed?: Router.Middleware; 6 | ignoreTrailingSlash?: Boolean; 7 | } 8 | export interface IRouterParamContext { 9 | /** 10 | * url params 11 | */ 12 | params: { [key: string]: string }; 13 | /** 14 | * the router instance 15 | */ 16 | router: Router; 17 | } 18 | export type RouterContext< 19 | StateT = any, 20 | CustomT = {} 21 | > = Koa.ParameterizedContext< 22 | StateT, 23 | CustomT & IRouterParamContext 24 | >; 25 | export interface IRouterContext extends RouterContext {} 26 | export type Middleware = Koa.Middleware< 27 | StateT, 28 | CustomT & IRouterParamContext 29 | >; 30 | export { RouteGroup }; 31 | } 32 | 33 | declare class Router { 34 | /** 35 | * Create a new router. 36 | */ 37 | constructor(opts?: Router.IRouterOptions); 38 | /** 39 | * Register a new route. 40 | */ 41 | on( 42 | method: string, 43 | path: string, 44 | ...middleware: Array> 45 | ): this; 46 | /** 47 | * HTTP GET method 48 | */ 49 | get( 50 | path: string, 51 | ...middleware: Array> 52 | ): this; 53 | /** 54 | * HTTP POST method 55 | */ 56 | post( 57 | path: string, 58 | ...middleware: Array> 59 | ): this; 60 | /** 61 | * HTTP PUT method 62 | */ 63 | put( 64 | path: string, 65 | ...middleware: Array> 66 | ): this; 67 | /** 68 | * HTTP DELETE method 69 | */ 70 | delete( 71 | path: string, 72 | ...middleware: Array> 73 | ): this; 74 | /** 75 | * HTTP HEAD method 76 | */ 77 | head( 78 | path: string, 79 | ...middleware: Array> 80 | ): this; 81 | /** 82 | * HTTP OPTIONS method 83 | */ 84 | options( 85 | path: string, 86 | ...middleware: Array> 87 | ): this; 88 | /** 89 | * HTTP PATCH method 90 | */ 91 | patch( 92 | path: string, 93 | ...middleware: Array> 94 | ): this; 95 | /** 96 | * HTTP TRACE method 97 | */ 98 | trace( 99 | path: string, 100 | ...middleware: Array> 101 | ): this; 102 | /** 103 | * HTTP CONNECT method 104 | */ 105 | connect( 106 | path: string, 107 | ...middleware: Array> 108 | ): this; 109 | /** 110 | * Prepend handlers to all future routes 111 | * Must be called before register new handlers 112 | */ 113 | use( ...middleware: Array>): void; 114 | /** 115 | * Register route with all methods. 116 | */ 117 | all( 118 | path: string, 119 | ...middleware: Array> 120 | ): this; 121 | /** 122 | * Returns router middleware. 123 | */ 124 | routes(): Router.Middleware; 125 | /** 126 | * Create groups of routes 127 | */ 128 | newGroup(path: string): RouteGroup; 129 | } 130 | 131 | declare class RouteGroup { 132 | /** 133 | * Create a new router. 134 | */ 135 | constructor(router: Router, path: string); 136 | /** 137 | * Register a new route. 138 | */ 139 | on( 140 | method: string, 141 | path: string, 142 | ...middleware: Array> 143 | ): this; 144 | /** 145 | * HTTP GET method 146 | */ 147 | get( 148 | path: string, 149 | ...middleware: Array> 150 | ): this; 151 | /** 152 | * HTTP POST method 153 | */ 154 | post( 155 | path: string, 156 | ...middleware: Array> 157 | ): this; 158 | /** 159 | * HTTP PUT method 160 | */ 161 | put( 162 | path: string, 163 | ...middleware: Array> 164 | ): this; 165 | /** 166 | * HTTP DELETE method 167 | */ 168 | delete( 169 | path: string, 170 | ...middleware: Array> 171 | ): this; 172 | /** 173 | * HTTP HEAD method 174 | */ 175 | head( 176 | path: string, 177 | ...middleware: Array> 178 | ): this; 179 | /** 180 | * HTTP OPTIONS method 181 | */ 182 | options( 183 | path: string, 184 | ...middleware: Array> 185 | ): this; 186 | /** 187 | * HTTP PATCH method 188 | */ 189 | patch( 190 | path: string, 191 | ...middleware: Array> 192 | ): this; 193 | /** 194 | * HTTP TRACE method 195 | */ 196 | trace( 197 | path: string, 198 | ...middleware: Array> 199 | ): this; 200 | /** 201 | * HTTP CONNECT method 202 | */ 203 | connect( 204 | path: string, 205 | ...middleware: Array> 206 | ): this; 207 | /** 208 | * Register route with all methods. 209 | */ 210 | all( 211 | path: string, 212 | ...middleware: Array> 213 | ): this; 214 | /** 215 | * Prepend handlers to all future routes in the group 216 | * Must be called before register new handlers 217 | */ 218 | use( ...middleware: Array>): void; 219 | /** 220 | * Returns router middleware. 221 | */ 222 | routes(): Router.Middleware; 223 | /** 224 | * Create groups of routes 225 | */ 226 | newGroup(path: string): RouteGroup; 227 | } 228 | 229 | export = Router; 230 | -------------------------------------------------------------------------------- /tree.js: -------------------------------------------------------------------------------- 1 | const STATIC = 0; 2 | const ROOT = 1; 3 | const PARAM = 2; 4 | const CATCH_ALL = 3; 5 | 6 | /** 7 | * Search for a wildcard segment and check the name for invalid characters. 8 | * Returns -1 as index, if no wildcard was found 9 | * @param {string} path 10 | */ 11 | function findWildcard(path) { 12 | for (let i = 0; i < path.length; i++) { 13 | const c = path[i]; 14 | if (c !== ":" && c !== "*") { 15 | continue; 16 | } 17 | 18 | let valid = true; 19 | const remaining = path.slice(i + 1); 20 | for (let end = 0; end < remaining.length; end++) { 21 | const char = remaining[end]; 22 | if (char === "/") { 23 | return { 24 | wildcard: path.slice(i, i + 1 + end), 25 | i, 26 | valid 27 | }; 28 | } 29 | if (char === ":" || char === "*") { 30 | valid = false; 31 | } 32 | } 33 | 34 | return { 35 | wildcard: path.slice(i), 36 | i, 37 | valid 38 | }; 39 | } 40 | 41 | return { 42 | wildcard: "", 43 | i: -1, 44 | valid: false 45 | }; 46 | } 47 | 48 | /** 49 | * @param {string} a 50 | * @param {string} b 51 | */ 52 | function longestCommonPrefix(a, b) { 53 | let i = 0; 54 | const max = Math.min(a.length, b.length); 55 | while (i < max && a[i] === b[i]) { 56 | i++; 57 | } 58 | 59 | return i; 60 | } 61 | 62 | class Node { 63 | /** 64 | * 65 | * @param {string} path 66 | * @param {boolean} wildChild 67 | * @param {number} type 68 | * @param {string} indices 69 | * @param {Node[]} children 70 | * @param {function[]=} handle 71 | * @param {number} priority 72 | */ 73 | constructor( 74 | path = "", 75 | wildChild = false, 76 | type = STATIC, 77 | indices = "", 78 | children = [], 79 | handle = null, 80 | priority = 0 81 | ) { 82 | this.path = path; 83 | this.wildChild = wildChild; 84 | this.type = type; 85 | this.indices = indices; 86 | this.children = children; 87 | this.handle = handle; 88 | this.priority = priority; 89 | } 90 | /** 91 | * 92 | * @param {number} pos 93 | */ 94 | addPriority(pos) { 95 | const children = this.children; 96 | children[pos].priority++; 97 | const prio = children[pos].priority; 98 | 99 | // Adjust position (move to fron) 100 | let newPos = pos; 101 | while (newPos > 0 && children[newPos - 1].priority < prio) { 102 | const temp = children[newPos]; 103 | children[newPos] = children[newPos - 1]; 104 | children[newPos - 1] = temp; 105 | newPos--; 106 | } 107 | 108 | // Build new index char string 109 | if (newPos !== pos) { 110 | this.indices = 111 | this.indices.slice(0, newPos) + 112 | this.indices[pos] + 113 | this.indices.slice(newPos, pos) + 114 | this.indices.slice(pos + 1); 115 | } 116 | 117 | return newPos; 118 | } 119 | /** 120 | * Adds a node with the given handle to the path 121 | * @param {string} path 122 | * @param {function[]} handle 123 | */ 124 | addRoute(path, handle) { 125 | let n = this; 126 | let fullPath = path; 127 | n.priority++; 128 | 129 | if (n.path.length === 0 && n.children.length === 0) { 130 | n.insertChild(path, fullPath, handle); 131 | n.type = ROOT; 132 | return; 133 | } 134 | 135 | walk: while (true) { 136 | // Find the longest common prefix 137 | // This also implies that the common prefix contains no ':' or '*' 138 | // since the existing key can't contain those chars. 139 | let i = longestCommonPrefix(path, n.path); 140 | 141 | // Split edge 142 | if (i < n.path.length) { 143 | const child = new Node( 144 | n.path.slice(i), 145 | n.wildChild, 146 | STATIC, 147 | n.indices, 148 | n.children, 149 | n.handle, 150 | n.priority - 1 151 | ); 152 | 153 | n.children = [child]; 154 | n.indices = n.path[i]; 155 | n.path = path.slice(0, i); 156 | n.handle = null; 157 | n.wildChild = false; 158 | } 159 | 160 | // Make new node a child of this node 161 | if (i < path.length) { 162 | path = path.slice(i); 163 | 164 | if (n.wildChild) { 165 | n = n.children[0]; 166 | n.priority++; 167 | 168 | // Check if the wildcard matches 169 | if ( 170 | path.length >= n.path.length && 171 | n.path === path.slice(0, n.path.length) && 172 | // Adding a child to a catchAll is not possible 173 | n.type !== CATCH_ALL && 174 | (n.path.length >= path.length || path[n.path.length] === "/") 175 | ) { 176 | continue walk; 177 | } else { 178 | // Wildcard conflict 179 | let pathSeg = path; 180 | if (n.type !== CATCH_ALL) { 181 | pathSeg = path.split("/")[0]; 182 | } 183 | const prefix = 184 | fullPath.slice(0, fullPath.indexOf(pathSeg)) + n.path; 185 | throw new Error( 186 | `'${pathSeg}' in new path '${fullPath}' conflicts with existing wildcard '${n.path}' in existing prefix '${prefix}'` 187 | ); 188 | } 189 | } 190 | 191 | const c = path[0]; 192 | 193 | // Slash after param 194 | if (n.type === PARAM && c === "/" && n.children.length === 1) { 195 | n = n.children[0]; 196 | n.priority++; 197 | continue walk; 198 | } 199 | 200 | // Check if a child with the next path char exists 201 | for (let j = 0; j < n.indices.length; j++) { 202 | if (c === n.indices[j]) { 203 | j = n.addPriority(j); 204 | n = n.children[j]; 205 | continue walk; 206 | } 207 | } 208 | 209 | // Otherwise insert it 210 | if (c !== ":" && c !== "*") { 211 | n.indices += c; 212 | const child = new Node("", false, STATIC); 213 | n.children.push(child); 214 | n.addPriority(n.indices.length - 1); 215 | n = child; 216 | } 217 | n.insertChild(path, fullPath, handle); 218 | return; 219 | } 220 | 221 | if (n.handle !== null) { 222 | throw new Error( 223 | "A handle is already registered for path '" + fullPath + "'" 224 | ); 225 | } 226 | n.handle = handle; 227 | return; 228 | } 229 | } 230 | /** 231 | * 232 | * @param {string} path 233 | * @param {string} fullPath 234 | * @param {function[]} handle 235 | */ 236 | insertChild(path, fullPath, handle) { 237 | let n = this; 238 | 239 | while (true) { 240 | // Find prefix until first wildcard 241 | let { wildcard, i, valid } = findWildcard(path); 242 | if (i < 0) { 243 | break; 244 | } 245 | 246 | if (!valid) { 247 | throw new Error( 248 | "only one wildcard per path segment is allowed, has: '" + 249 | wildcard + 250 | "' in path '" + 251 | fullPath + 252 | "'" 253 | ); 254 | } 255 | 256 | if (wildcard.length < 2) { 257 | throw new Error( 258 | "wildcards must be named with a non-empty name in path '" + 259 | fullPath + 260 | "'" 261 | ); 262 | } 263 | 264 | if (n.children.length > 0) { 265 | throw new Error( 266 | "wildcard route '" + 267 | wildcard + 268 | "' conflicts with existing children in path '" + 269 | fullPath + 270 | "'" 271 | ); 272 | } 273 | 274 | if (wildcard[0] === ":") { 275 | // param 276 | if (i > 0) { 277 | // Insert prefix before the current wildcard 278 | n.path = path.slice(0, i); 279 | path = path.slice(i); 280 | } 281 | 282 | n.wildChild = true; 283 | const child = new Node(wildcard, false, PARAM); 284 | n.children = [child]; 285 | n = child; 286 | n.priority++; 287 | 288 | if (wildcard.length < path.length) { 289 | path = path.slice(wildcard.length); 290 | 291 | const staticChild = new Node("", false, STATIC, "", [], null, 1); 292 | n.children = [staticChild]; 293 | n = staticChild; 294 | continue; 295 | } 296 | 297 | // Otherwise we're done. Insert the handle in the new leaf 298 | n.handle = handle; 299 | return; 300 | } else { 301 | // catchAll 302 | if (i + wildcard.length != path.length) { 303 | throw new Error( 304 | "catch-all routes are only allowed at the end of the path in path '" + 305 | fullPath + 306 | "'" 307 | ); 308 | } 309 | 310 | if (n.path.length > 0 && n.path[n.path.length - 1] === "/") { 311 | throw new Error( 312 | "catch-all conflicts with existing handle for the path segment root in path '" + 313 | fullPath + 314 | "'" 315 | ); 316 | } 317 | 318 | // Currently fixed width 1 for '/ 319 | i--; 320 | if (path[i] !== "/") { 321 | throw new Error("no / before catch-all in path '" + fullPath + "'"); 322 | } 323 | 324 | n.path = path.slice(0, i); 325 | 326 | // First node: catchAll node with empty path 327 | const catchAllChild = new Node("", true, CATCH_ALL); 328 | n.children = [catchAllChild]; 329 | n.indices = "/"; 330 | n = catchAllChild; 331 | n.priority++; 332 | 333 | // Second node: node holding the variable 334 | const child = new Node( 335 | path.slice(i), 336 | false, 337 | CATCH_ALL, 338 | "", 339 | [], 340 | handle, 341 | 1 342 | ); 343 | n.children = [child]; 344 | 345 | return; 346 | } 347 | } 348 | 349 | // Insert remaining path part and handle to the leaf 350 | n.path = path; 351 | n.handle = handle; 352 | } 353 | /** 354 | * 355 | * @param {string} path 356 | */ 357 | search(path) { 358 | let handle = null; 359 | const params = []; 360 | let n = this; 361 | 362 | walk: while (true) { 363 | if (path.length > n.path.length) { 364 | if (path.slice(0, n.path.length) === n.path) { 365 | path = path.slice(n.path.length); 366 | // If this node does not have a wildcard child, 367 | // we can just look up the next child node and continue 368 | // to walk down the tree 369 | if (!n.wildChild) { 370 | const c = path.charCodeAt(0); 371 | for (let i = 0; i < n.indices.length; i++) { 372 | if (c === n.indices.charCodeAt(i)) { 373 | n = n.children[i]; 374 | continue walk; 375 | } 376 | } 377 | 378 | // Nothing found. 379 | return { handle, params }; 380 | } 381 | 382 | // Handle wildcard child 383 | n = n.children[0]; 384 | switch (n.type) { 385 | case PARAM: 386 | // Find param end 387 | let end = 0; 388 | while (end < path.length && path.charCodeAt(end) !== 47) { 389 | end++; 390 | } 391 | 392 | // Save param value 393 | params.push({ key: n.path.slice(1), value: path.slice(0, end) }); 394 | 395 | // We need to go deeper! 396 | if (end < path.length) { 397 | if (n.children.length > 0) { 398 | path = path.slice(end); 399 | n = n.children[0]; 400 | continue walk; 401 | } 402 | 403 | // ... but we can't 404 | return { handle, params }; 405 | } 406 | 407 | handle = n.handle; 408 | 409 | return { handle, params }; 410 | 411 | case CATCH_ALL: 412 | params.push({ key: n.path.slice(2), value: path }); 413 | 414 | handle = n.handle; 415 | return { handle, params }; 416 | 417 | default: 418 | throw new Error("invalid node type"); 419 | } 420 | } 421 | } else if (path === n.path) { 422 | handle = n.handle; 423 | } 424 | 425 | return { handle, params }; 426 | } 427 | } 428 | } 429 | 430 | export default Node; 431 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | overrides: 8 | formidable@<3.2.4: '>=3.2.4' 9 | 10 | importers: 11 | 12 | .: 13 | dependencies: 14 | '@types/koa': 15 | specifier: ^3.0.0 16 | version: 3.0.0 17 | koa-compose: 18 | specifier: ^4.1.0 19 | version: 4.1.0 20 | devDependencies: 21 | koa: 22 | specifier: ^3.0.1 23 | version: 3.0.1 24 | supertest: 25 | specifier: ^7.1.4 26 | version: 7.1.4 27 | 28 | packages: 29 | 30 | '@noble/hashes@1.8.0': 31 | resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} 32 | engines: {node: ^14.21.3 || >=16} 33 | 34 | '@paralleldrive/cuid2@2.2.2': 35 | resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} 36 | 37 | '@types/accepts@1.3.7': 38 | resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} 39 | 40 | '@types/body-parser@1.19.6': 41 | resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} 42 | 43 | '@types/connect@3.4.38': 44 | resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} 45 | 46 | '@types/content-disposition@0.5.9': 47 | resolution: {integrity: sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==} 48 | 49 | '@types/cookies@0.9.1': 50 | resolution: {integrity: sha512-E/DPgzifH4sM1UMadJMWd6mO2jOd4g1Ejwzx8/uRCDpJis1IrlyQEcGAYEomtAqRYmD5ORbNXMeI9U0RiVGZbg==} 51 | 52 | '@types/express-serve-static-core@5.0.7': 53 | resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==} 54 | 55 | '@types/express@5.0.3': 56 | resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} 57 | 58 | '@types/http-assert@1.5.6': 59 | resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==} 60 | 61 | '@types/http-errors@2.0.5': 62 | resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} 63 | 64 | '@types/keygrip@1.0.6': 65 | resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} 66 | 67 | '@types/koa-compose@3.2.8': 68 | resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==} 69 | 70 | '@types/koa@3.0.0': 71 | resolution: {integrity: sha512-MOcVYdVYmkSutVHZZPh8j3+dAjLyR5Tl59CN0eKgpkE1h/LBSmPAsQQuWs+bKu7WtGNn+hKfJH9Gzml+PulmDg==} 72 | 73 | '@types/mime@1.3.5': 74 | resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} 75 | 76 | '@types/node@24.1.0': 77 | resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==} 78 | 79 | '@types/qs@6.14.0': 80 | resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} 81 | 82 | '@types/range-parser@1.2.7': 83 | resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} 84 | 85 | '@types/send@0.17.5': 86 | resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} 87 | 88 | '@types/serve-static@1.15.8': 89 | resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} 90 | 91 | accepts@1.3.8: 92 | resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} 93 | engines: {node: '>= 0.6'} 94 | 95 | asap@2.0.6: 96 | resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} 97 | 98 | asynckit@0.4.0: 99 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 100 | 101 | call-bind-apply-helpers@1.0.2: 102 | resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 103 | engines: {node: '>= 0.4'} 104 | 105 | call-bound@1.0.4: 106 | resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} 107 | engines: {node: '>= 0.4'} 108 | 109 | combined-stream@1.0.8: 110 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 111 | engines: {node: '>= 0.8'} 112 | 113 | component-emitter@1.3.1: 114 | resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} 115 | 116 | content-disposition@0.5.4: 117 | resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} 118 | engines: {node: '>= 0.6'} 119 | 120 | content-type@1.0.5: 121 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 122 | engines: {node: '>= 0.6'} 123 | 124 | cookiejar@2.1.4: 125 | resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} 126 | 127 | cookies@0.9.1: 128 | resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} 129 | engines: {node: '>= 0.8'} 130 | 131 | debug@4.4.1: 132 | resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} 133 | engines: {node: '>=6.0'} 134 | peerDependencies: 135 | supports-color: '*' 136 | peerDependenciesMeta: 137 | supports-color: 138 | optional: true 139 | 140 | deep-equal@1.0.1: 141 | resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} 142 | 143 | delayed-stream@1.0.0: 144 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 145 | engines: {node: '>=0.4.0'} 146 | 147 | delegates@1.0.0: 148 | resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} 149 | 150 | depd@1.1.2: 151 | resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} 152 | engines: {node: '>= 0.6'} 153 | 154 | depd@2.0.0: 155 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 156 | engines: {node: '>= 0.8'} 157 | 158 | destroy@1.2.0: 159 | resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} 160 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 161 | 162 | dezalgo@1.0.4: 163 | resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} 164 | 165 | dunder-proto@1.0.1: 166 | resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 167 | engines: {node: '>= 0.4'} 168 | 169 | ee-first@1.1.1: 170 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 171 | 172 | encodeurl@2.0.0: 173 | resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} 174 | engines: {node: '>= 0.8'} 175 | 176 | es-define-property@1.0.1: 177 | resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 178 | engines: {node: '>= 0.4'} 179 | 180 | es-errors@1.3.0: 181 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 182 | engines: {node: '>= 0.4'} 183 | 184 | es-object-atoms@1.1.1: 185 | resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 186 | engines: {node: '>= 0.4'} 187 | 188 | es-set-tostringtag@2.1.0: 189 | resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} 190 | engines: {node: '>= 0.4'} 191 | 192 | escape-html@1.0.3: 193 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 194 | 195 | fast-safe-stringify@2.1.1: 196 | resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} 197 | 198 | form-data@4.0.4: 199 | resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} 200 | engines: {node: '>= 6'} 201 | 202 | formidable@3.5.4: 203 | resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} 204 | engines: {node: '>=14.0.0'} 205 | 206 | fresh@0.5.2: 207 | resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} 208 | engines: {node: '>= 0.6'} 209 | 210 | function-bind@1.1.2: 211 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 212 | 213 | get-intrinsic@1.3.0: 214 | resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 215 | engines: {node: '>= 0.4'} 216 | 217 | get-proto@1.0.1: 218 | resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 219 | engines: {node: '>= 0.4'} 220 | 221 | gopd@1.2.0: 222 | resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 223 | engines: {node: '>= 0.4'} 224 | 225 | has-symbols@1.1.0: 226 | resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 227 | engines: {node: '>= 0.4'} 228 | 229 | has-tostringtag@1.0.2: 230 | resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} 231 | engines: {node: '>= 0.4'} 232 | 233 | hasown@2.0.2: 234 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 235 | engines: {node: '>= 0.4'} 236 | 237 | http-assert@1.5.0: 238 | resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} 239 | engines: {node: '>= 0.8'} 240 | 241 | http-errors@1.8.1: 242 | resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} 243 | engines: {node: '>= 0.6'} 244 | 245 | http-errors@2.0.0: 246 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 247 | engines: {node: '>= 0.8'} 248 | 249 | inherits@2.0.4: 250 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 251 | 252 | keygrip@1.1.0: 253 | resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} 254 | engines: {node: '>= 0.6'} 255 | 256 | koa-compose@4.1.0: 257 | resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} 258 | 259 | koa@3.0.1: 260 | resolution: {integrity: sha512-oDxVkRwPOHhGlxKIDiDB2h+/l05QPtefD7nSqRgDfZt8P+QVYFWjfeK8jANf5O2YXjk8egd7KntvXKYx82wOag==} 261 | engines: {node: '>= 18'} 262 | 263 | math-intrinsics@1.1.0: 264 | resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 265 | engines: {node: '>= 0.4'} 266 | 267 | media-typer@1.1.0: 268 | resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} 269 | engines: {node: '>= 0.8'} 270 | 271 | methods@1.1.2: 272 | resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} 273 | engines: {node: '>= 0.6'} 274 | 275 | mime-db@1.52.0: 276 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 277 | engines: {node: '>= 0.6'} 278 | 279 | mime-db@1.54.0: 280 | resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} 281 | engines: {node: '>= 0.6'} 282 | 283 | mime-types@2.1.35: 284 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 285 | engines: {node: '>= 0.6'} 286 | 287 | mime-types@3.0.1: 288 | resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} 289 | engines: {node: '>= 0.6'} 290 | 291 | mime@2.6.0: 292 | resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} 293 | engines: {node: '>=4.0.0'} 294 | hasBin: true 295 | 296 | ms@2.1.3: 297 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 298 | 299 | negotiator@0.6.3: 300 | resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} 301 | engines: {node: '>= 0.6'} 302 | 303 | object-inspect@1.13.4: 304 | resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} 305 | engines: {node: '>= 0.4'} 306 | 307 | on-finished@2.4.1: 308 | resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 309 | engines: {node: '>= 0.8'} 310 | 311 | once@1.4.0: 312 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 313 | 314 | parseurl@1.3.3: 315 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 316 | engines: {node: '>= 0.8'} 317 | 318 | qs@6.14.0: 319 | resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} 320 | engines: {node: '>=0.6'} 321 | 322 | safe-buffer@5.2.1: 323 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 324 | 325 | setprototypeof@1.2.0: 326 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 327 | 328 | side-channel-list@1.0.0: 329 | resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} 330 | engines: {node: '>= 0.4'} 331 | 332 | side-channel-map@1.0.1: 333 | resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} 334 | engines: {node: '>= 0.4'} 335 | 336 | side-channel-weakmap@1.0.2: 337 | resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} 338 | engines: {node: '>= 0.4'} 339 | 340 | side-channel@1.1.0: 341 | resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} 342 | engines: {node: '>= 0.4'} 343 | 344 | statuses@1.5.0: 345 | resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} 346 | engines: {node: '>= 0.6'} 347 | 348 | statuses@2.0.1: 349 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 350 | engines: {node: '>= 0.8'} 351 | 352 | statuses@2.0.2: 353 | resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} 354 | engines: {node: '>= 0.8'} 355 | 356 | superagent@10.2.3: 357 | resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==} 358 | engines: {node: '>=14.18.0'} 359 | 360 | supertest@7.1.4: 361 | resolution: {integrity: sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==} 362 | engines: {node: '>=14.18.0'} 363 | 364 | toidentifier@1.0.1: 365 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 366 | engines: {node: '>=0.6'} 367 | 368 | tsscmp@1.0.6: 369 | resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} 370 | engines: {node: '>=0.6.x'} 371 | 372 | type-is@2.0.1: 373 | resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} 374 | engines: {node: '>= 0.6'} 375 | 376 | undici-types@7.8.0: 377 | resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} 378 | 379 | vary@1.1.2: 380 | resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 381 | engines: {node: '>= 0.8'} 382 | 383 | wrappy@1.0.2: 384 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 385 | 386 | snapshots: 387 | 388 | '@noble/hashes@1.8.0': {} 389 | 390 | '@paralleldrive/cuid2@2.2.2': 391 | dependencies: 392 | '@noble/hashes': 1.8.0 393 | 394 | '@types/accepts@1.3.7': 395 | dependencies: 396 | '@types/node': 24.1.0 397 | 398 | '@types/body-parser@1.19.6': 399 | dependencies: 400 | '@types/connect': 3.4.38 401 | '@types/node': 24.1.0 402 | 403 | '@types/connect@3.4.38': 404 | dependencies: 405 | '@types/node': 24.1.0 406 | 407 | '@types/content-disposition@0.5.9': {} 408 | 409 | '@types/cookies@0.9.1': 410 | dependencies: 411 | '@types/connect': 3.4.38 412 | '@types/express': 5.0.3 413 | '@types/keygrip': 1.0.6 414 | '@types/node': 24.1.0 415 | 416 | '@types/express-serve-static-core@5.0.7': 417 | dependencies: 418 | '@types/node': 24.1.0 419 | '@types/qs': 6.14.0 420 | '@types/range-parser': 1.2.7 421 | '@types/send': 0.17.5 422 | 423 | '@types/express@5.0.3': 424 | dependencies: 425 | '@types/body-parser': 1.19.6 426 | '@types/express-serve-static-core': 5.0.7 427 | '@types/serve-static': 1.15.8 428 | 429 | '@types/http-assert@1.5.6': {} 430 | 431 | '@types/http-errors@2.0.5': {} 432 | 433 | '@types/keygrip@1.0.6': {} 434 | 435 | '@types/koa-compose@3.2.8': 436 | dependencies: 437 | '@types/koa': 3.0.0 438 | 439 | '@types/koa@3.0.0': 440 | dependencies: 441 | '@types/accepts': 1.3.7 442 | '@types/content-disposition': 0.5.9 443 | '@types/cookies': 0.9.1 444 | '@types/http-assert': 1.5.6 445 | '@types/http-errors': 2.0.5 446 | '@types/keygrip': 1.0.6 447 | '@types/koa-compose': 3.2.8 448 | '@types/node': 24.1.0 449 | 450 | '@types/mime@1.3.5': {} 451 | 452 | '@types/node@24.1.0': 453 | dependencies: 454 | undici-types: 7.8.0 455 | 456 | '@types/qs@6.14.0': {} 457 | 458 | '@types/range-parser@1.2.7': {} 459 | 460 | '@types/send@0.17.5': 461 | dependencies: 462 | '@types/mime': 1.3.5 463 | '@types/node': 24.1.0 464 | 465 | '@types/serve-static@1.15.8': 466 | dependencies: 467 | '@types/http-errors': 2.0.5 468 | '@types/node': 24.1.0 469 | '@types/send': 0.17.5 470 | 471 | accepts@1.3.8: 472 | dependencies: 473 | mime-types: 2.1.35 474 | negotiator: 0.6.3 475 | 476 | asap@2.0.6: {} 477 | 478 | asynckit@0.4.0: {} 479 | 480 | call-bind-apply-helpers@1.0.2: 481 | dependencies: 482 | es-errors: 1.3.0 483 | function-bind: 1.1.2 484 | 485 | call-bound@1.0.4: 486 | dependencies: 487 | call-bind-apply-helpers: 1.0.2 488 | get-intrinsic: 1.3.0 489 | 490 | combined-stream@1.0.8: 491 | dependencies: 492 | delayed-stream: 1.0.0 493 | 494 | component-emitter@1.3.1: {} 495 | 496 | content-disposition@0.5.4: 497 | dependencies: 498 | safe-buffer: 5.2.1 499 | 500 | content-type@1.0.5: {} 501 | 502 | cookiejar@2.1.4: {} 503 | 504 | cookies@0.9.1: 505 | dependencies: 506 | depd: 2.0.0 507 | keygrip: 1.1.0 508 | 509 | debug@4.4.1: 510 | dependencies: 511 | ms: 2.1.3 512 | 513 | deep-equal@1.0.1: {} 514 | 515 | delayed-stream@1.0.0: {} 516 | 517 | delegates@1.0.0: {} 518 | 519 | depd@1.1.2: {} 520 | 521 | depd@2.0.0: {} 522 | 523 | destroy@1.2.0: {} 524 | 525 | dezalgo@1.0.4: 526 | dependencies: 527 | asap: 2.0.6 528 | wrappy: 1.0.2 529 | 530 | dunder-proto@1.0.1: 531 | dependencies: 532 | call-bind-apply-helpers: 1.0.2 533 | es-errors: 1.3.0 534 | gopd: 1.2.0 535 | 536 | ee-first@1.1.1: {} 537 | 538 | encodeurl@2.0.0: {} 539 | 540 | es-define-property@1.0.1: {} 541 | 542 | es-errors@1.3.0: {} 543 | 544 | es-object-atoms@1.1.1: 545 | dependencies: 546 | es-errors: 1.3.0 547 | 548 | es-set-tostringtag@2.1.0: 549 | dependencies: 550 | es-errors: 1.3.0 551 | get-intrinsic: 1.3.0 552 | has-tostringtag: 1.0.2 553 | hasown: 2.0.2 554 | 555 | escape-html@1.0.3: {} 556 | 557 | fast-safe-stringify@2.1.1: {} 558 | 559 | form-data@4.0.4: 560 | dependencies: 561 | asynckit: 0.4.0 562 | combined-stream: 1.0.8 563 | es-set-tostringtag: 2.1.0 564 | hasown: 2.0.2 565 | mime-types: 2.1.35 566 | 567 | formidable@3.5.4: 568 | dependencies: 569 | '@paralleldrive/cuid2': 2.2.2 570 | dezalgo: 1.0.4 571 | once: 1.4.0 572 | 573 | fresh@0.5.2: {} 574 | 575 | function-bind@1.1.2: {} 576 | 577 | get-intrinsic@1.3.0: 578 | dependencies: 579 | call-bind-apply-helpers: 1.0.2 580 | es-define-property: 1.0.1 581 | es-errors: 1.3.0 582 | es-object-atoms: 1.1.1 583 | function-bind: 1.1.2 584 | get-proto: 1.0.1 585 | gopd: 1.2.0 586 | has-symbols: 1.1.0 587 | hasown: 2.0.2 588 | math-intrinsics: 1.1.0 589 | 590 | get-proto@1.0.1: 591 | dependencies: 592 | dunder-proto: 1.0.1 593 | es-object-atoms: 1.1.1 594 | 595 | gopd@1.2.0: {} 596 | 597 | has-symbols@1.1.0: {} 598 | 599 | has-tostringtag@1.0.2: 600 | dependencies: 601 | has-symbols: 1.1.0 602 | 603 | hasown@2.0.2: 604 | dependencies: 605 | function-bind: 1.1.2 606 | 607 | http-assert@1.5.0: 608 | dependencies: 609 | deep-equal: 1.0.1 610 | http-errors: 1.8.1 611 | 612 | http-errors@1.8.1: 613 | dependencies: 614 | depd: 1.1.2 615 | inherits: 2.0.4 616 | setprototypeof: 1.2.0 617 | statuses: 1.5.0 618 | toidentifier: 1.0.1 619 | 620 | http-errors@2.0.0: 621 | dependencies: 622 | depd: 2.0.0 623 | inherits: 2.0.4 624 | setprototypeof: 1.2.0 625 | statuses: 2.0.1 626 | toidentifier: 1.0.1 627 | 628 | inherits@2.0.4: {} 629 | 630 | keygrip@1.1.0: 631 | dependencies: 632 | tsscmp: 1.0.6 633 | 634 | koa-compose@4.1.0: {} 635 | 636 | koa@3.0.1: 637 | dependencies: 638 | accepts: 1.3.8 639 | content-disposition: 0.5.4 640 | content-type: 1.0.5 641 | cookies: 0.9.1 642 | delegates: 1.0.0 643 | destroy: 1.2.0 644 | encodeurl: 2.0.0 645 | escape-html: 1.0.3 646 | fresh: 0.5.2 647 | http-assert: 1.5.0 648 | http-errors: 2.0.0 649 | koa-compose: 4.1.0 650 | mime-types: 3.0.1 651 | on-finished: 2.4.1 652 | parseurl: 1.3.3 653 | statuses: 2.0.2 654 | type-is: 2.0.1 655 | vary: 1.1.2 656 | 657 | math-intrinsics@1.1.0: {} 658 | 659 | media-typer@1.1.0: {} 660 | 661 | methods@1.1.2: {} 662 | 663 | mime-db@1.52.0: {} 664 | 665 | mime-db@1.54.0: {} 666 | 667 | mime-types@2.1.35: 668 | dependencies: 669 | mime-db: 1.52.0 670 | 671 | mime-types@3.0.1: 672 | dependencies: 673 | mime-db: 1.54.0 674 | 675 | mime@2.6.0: {} 676 | 677 | ms@2.1.3: {} 678 | 679 | negotiator@0.6.3: {} 680 | 681 | object-inspect@1.13.4: {} 682 | 683 | on-finished@2.4.1: 684 | dependencies: 685 | ee-first: 1.1.1 686 | 687 | once@1.4.0: 688 | dependencies: 689 | wrappy: 1.0.2 690 | 691 | parseurl@1.3.3: {} 692 | 693 | qs@6.14.0: 694 | dependencies: 695 | side-channel: 1.1.0 696 | 697 | safe-buffer@5.2.1: {} 698 | 699 | setprototypeof@1.2.0: {} 700 | 701 | side-channel-list@1.0.0: 702 | dependencies: 703 | es-errors: 1.3.0 704 | object-inspect: 1.13.4 705 | 706 | side-channel-map@1.0.1: 707 | dependencies: 708 | call-bound: 1.0.4 709 | es-errors: 1.3.0 710 | get-intrinsic: 1.3.0 711 | object-inspect: 1.13.4 712 | 713 | side-channel-weakmap@1.0.2: 714 | dependencies: 715 | call-bound: 1.0.4 716 | es-errors: 1.3.0 717 | get-intrinsic: 1.3.0 718 | object-inspect: 1.13.4 719 | side-channel-map: 1.0.1 720 | 721 | side-channel@1.1.0: 722 | dependencies: 723 | es-errors: 1.3.0 724 | object-inspect: 1.13.4 725 | side-channel-list: 1.0.0 726 | side-channel-map: 1.0.1 727 | side-channel-weakmap: 1.0.2 728 | 729 | statuses@1.5.0: {} 730 | 731 | statuses@2.0.1: {} 732 | 733 | statuses@2.0.2: {} 734 | 735 | superagent@10.2.3: 736 | dependencies: 737 | component-emitter: 1.3.1 738 | cookiejar: 2.1.4 739 | debug: 4.4.1 740 | fast-safe-stringify: 2.1.1 741 | form-data: 4.0.4 742 | formidable: 3.5.4 743 | methods: 1.1.2 744 | mime: 2.6.0 745 | qs: 6.14.0 746 | transitivePeerDependencies: 747 | - supports-color 748 | 749 | supertest@7.1.4: 750 | dependencies: 751 | methods: 1.1.2 752 | superagent: 10.2.3 753 | transitivePeerDependencies: 754 | - supports-color 755 | 756 | toidentifier@1.0.1: {} 757 | 758 | tsscmp@1.0.6: {} 759 | 760 | type-is@2.0.1: 761 | dependencies: 762 | content-type: 1.0.5 763 | media-typer: 1.1.0 764 | mime-types: 3.0.1 765 | 766 | undici-types@7.8.0: {} 767 | 768 | vary@1.1.2: {} 769 | 770 | wrappy@1.0.2: {} 771 | --------------------------------------------------------------------------------