├── .cjs.swcrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .swcrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── benchmarks ├── index.ts ├── libs │ ├── express.ts │ ├── find-my-way.ts │ ├── hono-regexp-router.ts │ ├── hono-smart-router.ts │ ├── hono-tire-router.ts │ ├── medley.ts │ ├── radix3.ts │ ├── raikiri.ts │ └── trouter.ts └── utils.ts ├── bun.lockb ├── example └── index.ts ├── package.json ├── src ├── ei.ts ├── index.ts └── mei.ts ├── test ├── add.test.ts └── index.test.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json /.cjs.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "module": { 4 | "type": "commonjs" 5 | }, 6 | "exclude": ["(.*).(spec|test).(j|t)s$"], 7 | "jsc": { 8 | "target": "es2022", 9 | "parser": { 10 | "syntax": "typescript" 11 | }, 12 | "minify": { 13 | "mangle": true, 14 | "compress": { 15 | "hoist_funs": true, 16 | "reduce_funcs": true 17 | } 18 | } 19 | }, 20 | "minify": true, 21 | "sourceMaps": false 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/ban-types": 'off', 20 | '@typescript-eslint/no-explicit-any': 'off' 21 | }, 22 | "ignorePatterns": ["example/*", "tests/**/*"] 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | .pnpm-debug.log 5 | dist 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .prettierrc 4 | .swcrc 5 | .cjs.swcrc 6 | .esm.swcrc 7 | .eslintrc.js 8 | 9 | bun.lockb 10 | pnpm-lock.yaml 11 | 12 | node_modules 13 | tsconfig.json 14 | tsconfig.cjs.json 15 | tsconfig.esm.json 16 | CHANGELOG.md 17 | jest.config.js 18 | nodemon.json 19 | 20 | example 21 | tests 22 | test 23 | CHANGELOG.md 24 | benchmarks 25 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "module": { 4 | "type": "es6" 5 | }, 6 | "exclude": ["(.*).(spec|test).(j|t)s$"], 7 | "jsc": { 8 | "target": "es2022", 9 | "parser": { 10 | "syntax": "typescript" 11 | }, 12 | "minify": { 13 | "mangle": true, 14 | "compress": { 15 | "hoist_funs": true, 16 | "reduce_funcs": true 17 | } 18 | } 19 | }, 20 | "minify": true, 21 | "sourceMaps": false 22 | } 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.3 - 25 Apr 2023 2 | Fix: 3 | - using mangled part unioned instead of accidental first char 4 | - using exact match for left over part 5 | - should be a ble to register param after same prefix 6 | 7 | # 0.1.2 - 21 Apr 2023 8 | Fix: 9 | - Register mangled path 10 | 11 | # 0.1.0-beta.1 - 21 Mar 2023 12 | Improvement: 13 | - Return nullable or literal 14 | 15 | # 0.1.0-beta.0 - 15 Mar 2023 16 | Fix: 17 | - Wildcard without static fallback fail to resolve path 18 | 19 | # 0.1.0-beta.0 - 15 Mar 2023 20 | Improvement: 21 | - Rewrite router 22 | - Using single decision path parameter 23 | 24 | # 0.0.0-beta.8 - 23 Feb 2023 25 | Fix: 26 | - Don't enforce es module 27 | 28 | # 0.0.0-beta.7 - 23 Feb 2023 29 | Fix: 30 | - Set dependencies to devDependencies 31 | 32 | # 0.0.0-beta.6 - 23 Feb 2023 33 | Improvement: 34 | - add cjs support 35 | 36 | # 0.0.0-beta.5 - 1 Feb 2023 37 | Fix: 38 | - path mangling clean up 39 | 40 | # 0.0.0-beta.4 - 1 Feb 2023 41 | Improvement: 42 | - Using charCode for fast fracture mapping 43 | - Auto re-ordering for static path 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 saltyAom 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raikiri ⚡️ 2 | A fast router optimized for static path searching. 3 | 4 | Raikiri is a router implemented using a Radix Tree algorithm. 5 | -------------------------------------------------------------------------------- /benchmarks/index.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'fs' 2 | 3 | readdirSync(`${import.meta.dir}/libs`) 4 | .sort((a, b) => a.localeCompare(b)) 5 | .forEach(async (file) => { 6 | const process = await Bun.spawnSync(['bun', `libs/${file}`], { 7 | cwd: import.meta.dir, 8 | stdout: 'pipe' 9 | }) 10 | 11 | console.log(process.stdout?.toString()) 12 | }) 13 | -------------------------------------------------------------------------------- /benchmarks/libs/express.ts: -------------------------------------------------------------------------------- 1 | import { title, now, print, operations } from '../utils' 2 | const router = require('express/lib/router')() 3 | 4 | title('express benchmark (WARNING: includes handling)') 5 | 6 | const routes = [ 7 | { method: 'GET', url: '/user' }, 8 | { method: 'GET', url: '/user/comments' }, 9 | { method: 'GET', url: '/user/avatar' }, 10 | { method: 'GET', url: '/user/lookup/username/:username' }, 11 | { method: 'GET', url: '/user/lookup/email/:address' }, 12 | { method: 'GET', url: '/event/:id' }, 13 | { method: 'GET', url: '/event/:id/comments' }, 14 | { method: 'POST', url: '/event/:id/comment' }, 15 | { method: 'GET', url: '/map/:location/events' }, 16 | { method: 'GET', url: '/status' }, 17 | { method: 'GET', url: '/very/deeply/nested/route/hello/there' }, 18 | { method: 'GET', url: '/static/*' } 19 | ] 20 | 21 | function noop() {} 22 | var i = 0 23 | var time = 0 24 | 25 | routes.forEach((route) => { 26 | if (route.method === 'GET') { 27 | router.route(route.url).get(noop) 28 | } else { 29 | router.route(route.url).post(noop) 30 | } 31 | }) 32 | 33 | time = now() 34 | for (i = 0; i < operations; i++) { 35 | router.handle({ method: 'GET', url: '/user' }) 36 | } 37 | print('short static:', time) 38 | 39 | time = now() 40 | for (i = 0; i < operations; i++) { 41 | router.handle({ method: 'GET', url: '/user/comments' }) 42 | } 43 | print('static with same radix:', time) 44 | 45 | time = now() 46 | for (i = 0; i < operations; i++) { 47 | router.handle({ method: 'GET', url: '/user/lookup/username/john' }) 48 | } 49 | print('dynamic route:', time) 50 | 51 | time = now() 52 | for (i = 0; i < operations; i++) { 53 | router.handle( 54 | { method: 'GET', url: '/event/abcd1234/comments' }, 55 | null, 56 | noop 57 | ) 58 | } 59 | print('mixed static dynamic:', time) 60 | 61 | time = now() 62 | for (i = 0; i < operations; i++) { 63 | router.handle( 64 | { method: 'GET', url: '/very/deeply/nested/route/hello/there' }, 65 | null, 66 | noop 67 | ) 68 | } 69 | print('long static:', time) 70 | 71 | time = now() 72 | for (i = 0; i < operations; i++) { 73 | router.handle({ method: 'GET', url: '/static/index.html' }, null, noop) 74 | } 75 | print('wildcard:', time) 76 | -------------------------------------------------------------------------------- /benchmarks/libs/find-my-way.ts: -------------------------------------------------------------------------------- 1 | import { title, now, print, operations } from '../utils' 2 | const router = require('find-my-way')() 3 | 4 | title('find-my-way benchmark') 5 | 6 | const routes = [ 7 | { method: 'GET', url: '/user' }, 8 | { method: 'GET', url: '/user/comments' }, 9 | { method: 'GET', url: '/user/avatar' }, 10 | { method: 'GET', url: '/user/lookup/username/:username' }, 11 | { method: 'GET', url: '/user/lookup/email/:address' }, 12 | { method: 'GET', url: '/event/:id' }, 13 | { method: 'GET', url: '/event/:id/comments' }, 14 | { method: 'POST', url: '/event/:id/comment' }, 15 | { method: 'GET', url: '/map/:location/events' }, 16 | { method: 'GET', url: '/status' }, 17 | { method: 'GET', url: '/very/deeply/nested/route/hello/there' }, 18 | { method: 'GET', url: '/static/*' } 19 | ] 20 | 21 | function noop () {} 22 | var i = 0 23 | var time = 0 24 | 25 | routes.forEach(route => { 26 | router.on(route.method, route.url, noop) 27 | }) 28 | 29 | time = now() 30 | for (i = 0; i < operations; i++) { 31 | router.find('GET', '/user') 32 | } 33 | print('short static:', time) 34 | 35 | time = now() 36 | for (i = 0; i < operations; i++) { 37 | router.find('GET', 'http://localhost:8080/user/comments') 38 | } 39 | print('static with same radix:', time) 40 | 41 | time = now() 42 | for (i = 0; i < operations; i++) { 43 | router.find('GET', 'http://localhost:8080/user/lookup/username/john') 44 | } 45 | print('dynamic route:', time) 46 | 47 | time = now() 48 | for (i = 0; i < operations; i++) { 49 | router.find('GET', 'http://localhost:8080/event/abcd1234/comments') 50 | } 51 | print('mixed static dynamic:', time) 52 | 53 | time = now() 54 | for (i = 0; i < operations; i++) { 55 | router.find('GET', 'http://localhost:8080/very/deeply/nested/route/hello/there') 56 | } 57 | print('long static:', time) 58 | 59 | time = now() 60 | for (i = 0; i < operations; i++) { 61 | router.find('GET', 'http://localhost:8080/static/index.html') 62 | } 63 | print('wildcard:', time) 64 | -------------------------------------------------------------------------------- /benchmarks/libs/hono-regexp-router.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { RegExpRouter } from '../../node_modules/hono/dist/router/reg-exp-router' 3 | import { title, now, print, operations } from '../utils' 4 | 5 | const router = new RegExpRouter() 6 | 7 | title('hono/reg-exp-router') 8 | 9 | const routes = [ 10 | { method: 'GET', url: 'http://localhost:8080/user' }, 11 | { method: 'GET', url: 'http://localhost:8080/user/comments' }, 12 | { method: 'GET', url: 'http://localhost:8080/user/avatar' }, 13 | { method: 'GET', url: 'http://localhost:8080/user/lookup/username/:username' }, 14 | { method: 'GET', url: 'http://localhost:8080/user/lookup/email/:address' }, 15 | { method: 'GET', url: 'http://localhost:8080/event/:id' }, 16 | { method: 'GET', url: 'http://localhost:8080/event/:id/comments' }, 17 | { method: 'POST', url: 'http://localhost:8080/event/:id/comment' }, 18 | { method: 'GET', url: 'http://localhost:8080/map/:location/events' }, 19 | { method: 'GET', url: 'http://localhost:8080/status' }, 20 | { method: 'GET', url: 'http://localhost:8080/very/deeply/nested/route/hello/there' }, 21 | { method: 'GET', url: 'http://localhost:8080/static/*' } 22 | ] 23 | 24 | function noop() {} 25 | var i = 0 26 | var time = 0 27 | 28 | routes.forEach((route) => { 29 | router.add(route.method, route.url, noop) 30 | }) 31 | 32 | time = now() 33 | for (i = 0; i < operations; i++) { 34 | router.match('GET', '/user') 35 | } 36 | print('short static:', time) 37 | 38 | time = now() 39 | for (i = 0; i < operations; i++) { 40 | router.match('GET', '/user/comments') 41 | } 42 | print('static with same radix:', time) 43 | 44 | time = now() 45 | for (i = 0; i < operations; i++) { 46 | router.match('GET', '/user/lookup/username/john') 47 | } 48 | print('dynamic route:', time) 49 | 50 | time = now() 51 | for (i = 0; i < operations; i++) { 52 | router.match('GET', '/event/abcd1234/comments') 53 | } 54 | print('mixed static dynamic:', time) 55 | 56 | time = now() 57 | for (i = 0; i < operations; i++) { 58 | router.match('GET', '/very/deeply/nested/route/hello/there') 59 | } 60 | print('long static:', time) 61 | 62 | time = now() 63 | for (i = 0; i < operations; i++) { 64 | router.match('GET', '/static/index.html') 65 | } 66 | print('wildcard:', time) 67 | -------------------------------------------------------------------------------- /benchmarks/libs/hono-smart-router.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { SmartRouter } from '../../node_modules/hono/dist/router/smart-router' 3 | // @ts-ignore 4 | import { RegExpRouter } from 'hono/dist/router/reg-exp-router' 5 | // @ts-ignore 6 | import { StaticRouter } from 'hono/dist/router/static-router' 7 | // @ts-ignore 8 | import { TrieRouter } from 'hono/dist/router/trie-router' 9 | 10 | import { title, now, print, operations } from '../utils' 11 | 12 | const router = new SmartRouter({ 13 | routers: [new RegExpRouter(), new StaticRouter(), new TrieRouter()] 14 | }) 15 | 16 | title('hono/smart-router') 17 | 18 | const routes = [ 19 | { method: 'GET', url: '/user' }, 20 | { method: 'GET', url: '/user/comments' }, 21 | { method: 'GET', url: '/user/avatar' }, 22 | { method: 'GET', url: '/user/lookup/username/:username' }, 23 | { method: 'GET', url: '/user/lookup/email/:address' }, 24 | { method: 'GET', url: '/event/:id' }, 25 | { method: 'GET', url: '/event/:id/comments' }, 26 | { method: 'POST', url: '/event/:id/comment' }, 27 | { method: 'GET', url: '/map/:location/events' }, 28 | { method: 'GET', url: '/status' }, 29 | { method: 'GET', url: '/very/deeply/nested/route/hello/there' }, 30 | { method: 'GET', url: '/static/*' } 31 | ] 32 | 33 | function noop() {} 34 | var i = 0 35 | var time = 0 36 | 37 | routes.forEach((route) => { 38 | router.add(route.method, route.url, noop) 39 | }) 40 | 41 | time = now() 42 | for (i = 0; i < operations; i++) { 43 | router.match('GET', '/user') 44 | } 45 | print('short static:', time) 46 | 47 | time = now() 48 | for (i = 0; i < operations; i++) { 49 | router.match('GET', '/user/comments') 50 | } 51 | print('static with same radix:', time) 52 | 53 | time = now() 54 | for (i = 0; i < operations; i++) { 55 | router.match('GET', '/user/lookup/username/john') 56 | } 57 | print('dynamic route:', time) 58 | 59 | time = now() 60 | for (i = 0; i < operations; i++) { 61 | router.match('GET', '/event/abcd1234/comments') 62 | } 63 | print('mixed static dynamic:', time) 64 | 65 | time = now() 66 | for (i = 0; i < operations; i++) { 67 | router.match('GET', '/very/deeply/nested/route/hello/there') 68 | } 69 | print('long static:', time) 70 | 71 | time = now() 72 | for (i = 0; i < operations; i++) { 73 | router.match('GET', '/static/index.html') 74 | } 75 | print('wildcard:', time) 76 | -------------------------------------------------------------------------------- /benchmarks/libs/hono-tire-router.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { TrieRouter } from '../../node_modules/hono/dist/router/trie-router' 3 | import { title, now, print, operations } from '../utils' 4 | 5 | const router = new TrieRouter() 6 | 7 | title('hono/tire-router') 8 | 9 | const routes = [ 10 | { method: 'GET', url: '/user' }, 11 | { method: 'GET', url: '/user/comments' }, 12 | { method: 'GET', url: '/user/avatar' }, 13 | { method: 'GET', url: '/user/lookup/username/:username' }, 14 | { method: 'GET', url: '/user/lookup/email/:address' }, 15 | { method: 'GET', url: '/event/:id' }, 16 | { method: 'GET', url: '/event/:id/comments' }, 17 | { method: 'POST', url: '/event/:id/comment' }, 18 | { method: 'GET', url: '/map/:location/events' }, 19 | { method: 'GET', url: '/status' }, 20 | { method: 'GET', url: '/very/deeply/nested/route/hello/there' }, 21 | { method: 'GET', url: '/static/*' } 22 | ] 23 | 24 | function noop() {} 25 | var i = 0 26 | var time = 0 27 | 28 | routes.forEach((route) => { 29 | router.add(route.method, route.url, noop) 30 | }) 31 | 32 | time = now() 33 | for (i = 0; i < operations; i++) { 34 | router.match('GET', '/user') 35 | } 36 | print('short static:', time) 37 | 38 | time = now() 39 | for (i = 0; i < operations; i++) { 40 | router.match('GET', '/user/comments') 41 | } 42 | print('static with same radix:', time) 43 | 44 | time = now() 45 | for (i = 0; i < operations; i++) { 46 | router.match('GET', '/user/lookup/username/john') 47 | } 48 | print('dynamic route:', time) 49 | 50 | time = now() 51 | for (i = 0; i < operations; i++) { 52 | router.match('GET', '/event/abcd1234/comments') 53 | } 54 | print('mixed static dynamic:', time) 55 | 56 | time = now() 57 | for (i = 0; i < operations; i++) { 58 | router.match('GET', '/very/deeply/nested/route/hello/there') 59 | } 60 | print('long static:', time) 61 | 62 | time = now() 63 | for (i = 0; i < operations; i++) { 64 | router.match('GET', '/static/index.html') 65 | } 66 | print('wildcard:', time) 67 | -------------------------------------------------------------------------------- /benchmarks/libs/medley.ts: -------------------------------------------------------------------------------- 1 | import { title, now, print, operations } from '../utils' 2 | 3 | const Router = require('@medley/router') 4 | 5 | const router = new Router() 6 | 7 | title('@medley/router') 8 | 9 | const routes = [ 10 | { method: 'GET', url: '/user' }, 11 | { method: 'GET', url: '/user/comments' }, 12 | { method: 'GET', url: '/user/avatar' }, 13 | { method: 'GET', url: '/user/lookup/username/:username' }, 14 | { method: 'GET', url: '/user/lookup/email/:address' }, 15 | { method: 'GET', url: '/event/:id' }, 16 | { method: 'GET', url: '/event/:id/comments' }, 17 | { method: 'POST', url: '/event/:id/comment' }, 18 | { method: 'GET', url: '/map/:location/events' }, 19 | { method: 'GET', url: '/status' }, 20 | { method: 'GET', url: '/very/deeply/nested/route/hello/there' }, 21 | { method: 'GET', url: '/static/*' } 22 | ] 23 | 24 | function addRoute(method: string, path: string, handler: any) { 25 | const store = router.register(path) 26 | store[method] = handler 27 | } 28 | 29 | function noop() {} 30 | var i = 0 31 | var time = 0 32 | 33 | routes.forEach((route) => { 34 | addRoute(route.method, route.url, noop) 35 | }) 36 | 37 | time = now() 38 | for (i = 0; i < operations; i++) { 39 | router.find('/user').store['GET'] 40 | } 41 | print('short static:', time) 42 | 43 | time = now() 44 | for (i = 0; i < operations; i++) { 45 | router.find('/user/comments').store['GET'] 46 | } 47 | print('static with same radix:', time) 48 | 49 | time = now() 50 | for (i = 0; i < operations; i++) { 51 | router.find('/user/lookup/username/john').store['GET'] 52 | } 53 | print('dynamic route:', time) 54 | 55 | time = now() 56 | for (i = 0; i < operations; i++) { 57 | router.find('/event/abcd1234/comments').store['GET'] 58 | } 59 | print('mixed static dynamic:', time) 60 | 61 | time = now() 62 | for (i = 0; i < operations; i++) { 63 | router.find('/very/deeply/nested/route/hello/there').store['GET'] 64 | } 65 | print('long static:', time) 66 | 67 | time = now() 68 | for (i = 0; i < operations; i++) { 69 | router.find('/static/index.html').store['GET'] 70 | } 71 | print('wildcard:', time) 72 | -------------------------------------------------------------------------------- /benchmarks/libs/radix3.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from 'radix3' 2 | import { title, now, print, operations } from '../utils' 3 | 4 | const router = createRouter() 5 | 6 | title('Radix3 benchmark') 7 | 8 | const routes = [ 9 | { method: 'GET', url: '/user' }, 10 | { method: 'GET', url: '/user/comments' }, 11 | { method: 'GET', url: '/user/avatar' }, 12 | { method: 'GET', url: '/user/lookup/username/:username' }, 13 | { method: 'GET', url: '/user/lookup/email/:address' }, 14 | { method: 'GET', url: '/event/:id' }, 15 | { method: 'GET', url: '/event/:id/comments' }, 16 | { method: 'POST', url: '/event/:id/comment' }, 17 | { method: 'GET', url: '/map/:location/events' }, 18 | { method: 'GET', url: '/status' }, 19 | { method: 'GET', url: '/very/deeply/nested/route/hello/there' }, 20 | { method: 'GET', url: '/static/*' } 21 | ] 22 | 23 | function noop() {} 24 | var i = 0 25 | var time = 0 26 | 27 | routes.forEach((route) => { 28 | router.insert(route.url, { 29 | [route.method]: noop 30 | }) 31 | }) 32 | 33 | time = now() 34 | for (i = 0; i < operations; i++) { 35 | router.lookup('/user')?.['GET'] 36 | } 37 | print('short static:', time) 38 | 39 | time = now() 40 | for (i = 0; i < operations; i++) { 41 | router.lookup('/user/comments')?.['GET'] 42 | } 43 | print('static with same radix:', time) 44 | 45 | time = now() 46 | for (i = 0; i < operations; i++) { 47 | router.lookup('/user/lookup/username/john')?.['GET'] 48 | } 49 | print('dynamic route:', time) 50 | 51 | time = now() 52 | for (i = 0; i < operations; i++) { 53 | router.lookup('/event/abcd1234/comments')?.['GET'] 54 | } 55 | print('mixed static dynamic:', time) 56 | 57 | time = now() 58 | for (i = 0; i < operations; i++) { 59 | router.lookup('/very/deeply/nested/route/hello/there')?.['GET'] 60 | } 61 | print('long static:', time) 62 | 63 | time = now() 64 | for (i = 0; i < operations; i++) { 65 | router.lookup('/static/index.html')?.['GET'] 66 | } 67 | print('wildcard:', time) 68 | -------------------------------------------------------------------------------- /benchmarks/libs/raikiri.ts: -------------------------------------------------------------------------------- 1 | import { title, now, print, operations } from '../utils' 2 | import Rikiri from '../../src' 3 | 4 | const router = new Rikiri() 5 | 6 | title('Raikiri benchmark') 7 | 8 | const routes = [ 9 | { method: 'GET', url: '/user' }, 10 | { method: 'GET', url: '/user/comments' }, 11 | { method: 'GET', url: '/user/avatar' }, 12 | { method: 'GET', url: '/user/lookup/username/:username' }, 13 | { method: 'GET', url: '/user/lookup/email/:address' }, 14 | { method: 'GET', url: '/event/:id' }, 15 | { method: 'GET', url: '/event/:id/comments' }, 16 | { method: 'POST', url: '/event/:id/comment' }, 17 | { method: 'GET', url: '/map/:location/events' }, 18 | { method: 'GET', url: '/status' }, 19 | { method: 'GET', url: '/very/deeply/nested/route/hello/there' }, 20 | { method: 'GET', url: '/static/*' } 21 | ] 22 | 23 | function noop() {} 24 | var i = 0 25 | var time = 0 26 | 27 | routes.forEach((route) => { 28 | router.add(route.method, route.url, noop) 29 | }) 30 | 31 | time = now() 32 | for (i = 0; i < operations; i++) { 33 | router.match('GET', '/user') 34 | } 35 | print('short static:', time) 36 | 37 | time = now() 38 | for (i = 0; i < operations; i++) { 39 | router.match('GET', '/user/comments') 40 | } 41 | print('static with same radix:', time) 42 | 43 | time = now() 44 | for (i = 0; i < operations; i++) { 45 | router.match('GET', '/user/lookup/username/john') 46 | } 47 | print('dynamic route:', time) 48 | 49 | time = now() 50 | for (i = 0; i < operations; i++) { 51 | router.match('GET', '/event/abcd1234/comments') 52 | } 53 | print('mixed static dynamic:', time) 54 | 55 | time = now() 56 | for (i = 0; i < operations; i++) { 57 | router.match('GET', '/very/deeply/nested/route/hello/there') 58 | } 59 | print('long static:', time) 60 | 61 | time = now() 62 | for (i = 0; i < operations; i++) { 63 | router.match('GET', '/static/index.html') 64 | } 65 | print('wildcard:', time) 66 | 67 | // Uncomment this if correction is need 😭😭💢💢💢 68 | // console.log(router.match('GET', '/user')) 69 | // console.log(router.match('GET', '/user/comments')) 70 | // console.log(router.match('GET', '/user/lookup/username/john')) 71 | // console.log(router.match('GET', '/event/abcd1234/comments')) 72 | // console.log(router.match('GET', '/very/deeply/nested/route/hello/there')) 73 | // console.log(router.match('GET', '/static/index.html')) 74 | -------------------------------------------------------------------------------- /benchmarks/libs/trouter.ts: -------------------------------------------------------------------------------- 1 | import TRouter from 'trouter' 2 | import { title, now, print, operations } from '../utils' 3 | 4 | const router = new TRouter() 5 | 6 | title('trouter benchmark') 7 | 8 | const routes = [ 9 | { method: 'GET', url: '/user' }, 10 | { method: 'GET', url: '/user/comments' }, 11 | { method: 'GET', url: '/user/avatar' }, 12 | { method: 'GET', url: '/user/lookup/username/:username' }, 13 | { method: 'GET', url: '/user/lookup/email/:address' }, 14 | { method: 'GET', url: '/event/:id' }, 15 | { method: 'GET', url: '/event/:id/comments' }, 16 | { method: 'POST', url: '/event/:id/comment' }, 17 | { method: 'GET', url: '/map/:location/events' }, 18 | { method: 'GET', url: '/status' }, 19 | { method: 'GET', url: '/very/deeply/nested/route/hello/there' }, 20 | { method: 'GET', url: '/static/*' } 21 | ] 22 | 23 | function noop() {} 24 | var i = 0 25 | var time = 0 26 | 27 | routes.forEach((route) => { 28 | router.add(route.method as any, route.url, noop) 29 | }) 30 | 31 | time = now() 32 | for (i = 0; i < operations; i++) { 33 | router.find('GET', '/user') 34 | } 35 | print('short static:', time) 36 | 37 | time = now() 38 | for (i = 0; i < operations; i++) { 39 | router.find('GET', '/user/comments') 40 | } 41 | print('static with same radix:', time) 42 | 43 | time = now() 44 | for (i = 0; i < operations; i++) { 45 | router.find('GET', '/user/lookup/username/john') 46 | } 47 | print('dynamic route:', time) 48 | 49 | time = now() 50 | for (i = 0; i < operations; i++) { 51 | router.find('GET', '/event/abcd1234/comments') 52 | } 53 | print('mixed static dynamic:', time) 54 | 55 | time = now() 56 | for (i = 0; i < operations; i++) { 57 | router.find('GET', '/very/deeply/nested/route/hello/there') 58 | } 59 | print('long static:', time) 60 | 61 | time = now() 62 | for (i = 0; i < operations; i++) { 63 | router.find('GET', '/static/index.html') 64 | } 65 | print('wildcard:', time) 66 | -------------------------------------------------------------------------------- /benchmarks/utils.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { hrtime } from 'process' 4 | 5 | const operations = 1000000 6 | 7 | function now() { 8 | var ts = hrtime() 9 | 10 | return ts[0] * 1e3 + ts[1] / 1e6 11 | } 12 | 13 | function getOpsSec(ms: number) { 14 | return Number(((operations * 1000) / ms).toFixed()).toLocaleString() 15 | } 16 | 17 | function print(name: string, time: number) { 18 | console.log(name, getOpsSec(now() - time), 'ops/sec') 19 | } 20 | 21 | function title(name: string) { 22 | console.log( 23 | `${'='.repeat(name.length + 2)} 24 | ${name} 25 | ${'='.repeat(name.length + 2)}` 26 | ) 27 | } 28 | 29 | class Queue { 30 | q: Function[] = [] 31 | running: boolean = false 32 | 33 | add(job: Function) { 34 | this.q.push(job) 35 | if (!this.running) this.run() 36 | } 37 | 38 | run() { 39 | this.running = true 40 | const job = this.q.shift() 41 | 42 | if (job) 43 | job(() => { 44 | if (this.q.length) { 45 | this.run() 46 | } else { 47 | this.running = false 48 | } 49 | }) 50 | } 51 | } 52 | 53 | export { now, getOpsSec, print, title, Queue, operations } 54 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltyAom/raikiri/1039ecd4659670afae7249690e202b3b85d0f932/bun.lockb -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { Raikiri } from '../src' 2 | 3 | const router = new Raikiri() 4 | 5 | // router.add('GET', '/api/search/:term', '/api/search/:term') 6 | router.add('GET', '/api/abc/view/:id', '/api/abc/view/:id') 7 | router.add( 8 | 'GET', 9 | '/api/abc/:type', 10 | '/api/abc/:type' 11 | ) 12 | 13 | console.log('GOT', router.match('GET', '/api/abc/type')) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raikiri", 3 | "version": "0.1.3", 4 | "description": "Elysia's Radix tree router", 5 | "author": { 6 | "name": "saltyAom", 7 | "url": "https://github.com/SaltyAom", 8 | "email": "saltyaom@gmail.com" 9 | }, 10 | "main": "./dist/index.js", 11 | "exports": { 12 | "require": "./dist/cjs/index.js", 13 | "import": "./dist/index.js" 14 | }, 15 | "types": "./src/index.ts", 16 | "keywords": [ 17 | "elysia", 18 | "raikiri", 19 | "router" 20 | ], 21 | "homepage": "https://github.com/saltyaom/raikiri", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/saltyaom/raikiri" 25 | }, 26 | "bugs": "https://github.com/saltyaom/raikiri/issues", 27 | "license": "MIT", 28 | "scripts": { 29 | "dev": "bun run --hot example/index.ts", 30 | "test": "bun wiptest", 31 | "build": "rimraf dist && pnpm build:esm && pnpm build:cjs", 32 | "build:cjs": "swc src --config-file .cjs.swcrc -d dist/cjs && tsc --project tsconfig.esm.json", 33 | "build:esm": "swc src -d dist && tsc --project tsconfig.esm.json", 34 | "benchmark": "bun benchmarks/index.ts", 35 | "benchmark:raikiri": "bun benchmarks/libs/raikiri.ts", 36 | "release": "npm run build && npm run test && npm publish --access public" 37 | }, 38 | "devDependencies": { 39 | "@medley/router": "^0.2.1", 40 | "@swc/cli": "^0.1.62", 41 | "@swc/core": "^1.3.40", 42 | "@types/node": "^18.11.7", 43 | "@types/trouter": "^3.1.1", 44 | "bun-types": "^0.5.0", 45 | "express": "^4.18.2", 46 | "find-my-way": "^7.4.0", 47 | "hono": "^2.7.5", 48 | "radix3": "^1.0.0", 49 | "rimraf": "^3.0.2", 50 | "trouter": "^3.2.0", 51 | "typescript": "^4.9.4" 52 | }, 53 | "dependencies": {} 54 | } 55 | -------------------------------------------------------------------------------- /src/ei.ts: -------------------------------------------------------------------------------- 1 | const COLON = 58 2 | const WILDCARD = 42 3 | 4 | interface RadixNode { 5 | part: string 6 | static?: Record 7 | children: Map> 8 | store?: T 9 | param?: string 10 | } 11 | 12 | const unionChars = (target: string, value: string) => { 13 | let unioned = '' 14 | 15 | for (let i = 0; i < target.length; i++) { 16 | if (value[i] === target[i]) unioned += value[i] 17 | else break 18 | } 19 | 20 | return unioned 21 | } 22 | 23 | const createNode = ({ 24 | children = null, 25 | part = '', 26 | static: staticChildren = null 27 | }: { 28 | children?: Record> | null 29 | part?: string 30 | static?: Record | null 31 | } = {}): RadixNode => { 32 | const node: RadixNode = Object.create(null) 33 | 34 | if (part) node.part = part 35 | 36 | node.children = new Map() 37 | if (children) 38 | for (const child of Object.keys(children)) 39 | node.children.set(+child, children[+child]) 40 | 41 | if (staticChildren) node.static = staticChildren 42 | 43 | return node 44 | } 45 | 46 | const createParamNode = ( 47 | param: string, 48 | children: Record> | null = null, 49 | statiChildren: Record | null = null 50 | ): RadixNode => { 51 | const node: RadixNode = Object.create(null) 52 | node.param = param 53 | 54 | node.children = new Map() 55 | if (children) 56 | for (const child of Object.keys(children)) 57 | node.children.set(+child, children[+child]) 58 | 59 | if (statiChildren) node.static = statiChildren 60 | 61 | return node 62 | } 63 | 64 | export class Raikiri { 65 | root: Record> = {} 66 | history: [string, string, T][] = [] 67 | 68 | add(method: string, path: string, store: T) { 69 | this.history.push([method, path, store]) 70 | 71 | if (!(method in this.root)) this.root[method] = createNode() 72 | 73 | let node = this.root[method] 74 | const paths: string[] = [''] 75 | 76 | path.split('/').forEach((part) => { 77 | if (!part) return 78 | 79 | if (part.startsWith(':') || part.startsWith('*')) { 80 | paths.push(part) 81 | paths.push('/') 82 | return 83 | } 84 | 85 | paths[paths.length - 1] += `${part}/` 86 | }) 87 | 88 | if (!path.endsWith('/') && path !== '/') { 89 | paths[paths.length - 1] = paths[paths.length - 1].slice( 90 | 0, 91 | paths[paths.length - 1].length - 1 92 | ) 93 | } 94 | 95 | if (paths.length > 1 && paths[paths.length - 1] === '') paths.pop() 96 | 97 | // Single path is always static 98 | if (paths.length === 1) { 99 | const staticPath = '/' + paths[0] 100 | 101 | if (!node.static) node.static = {} 102 | node.static[staticPath] = store 103 | 104 | return 105 | } 106 | 107 | // Catch all root wildcard 108 | if (paths[1] === '*' && paths[0] === '/' && paths.length === 2) { 109 | const wildcardNode = createNode() 110 | wildcardNode.store = store 111 | this.root[method].children.set(WILDCARD, wildcardNode) 112 | 113 | return 114 | } 115 | 116 | // console.log('>', path) 117 | operation: for (let i = 0; i < paths.length; i++) { 118 | const path = paths[i] 119 | const isLast = i === paths.length - 1 120 | 121 | for (const key of node.children.keys()) { 122 | const keyNode = node.children.get(key)! 123 | const keyPart = keyNode.part 124 | 125 | const unioned = unionChars(node.part, path) 126 | 127 | if (unioned === path) { 128 | if (node.part !== path) { 129 | const migrateNode = { ...node } 130 | migrateNode.part = node.part.slice(path.length + 1) 131 | 132 | const newNode = createNode({ 133 | children: { 134 | [node.part.charCodeAt(path.length)]: migrateNode 135 | } 136 | }) 137 | 138 | node.part = path 139 | node.children = newNode.children 140 | 141 | node 142 | } 143 | 144 | continue operation 145 | } 146 | 147 | if ( 148 | !unioned && 149 | (path.charCodeAt(0) === COLON || 150 | path.charCodeAt(0) === WILDCARD) 151 | ) 152 | continue 153 | 154 | if (unioned) { 155 | const newKey = path.charCodeAt(unioned.length) 156 | 157 | const migrateNode = node.children.get(key)! 158 | const migratePart = node.part.slice(unioned.length + 1) 159 | const migrateKey = node.part.charCodeAt(unioned.length) 160 | 161 | const needMigrate = node.part !== unioned 162 | 163 | if (needMigrate) { 164 | const newChildNode = createNode({ 165 | part: path.slice(unioned.length + 1) 166 | }) 167 | 168 | node.part = unioned 169 | node.children.set(newKey, newChildNode) 170 | 171 | node.children.set( 172 | migrateKey, 173 | createNode({ 174 | part: migratePart, 175 | children: { 176 | [key]: migrateNode 177 | } 178 | }) 179 | ) 180 | 181 | node.children.delete(key) 182 | node = newChildNode 183 | } else { 184 | const candidate = node.children.get(newKey)! 185 | 186 | // key has same prefix 187 | if (candidate) { 188 | const left = path.slice(unioned.length + 1) 189 | 190 | // key is duplicated 191 | if (left === candidate.part) { 192 | node = candidate 193 | continue operation 194 | } 195 | 196 | if (!candidate.part) { 197 | node = candidate.children.get(key)! 198 | continue operation 199 | } 200 | 201 | const migrateKey = candidate.part.charCodeAt(0) 202 | 203 | const innerUnioned = unionChars( 204 | left, 205 | candidate.part 206 | ) 207 | 208 | candidate.part = candidate.part.slice( 209 | innerUnioned.length 210 | ) 211 | 212 | const newNode = createNode({ 213 | part: left.slice(innerUnioned.length) 214 | }) 215 | 216 | node.children.set( 217 | newKey, 218 | createNode({ 219 | part: left.slice(0, -1), 220 | children: { 221 | [candidate.part.charCodeAt( 222 | innerUnioned.length - 1 223 | )]: candidate, 224 | [left.charCodeAt( 225 | innerUnioned.length - 1 226 | )]: newNode 227 | } 228 | }) 229 | ) 230 | 231 | node = newNode 232 | 233 | continue operation 234 | } 235 | 236 | if (isLast) break 237 | 238 | const newChildNode = createNode({ 239 | part: path.slice(unioned.length + 1) 240 | }) 241 | 242 | node.children.set(newKey, newChildNode) 243 | 244 | node = newChildNode 245 | } 246 | } else if (node.part) { 247 | const newChildNode = createNode({ 248 | part: path.slice(1) 249 | }) 250 | 251 | const migrateNode = { ...node } 252 | migrateNode.part = migrateNode.part.slice(1) 253 | 254 | const newNode = createNode({ 255 | children: { 256 | [node.part.charCodeAt(0)]: migrateNode, 257 | [path.charCodeAt(0)]: newChildNode 258 | } 259 | }) 260 | 261 | node.part = '' 262 | node.children = newNode.children 263 | node = newChildNode 264 | } else { 265 | if (node.children.size) { 266 | if (!node.children.has(path.charCodeAt(0))) 267 | node.children.set( 268 | path.charCodeAt(0), 269 | createNode({ 270 | part: path.slice(1) 271 | }) 272 | ) 273 | 274 | node = node.children.get(path.charCodeAt(0))! 275 | } else { 276 | const newChildNode = createNode({ 277 | part: path.slice(1) 278 | }) 279 | 280 | node.children.set(path.charCodeAt(0), newChildNode) 281 | 282 | node = newChildNode 283 | } 284 | } 285 | 286 | continue operation 287 | } 288 | 289 | if (path.includes(':')) { 290 | if (node.children.has(COLON)) { 291 | node = node.children.get(COLON)! 292 | continue 293 | } 294 | 295 | // Is : 296 | node.children.set(COLON, createParamNode(path.slice(1))) 297 | node = node.children.get(COLON)! 298 | } else if (path.includes('*')) { 299 | node.children.set(WILDCARD, createNode()) 300 | node = node.children.get(WILDCARD)! 301 | 302 | break operation 303 | } else { 304 | if (isLast) { 305 | if (!node.static) node.static = {} 306 | node.static[path] = store 307 | 308 | return 309 | } 310 | 311 | node.part = path 312 | } 313 | 314 | continue 315 | } 316 | 317 | node.store = store 318 | } 319 | 320 | remove(_method: string, _path: string) { 321 | const router = new Raikiri() 322 | 323 | for (let i = 0; i < this.history.length; i++) { 324 | const [method, path, store] = this.history[i] 325 | 326 | if (method === _method && path === _path) continue 327 | 328 | router.add(method, path, store) 329 | } 330 | 331 | this.history = router.history 332 | this.root = router.root 333 | } 334 | 335 | match(method: string, path: string) { 336 | const node = this.root[method] 337 | if (!node) return 338 | 339 | const root = node.static?.[path] 340 | if (root) 341 | return { 342 | store: root, 343 | params: {} 344 | } 345 | 346 | return iterateFirst(path.slice(1), node, {}) 347 | } 348 | 349 | private _m(method: string, path: string) { 350 | const node = this.root[method] 351 | if (!node) return 352 | 353 | return iterateFirst(path.slice(1), node, {}) 354 | } 355 | } 356 | 357 | const iterateFirst = ( 358 | path: string, 359 | node: RadixNode, 360 | params: Record 361 | ): 362 | | { 363 | store: T 364 | params: Record 365 | } 366 | | undefined => { 367 | const child = node.children.get(path.charCodeAt(node.part?.length)) 368 | 369 | if (node.part && path.slice(0, node.part.length) !== node.part) return 370 | 371 | if (!child) { 372 | const dynamic = node.children.get(COLON) 373 | if (dynamic) { 374 | const nextSlash = path.indexOf('/', node.part.length) 375 | 376 | if (nextSlash === -1) { 377 | params[dynamic.param!] = path.slice(node.part.length) 378 | 379 | if (dynamic.store) 380 | return { 381 | store: dynamic.store!, 382 | params 383 | } 384 | else return 385 | } 386 | 387 | params[dynamic.param!] = path.slice(node.part.length, nextSlash) 388 | 389 | return iterate( 390 | path.slice(nextSlash), 391 | node.children.get(COLON)!, 392 | params 393 | ) 394 | } else if (node.children.has(WILDCARD)) { 395 | params['*'] = path.slice(node.part.length) 396 | 397 | const store = node.children.get(WILDCARD)!.store 398 | 399 | if (store) 400 | return { 401 | store, 402 | params 403 | } 404 | else return 405 | } 406 | 407 | if (node.store) { 408 | return { 409 | store: node.store, 410 | params 411 | } 412 | } else return 413 | } 414 | 415 | return iterate(path.slice(node.part.length + 1), child, params) 416 | } 417 | 418 | const iterate = ( 419 | path: string, 420 | node: RadixNode, 421 | params: Record 422 | ): 423 | | { 424 | store: T 425 | params: Record 426 | } 427 | | undefined => { 428 | const store = node.static?.[path] 429 | if (store) 430 | return { 431 | store, 432 | params 433 | } 434 | 435 | if (node.part && path.slice(0, node.part.length) !== node.part) return 436 | 437 | const child = node.children.get(path.charCodeAt(node.part?.length)) 438 | 439 | if (!child) { 440 | const dynamic = node.children.get(COLON) 441 | if (dynamic) { 442 | const nextSlash = path.indexOf('/', node.part?.length) 443 | 444 | if (nextSlash === -1) { 445 | params[dynamic.param!] = path.slice(node.part?.length) 446 | 447 | if (dynamic.store) 448 | return { 449 | store: dynamic.store, 450 | params 451 | } 452 | return 453 | } 454 | 455 | params[dynamic.param!] = path.slice(node.part?.length, nextSlash) 456 | 457 | return iterate( 458 | path.slice(nextSlash), 459 | node.children.get(COLON)!, 460 | params 461 | ) 462 | } else if (node.children.has(WILDCARD)) { 463 | if (node.part) params['*'] = path.slice(node.part?.length) 464 | else params['*'] = path 465 | 466 | const store = node.children.get(WILDCARD)!.store 467 | 468 | if (store) 469 | return { 470 | store, 471 | params 472 | } 473 | else return 474 | } 475 | 476 | if (node.store) { 477 | return { 478 | store: node.store, 479 | params 480 | } 481 | } else return 482 | } 483 | 484 | return iterate(path.slice(node.part.length + 1), child, params) 485 | } 486 | 487 | export default Raikiri 488 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Raikiri as Raikiri01 } from './mei' 2 | 3 | import { Raikiri } from './ei' 4 | export { Raikiri } from './ei' 5 | 6 | export default Raikiri -------------------------------------------------------------------------------- /src/mei.ts: -------------------------------------------------------------------------------- 1 | const COLON = 58 2 | const WILDCARD = 42 3 | 4 | interface RadixNode { 5 | children: Map> 6 | static?: Record 7 | store?: T 8 | name?: string 9 | } 10 | 11 | const createNode = ( 12 | children: Record> | null = null 13 | ): RadixNode => { 14 | const node: RadixNode = Object.create(null) 15 | 16 | node.children = new Map() 17 | if (children) 18 | for (const child in children) node.children.set(child, children[child]) 19 | 20 | return node 21 | } 22 | 23 | export class Raikiri { 24 | root: Record> = {} 25 | 26 | add(method: string, path: string, store: T) { 27 | if (!(method in this.root)) this.root[method] = createNode() 28 | 29 | let node = this.root[method] 30 | const paths: string[] = ['/'] 31 | 32 | path.split('/').forEach((part) => { 33 | if (!part) return 34 | 35 | if (part.startsWith(':') || part.startsWith('*')) { 36 | paths.push(part) 37 | paths.push('/') 38 | return 39 | } 40 | 41 | paths[paths.length - 1] += `${part}/` 42 | }) 43 | 44 | if (!path.endsWith('/') && path !== '/') { 45 | paths[paths.length - 1] = paths[paths.length - 1].slice( 46 | 0, 47 | paths[paths.length - 1].length - 1 48 | ) 49 | } 50 | 51 | if (paths.length > 1 && paths[paths.length - 1] === '') paths.pop() 52 | 53 | // Single path is always static 54 | if (paths.length === 1) { 55 | if (!node.static) node.static = {} 56 | node.static[paths[0]] = store 57 | 58 | return 59 | } 60 | 61 | // Catch all root wildcard 62 | if (paths[1] === '*' && paths[0] === '/' && paths.length === 2) { 63 | const wildcardNode = createNode() 64 | wildcardNode.store = store 65 | this.root[method].children.set(WILDCARD, wildcardNode) 66 | 67 | return 68 | } 69 | 70 | for (let i = 0; i < paths.length; i++) { 71 | const path = paths[i] 72 | const isLast = i === paths.length - 1 73 | let iterated = false 74 | 75 | for (const key of node.children.keys()) { 76 | let prefix: string | number = '' 77 | 78 | if (typeof key === 'string') 79 | for (const [charIndex, charKey] of key 80 | .split('') 81 | .entries()) { 82 | if (path[charIndex] === charKey) prefix += charKey 83 | else break 84 | } 85 | else prefix = 58 86 | 87 | if (!prefix) { 88 | iterated = true 89 | break 90 | } 91 | 92 | const prefixLength = 93 | typeof prefix === 'number' ? 1 : prefix.length 94 | 95 | if (node.children.has(prefix)) { 96 | iterated = true 97 | node = node.children.get(prefix)! 98 | 99 | const fracture = path.slice(prefixLength) 100 | 101 | if (prefix !== path && prefix !== COLON) { 102 | const part = path.slice(prefixLength) 103 | 104 | iterated = true 105 | 106 | if (!node.children.has(fracture)) 107 | if ( 108 | node.children.has(COLON) || 109 | node.children.has(WILDCARD) 110 | ) { 111 | // Move static above colon and wildcard 112 | const ordered = new Map() 113 | 114 | ordered.set(part, createNode()) 115 | 116 | for (const [ 117 | key, 118 | value 119 | ] of node.children.entries()) 120 | ordered.set(key, value) 121 | 122 | node.children = ordered 123 | } else { 124 | node.children.set(part, createNode()) 125 | } 126 | 127 | if (isLast) { 128 | if (!node.static) node.static = {} 129 | node.static[part] = store 130 | } 131 | 132 | node = node.children.get(fracture)! 133 | } 134 | 135 | break 136 | } 137 | 138 | if (prefix && prefix !== '/' && prefix !== path) { 139 | iterated = true 140 | 141 | // Newly created path: is empty 142 | const branch = path.slice(prefixLength) 143 | const migrate = 144 | typeof key === 'number' ? key : key.slice(prefixLength) 145 | 146 | const branchNode = createNode({ 147 | [migrate]: node.children.get(key)!, 148 | [branch]: createNode() 149 | }) 150 | 151 | node.children.delete(key) 152 | 153 | if (migrate !== COLON) { 154 | const migrateStore = 155 | branchNode.children.get(migrate)?.store! 156 | 157 | if (migrateStore) { 158 | if (!branchNode.static) branchNode.static = {} 159 | branchNode.static[migrate] = migrateStore 160 | } 161 | } 162 | 163 | node.children.set(prefix, branchNode) 164 | 165 | if (isLast && prefix !== COLON) { 166 | if (!node.static) node.static = {} 167 | node.static[path] = store 168 | } 169 | } 170 | } 171 | 172 | if (iterated || !path) continue 173 | 174 | if (path.startsWith(':')) { 175 | const paramNode = createNode() 176 | paramNode.name = path.slice(1) 177 | node.children.set(COLON, paramNode) 178 | node = paramNode 179 | } else if (isLast && path === '*') { 180 | node.children.set(WILDCARD, createNode()) 181 | node = node.children.get(WILDCARD)! 182 | } else { 183 | if (isLast) { 184 | if (!node.static) node.static = {} 185 | node.static[path] = store 186 | } 187 | 188 | // Leaf for possible nested dynamic path 189 | node.children.set(path, createNode()) 190 | node = node.children.get(path)! 191 | } 192 | } 193 | 194 | node.store = store 195 | } 196 | 197 | match = (method: string, path: string) => { 198 | let node = this.root[method] 199 | if (!node) return 200 | 201 | const root = node.static?.[path] 202 | if (root) 203 | return { 204 | store: root, 205 | params: {} 206 | } 207 | 208 | const params: { [key: string]: string } = {} 209 | let depth = 0 210 | 211 | find: while (true) { 212 | const children = node.children 213 | 214 | for (const fracture of children.keys()) { 215 | if (typeof fracture === 'number') { 216 | if (fracture === COLON) { 217 | node = children.get(COLON)! 218 | 219 | const index = path.indexOf('/', depth + 1) 220 | const value = 221 | index === -1 222 | ? path.slice(depth) 223 | : path.slice(depth, index) 224 | 225 | params[node.name!] = value 226 | 227 | depth += value.length 228 | } 229 | // Since it's special characters and not : then it's wildcard 230 | else { 231 | params['*'] = path.slice(depth) 232 | 233 | return { 234 | store: children.get(WILDCARD)!.store, 235 | params 236 | } 237 | } 238 | 239 | // Special characters should all be matched above, abort if not 240 | continue find 241 | } 242 | 243 | const current = depth + fracture.length 244 | const part = path.slice(depth, current) 245 | 246 | const root = node.static?.[part] 247 | if (root) { 248 | /** 249 | * Suppose there are: 250 | * - /id/:id/name/:name 251 | * - /id/:id/name/a 252 | * 253 | * Then if you pass in 254 | * - /id/1/name/ame 255 | * 256 | * Since fracture is /a, path is /ame then part become /a 257 | * It will be matched in node.static[part] 258 | * 259 | * This means we need to check path left by using current <= clear 260 | */ 261 | if (current <= path.length - 2) continue 262 | 263 | return { 264 | store: root, 265 | params 266 | } 267 | } 268 | 269 | if (children.has(part)) { 270 | node = children.get(part)! 271 | depth += part.length 272 | continue find 273 | } 274 | } 275 | 276 | break 277 | } 278 | 279 | if (node.store) return { store: node.store, params } 280 | } 281 | } 282 | 283 | export default Raikiri 284 | -------------------------------------------------------------------------------- /test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { Raikiri } from '../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | 5 | const router = new Raikiri() 6 | router.add('GET', '/v1/genres', '/g') 7 | router.add('GET', '/v1/genres/:id', '/g/:id') 8 | router.add('GET', '/v1/statuse', '/s') 9 | router.add('GET', '/v1/statuse/:id', '/s/:id') 10 | 11 | describe('Add', () => { 12 | it('Clean up path mangling', () => { 13 | expect(router.match('GET', '/v1/statuse/1')).toEqual({ 14 | store: '/s/:id', 15 | params: { 16 | id: '1' 17 | } 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Raikiri } from '../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | import { execPath } from 'process' 5 | 6 | const router = new Raikiri() 7 | router.add('GET', '/abc', '/abc') 8 | router.add('GET', '/id/:id/book', 'book') 9 | router.add('GET', '/id/:id/bowl', 'bowl') 10 | 11 | router.add('GET', '/', '/') 12 | router.add('GET', '/id/:id', '/id/:id') 13 | router.add('GET', '/id/:id/abc/def', '/id/:id/abc/def') 14 | router.add('GET', '/id/:id/abd/efd', '/id/:id/abd/efd') 15 | router.add('GET', '/id/:id/name/:name', '/id/:id/name/:name') 16 | router.add('GET', '/id/:id/name/a', '/id/:id/name/a') 17 | router.add('GET', '/dynamic/:name/then/static', '/dynamic/:name/then/static') 18 | router.add('GET', '/deep/nested/route', '/deep/nested/route') 19 | router.add('GET', '/rest/*', '/rest/*') 20 | 21 | describe('Raikiri', () => { 22 | it('match root', () => { 23 | expect(router.match('GET', '/')).toEqual({ 24 | store: '/', 25 | params: {} 26 | }) 27 | }) 28 | 29 | it('get path parameter', () => { 30 | expect(router.match('GET', '/id/1')).toEqual({ 31 | store: '/id/:id', 32 | params: { 33 | id: '1' 34 | } 35 | }) 36 | }) 37 | 38 | it('get multiple path parameters', () => { 39 | expect(router.match('GET', '/id/1/name/name')).toEqual({ 40 | store: '/id/:id/name/:name', 41 | params: { 42 | id: '1', 43 | name: 'name' 44 | } 45 | }) 46 | }) 47 | 48 | it('get deep static route', () => { 49 | expect(router.match('GET', '/deep/nested/route')).toEqual({ 50 | store: '/deep/nested/route', 51 | params: {} 52 | }) 53 | }) 54 | 55 | it('match wildcard', () => { 56 | expect(router.match('GET', '/rest/a/b/c')).toEqual({ 57 | store: '/rest/*', 58 | params: { 59 | '*': 'a/b/c' 60 | } 61 | }) 62 | }) 63 | 64 | it('handle mixed dynamic and static', () => { 65 | expect(router.match('GET', '/dynamic/param/then/static')).toEqual({ 66 | store: '/dynamic/:name/then/static', 67 | params: { 68 | name: 'param' 69 | } 70 | }) 71 | }) 72 | 73 | it('handle static path in dynamic', () => { 74 | expect(router.match('GET', '/id/1/name/a')).toEqual({ 75 | store: '/id/:id/name/a', 76 | params: { 77 | id: '1' 78 | } 79 | }) 80 | }) 81 | 82 | it('handle dynamic as fallback', () => { 83 | expect(router.match('GET', '/id/1/name/ame')).toEqual({ 84 | store: '/id/:id/name/:name', 85 | params: { 86 | id: '1', 87 | name: 'ame' 88 | } 89 | }) 90 | }) 91 | 92 | it('wildcard on root path', () => { 93 | const router = new Raikiri() 94 | 95 | router.add('GET', '/a/b', 'ok') 96 | router.add('GET', '/*', 'all') 97 | 98 | expect(router.match('GET', '/a/b/c/d')).toEqual({ 99 | store: 'all', 100 | params: { 101 | '*': 'a/b/c/d' 102 | } 103 | }) 104 | 105 | expect(router.match('GET', '/')).toEqual({ 106 | store: 'all', 107 | params: { 108 | '*': '' 109 | } 110 | }) 111 | }) 112 | 113 | it('can overwrite wildcard', () => { 114 | const router = new Raikiri() 115 | 116 | router.add('GET', '/', 'ok') 117 | router.add('GET', '/*', 'all') 118 | 119 | expect(router.match('GET', '/a/b/c/d')).toEqual({ 120 | store: 'all', 121 | params: { 122 | '*': 'a/b/c/d' 123 | } 124 | }) 125 | 126 | expect(router.match('GET', '/')).toEqual({ 127 | store: 'ok', 128 | params: {} 129 | }) 130 | }) 131 | 132 | it('handle trailing slash', () => { 133 | const router = new Raikiri() 134 | 135 | router.add('GET', '/abc/def', 'A') 136 | router.add('GET', '/abc/def/', 'A') 137 | 138 | expect(router.match('GET', '/abc/def')).toEqual({ 139 | store: 'A', 140 | params: {} 141 | }) 142 | 143 | expect(router.match('GET', '/abc/def/')).toEqual({ 144 | store: 'A', 145 | params: {} 146 | }) 147 | }) 148 | 149 | it('handle static prefix wildcard', () => { 150 | const router = new Raikiri() 151 | router.add('GET', '/a/b', 'ok') 152 | router.add('GET', '/*', 'all') 153 | 154 | expect(router.match('GET', '/a/b/c/d')).toEqual({ 155 | store: 'all', 156 | params: { 157 | '*': 'a/b/c/d' 158 | } 159 | }) 160 | 161 | expect(router.match('GET', '/')).toEqual({ 162 | store: 'all', 163 | params: { 164 | '*': '' 165 | } 166 | }) 167 | }) 168 | 169 | // ? https://github.com/SaltyAom/raikiri/issues/2 170 | // Migrate from mei to ei should work 171 | it('dynamic root', () => { 172 | const router = new Raikiri() 173 | router.add('GET', '/', 'root') 174 | router.add('GET', '/:param', 'it worked') 175 | 176 | expect(router.match('GET', '/')).toEqual({ 177 | store: 'root', 178 | params: {} 179 | }) 180 | 181 | expect(router.match('GET', '/bruh')).toEqual({ 182 | store: 'it worked', 183 | params: { 184 | param: 'bruh' 185 | } 186 | }) 187 | }) 188 | 189 | it('handle wildcard without static fallback', () => { 190 | const router = new Raikiri() 191 | router.add('GET', '/public/*', 'foo') 192 | router.add('GET', '/public-aliased/*', 'foo') 193 | 194 | expect(router.match('GET', '/public/takodachi.png')?.params['*']).toBe( 195 | 'takodachi.png' 196 | ) 197 | expect( 198 | router.match('GET', '/public/takodachi/ina.png')?.params['*'] 199 | ).toBe('takodachi/ina.png') 200 | }) 201 | 202 | it('restore mangled path', () => { 203 | const router = new Raikiri() 204 | 205 | router.add('GET', '/users/:userId', '/users/:userId') 206 | router.add('GET', '/game', '/game') 207 | router.add('GET', '/game/:gameId/state', '/game/:gameId/state') 208 | router.add('GET', '/game/:gameId', '/game/:gameId') 209 | 210 | expect(router.match('GET', '/game/1/state')?.store).toBe( 211 | '/game/:gameId/state' 212 | ) 213 | expect(router.match('GET', '/game/1')?.store).toBe('/game/:gameId') 214 | }) 215 | 216 | it('should be a ble to register param after same prefix', () => { 217 | const router = new Raikiri() 218 | 219 | router.add('GET', '/api/abc/view/:id', '/api/abc/view/:id') 220 | router.add('GET', '/api/abc/:type', '/api/abc/:type') 221 | 222 | expect(router.match('GET', '/api/abc/type')).toEqual({ 223 | store: '/api/abc/:type', 224 | params: { 225 | type: 'type' 226 | } 227 | }) 228 | 229 | expect(router.match('GET', '/api/abc/view/1')).toEqual({ 230 | store: '/api/abc/view/:id', 231 | params: { 232 | id: '1' 233 | } 234 | }) 235 | }) 236 | 237 | it('use exact match for part', () => { 238 | const router = new Raikiri() 239 | 240 | router.add('GET', '/api/search/:term', '/api/search/:term') 241 | router.add('GET', '/api/abc/view/:id', '/api/abc/view/:id') 242 | router.add('GET', '/api/abc/:type', '/api/abc/:type') 243 | 244 | expect(router.match('GET', '/api/abc/type')?.store).toBe( 245 | '/api/abc/:type' 246 | ) 247 | expect(router.match('GET', '/api/awd/type')).toBe(undefined) 248 | }) 249 | }) 250 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["ESNext", "DOM", "ScriptHost"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "CommonJS", /* Specify what module code is generated. */ 29 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./dist/cjs", /* Specify an output folder for all emitted files. */ 53 | "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 102 | }, 103 | "include": ["src/**/*"] 104 | } 105 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["ESNext", "DOM", "ScriptHost"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ES2022", /* Specify what module code is generated. */ 29 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 53 | "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 102 | }, 103 | "include": ["src/**/*"] 104 | } 105 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["ESNext", "DOM", "ScriptHost"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ESNext", /* Specify what module code is generated. */ 29 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./dist", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": false, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 102 | }, 103 | // "include": ["src/**/*"] 104 | } 105 | --------------------------------------------------------------------------------