Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@ditojs/router

Package Overview
Dependencies
Maintainers
4
Versions
322
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ditojs/router - npm Package Compare versions

Comparing version 1.24.0 to 1.25.0

6

package.json
{
"name": "@ditojs/router",
"version": "1.24.0",
"version": "1.25.0",
"type": "module",

@@ -24,3 +24,3 @@ "description": "Dito.js Router – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",

"dependencies": {
"@ditojs/utils": "^1.24.0"
"@ditojs/utils": "^1.25.0"
},

@@ -34,4 +34,4 @@ "keywords": [

],
"gitHead": "0ca96ce0335509daa9ebbac278ae2d346015baad",
"gitHead": "29e7f8d8c613c48a856d90d1ee876e85c8d84c08",
"readme": "# Dito.js Router\n\nDito.js is a declarative and modern web framework with a focus on API driven\ndevelopment, based on Koa.js, Objection.js and Vue.js\n\nReleased in 2018 under the MIT license, with support by https://lineto.com/\n\nDito.js Router is a high performance, tree-based, framework agnostic HTTP\nrouter, based on [trek-router](https://github.com/trekjs/router), which in turn\nis inspired by [Echo](https://github.com/labstack/echo)'s Router.\n\n## How does it work?\n\nThe router relies on a tree structure which makes heavy use of common\nprefixes, essentially a [prefix tree](https://en.wikipedia.org/wiki/Trie).\n\n## Usage\n\n```js\nimport Koa from 'koa'\nimport compose from 'koa-compose'\nimport Router from '@ditojs/router'\n\nconst app = new Koa()\nconst router = new Router()\n\n// static route\nrouter.get('/folders/files/bolt.gif', ctx => {\n ctx.body = `this ain't no GIF!`\n})\n\n// param route\nrouter.get('/users/:id', ctx => {\n ctx.body = `requesting user ${ctx.params.id}`\n})\n\n// match-any route\nrouter.get('/books/*', ctx => {\n ctx.body = `sub-route: ${ctx.params['*']}`\n})\n\n// Handler found\nlet { handler, params } = router.find('get', '/users/233')\nconsole.log(handler) // ctx => { ... }\n\n// Entry not Found\nlet { handler, params } = router.find('get', '/photos/233')\nconsole.log(handler) // null\n\n// Install router middleware\napp.use(async (ctx, next) => {\n const { method, path } = ctx\n const result = router.find(method, path)\n const { handler, params } = result\n if (handler) {\n ctx.params = params || {}\n return handler(ctx, next)\n } else {\n try {\n await next()\n } finally {\n if (ctx.body === undefined && ctx.status === 404) {\n ctx.status = result.status || 404\n if (ctx.status !== 404 && result.allowed) {\n ctx.set('Allow', result.allowed.join(', '))\n }\n }\n }\n }\n})\n\napp.listen(4040, () => console.log('Koa app listening on 4040'))\n```\n"
}
import { getCommonOffset } from '@ditojs/utils'
// Node Types:
const TYPE_STATIC = 0
const TYPE_PARAM = 1
const TYPE_MATCH_ANY = 2
const TYPE_STATIC = 0 // /static/patch
const TYPE_PARAM = 1 // /prefix/:param/suffix
const TYPE_PLACEHOLDER = 2 // /prefix/*/suffix (a.k.a shallow wildcard)
const TYPE_MATCH_ANY = 3 // /prefix/**/suffix (a.k.a deep wildcard)
// Char Codes
const CHAR_PARAM = ':'.charCodeAt(0)
const CHAR_MATCH_ANY = '*'.charCodeAt(0)
const CHAR_SLASH = '/'.charCodeAt(0)
const CHAR_SLASH = '/'[0]
const CHAR_PARAM = ':'[0]
const CHAR_WILDCARD = '*'[0]

@@ -19,57 +20,53 @@ export default class Node {

initialize(
type = TYPE_STATIC,
prefix = '/',
type = TYPE_STATIC,
children = [],
handler = null,
paramNames = null
parameters = null,
handler = null
) {
this.label = prefix.charCodeAt(0)
this.type = type
this.label = prefix[0]
this.prefix = prefix
this.type = type
this.children = children
this.parameters = parameters
this.handler = handler
this.paramNames = paramNames
this.paramName = null
this.paramKey = null
this.hasMatchAny = false
}
addChild(child) {
this.children.push(child)
}
findChild(label, type) {
for (const child of this.children) {
if (child.label === label && child.type === type) {
return child
}
setup(parameters, handler) {
this.parameters = parameters
this.handler = handler
if (this.type === TYPE_MATCH_ANY) {
parameters.setupPathPattern(this.prefix)
}
}
findChildWithLabel(label) {
for (const child of this.children) {
if (child.label === label) {
return child
}
}
addChild(child) {
this.children.push(child)
this.hasMatchAny ||= child.type === TYPE_MATCH_ANY
}
findChildWithType(type) {
for (const child of this.children) {
if (child.type === type) {
return child
}
}
}
add(path, handler) {
const paramNames = []
const parameters = new Parameters()
for (let pos = 0, length = path.length; pos < length; pos++) {
const ch = path.charCodeAt(pos)
if (ch === CHAR_PARAM) {
this.insert(path.slice(0, pos), TYPE_STATIC)
pos++ // Skip colon.
const ch = path[pos]
if (ch === CHAR_WILDCARD && path[pos + 1] === CHAR_WILDCARD) {
// Deep wildcard (**): matches any path, with a optional suffix:
this.insert(TYPE_STATIC, path.slice(0, pos))
pos += 2 // Skip '**'.
this.insert(TYPE_MATCH_ANY, path, parameters, handler)
return
} else if (ch === CHAR_PARAM || ch === CHAR_WILDCARD) {
// Param (:param) or shallow wildcard (*):
const isWildcard = ch === CHAR_WILDCARD
const type = isWildcard ? TYPE_PLACEHOLDER : TYPE_PARAM
this.insert(TYPE_STATIC, path.slice(0, pos))
pos++ // Skip colon or wildcard.
const start = pos
// Move pos to the next occurrence of the slash or the end:
pos += path.slice(pos).match(/^([^/]*)/)[1].length
paramNames.push(path.slice(start, pos))
parameters.add(
isWildcard ? parameters.getPlaceholderKey() : path.slice(start, pos)
)
// Chop out param name from path, but keep colon.

@@ -80,43 +77,41 @@ path = path.slice(0, start) + path.slice(pos)

if (start === length) {
return this.insert(path, TYPE_PARAM, paramNames, handler)
this.insert(type, path, parameters, handler)
return
}
pos = start
this.insert(path.slice(0, pos), TYPE_PARAM, paramNames)
} else if (ch === CHAR_MATCH_ANY) {
this.insert(path.slice(0, pos), TYPE_STATIC)
paramNames.push('*')
return this.insert(path, TYPE_MATCH_ANY, paramNames, handler)
this.insert(type, path.slice(0, pos), parameters)
}
}
this.insert(path, TYPE_STATIC, paramNames, handler)
this.insert(TYPE_STATIC, path, parameters, handler)
}
insert(path, type, paramNames, handler) {
insert(type, prefix, parameters = null, handler = null) {
let current = this
while (true) {
// Find the position where the path and the node's prefix start diverging.
const pos = getCommonOffset(current.prefix, path)
const { prefix } = current
if (pos < prefix.length) {
const curPrefix = current.prefix
const pos = getCommonOffset(curPrefix, prefix)
if (pos < curPrefix.length) {
// Split node
const node = new Node(
prefix.slice(pos),
current.type,
curPrefix.slice(pos),
current.children,
current.handler,
current.paramNames
current.parameters,
current.handler
)
// Reset parent node and add new node as child to it:
current.initialize(prefix.slice(0, pos))
// Reset parent node to a static and add new node as child to it:
current.initialize(TYPE_STATIC, curPrefix.slice(0, pos))
current.addChild(node)
if (pos < path.length) {
if (pos < prefix.length) {
// Create child node
const node = new Node(path.slice(pos), type)
const node = new Node(type, prefix.slice(pos))
current.addChild(node)
current = node // Switch to child to set handler and paramNames
current = node // Switch to child to set handler and parameters
}
} else if (pos < path.length) {
path = path.slice(pos)
const child = current.findChildWithLabel(path.charCodeAt(0))
if (child !== undefined) {
} else if (pos < prefix.length) {
prefix = prefix.slice(pos)
const label = prefix[0]
const child = current.children.find(child => child.label === label)
if (child !== undefined && child.type !== TYPE_MATCH_ANY) {
// Go deeper

@@ -127,14 +122,13 @@ current = child

// Create child node
const node = new Node(path, type)
const node = new Node(type, prefix)
current.addChild(node)
current = node // Switch to child to set handler and paramNames
current = node // Switch to child to set handler and parameters
}
if (handler) {
current.handler = handler
current.paramNames = paramNames
current.setup(parameters, handler)
}
if (paramNames) {
if (parameters) {
// Remember the last entry from the list of param names that keeps
// growing during parsing as the name of the current node.
current.paramName = paramNames[paramNames.length - 1]
current.paramKey = parameters.getLastKey()
}

@@ -151,8 +145,3 @@ break

if (handler) {
// Convert paramNames and values to params.
const params = {}
let i = 0
for (const name of this.paramNames) {
params[name] = paramValues[i++]
}
const params = this.parameters.getObject(paramValues)
// Support HTTP status on found entries.

@@ -169,3 +158,3 @@ return { handler, params, status: 200 }

path = path.slice(prefixLength)
} else if (this.type !== TYPE_PARAM) {
} else if (this.type !== TYPE_PARAM && this.type !== TYPE_PLACEHOLDER) {
// If the path doesn't fully match the prefix, we only need to look

@@ -179,3 +168,6 @@ // further on param nodes, which can have overlapping static children.

// Static node
const staticChild = this.findChild(path.charCodeAt(0), TYPE_STATIC)
const label = path[0]
const staticChild = this.children.find(
child => child.type === TYPE_STATIC && child.label === label
)
if (staticChild) {

@@ -189,8 +181,8 @@ const result = staticChild.find(path, paramValues)

// Node not found
if (!fullMatch) {
return null
}
if (!fullMatch) return null
// Param node
const paramChild = this.findChildWithType(TYPE_PARAM)
// Param / placeholder node
const paramChild = this.children.find(
child => child.type === TYPE_PARAM || child.type === TYPE_PLACEHOLDER
)
if (paramChild) {

@@ -200,3 +192,3 @@ // Find the position of the next slash:

const max = path.length
while (pos < max && path.charCodeAt(pos) !== CHAR_SLASH) {
while (pos < max && path[pos] !== CHAR_SLASH) {
pos++

@@ -212,7 +204,11 @@ }

// Match-any node
const matchAnyChild = this.findChildWithType(TYPE_MATCH_ANY)
if (matchAnyChild) {
paramValues.push(path)
return matchAnyChild.find('', paramValues) // '' == End
// Match-any nodes
if (this.hasMatchAny) {
for (const child of this.children) {
const match = child.parameters?.matchPathPattern(path)
if (match) {
paramValues.push(...Object.values(match.groups))
return child.find('', paramValues) // '' == End
}
}
}

@@ -231,3 +227,3 @@

this.type === TYPE_PARAM
? `${this.prefix}${this.paramName}`
? `${this.prefix}${this.paramKey}`
: this.prefix

@@ -248,1 +244,76 @@ }${

}
class Parameters {
constructor() {
this.keys = []
this.pathPattern = null
this.matchAnyIndex = 0
this.placeholderIndex = 0
}
add(...keys) {
this.keys.push(...keys)
}
getMatchAnyKey() {
return `$$${this.matchAnyIndex++}`
}
getPlaceholderKey() {
return `$${this.placeholderIndex++}`
}
getLastKey() {
return this.keys[this.keys.length - 1]
}
matchPathPattern(path) {
return this.pathPattern?.exec(path)
}
setupPathPattern(path) {
// Replace all '**' with '.+?' and all '*' with '[^/]+'.
// Use named groups to merge multiple ** or * into one.
const pattern = []
const keys = []
for (const token of path.split('/')) {
if (token === '**') {
const key = this.getMatchAnyKey()
pattern.push(`(?<${key}>.+?)`)
keys.push(key)
} else if (token === '*') {
const key = this.getPlaceholderKey()
pattern.push(`(?<${key}>[^/]+)`)
keys.push(key)
} else if (token.startsWith(':')) {
const key = token.slice(1)
pattern.push(`(?<${key}>[^/]+)`)
keys.push(key)
} else {
pattern.push(token)
}
}
this.pathPattern = new RegExp(`^${pattern.join('/')}$`)
this.add(...keys)
}
getObject(values) {
// Convert parameters and values to a params object, but rename path pattern
// groups back to param names
const params = {}
let i = 0
for (const key of this.keys) {
const name = key.startsWith('$$')
? this.matchAnyIndex === 1
? '$$'
: key
: key.startsWith('$')
? this.placeholderIndex === 1
? '$'
: key
: key
params[name] = values[i++]
}
return params
}
}

@@ -124,4 +124,4 @@ import Router from './Router.js'

it('handles match-any nodes (catch-all / wildcard)', () => {
router.add('GET', '/static/*', handler)
it('handles match-any nodes (catch-all)', () => {
router.add('GET', '/static/**', handler)

@@ -131,3 +131,3 @@ expect(router.toString()).toBe(deindent`

└── static/ children=1
└── * handler() children=0
└── ** handler() children=0
`.trim())

@@ -144,3 +144,3 @@

expect(result.params).toEqual({
'*': 'js'
$$: 'js'
})

@@ -151,6 +151,102 @@

expect(result.params).toEqual({
'*': 'css'
$$: 'css'
})
})
it('handles match-any nodes (wildcard)', () => {
router.add('GET', '/prefix/*/suffix', handler)
expect(router.toString()).toBe(deindent`
/ children=1
└── prefix/ children=1
└── * children=1
└── /suffix handler() children=0
`.trim())
result = router.find('GET', '/prefix')
expect(result.handler).toBeUndefined()
result = router.find('GET', '/prefix/*')
expect(result.handler).toBeUndefined()
result = router.find('GET', '/prefix/hello/there')
expect(result.handler).toBeUndefined()
result = router.find('GET', '/prefix/hello/suffix')
expect(result.handler).toBe(handler)
expect(result.params).toEqual({
$: 'hello'
})
})
it('handles match-any nodes with suffixes', () => {
router.add('GET', '/static/**/suffix', handler)
router.add('GET', '/static/**/suffix/**/more', handler)
router.add('GET', '/static/**/suffix/*/1/:param/2', handler)
router.add('GET', '/static/**/suffix/*/1/:param/2/**/end', handler)
expect(router.toString()).toBe(deindent`
/ children=1
└── static/ children=4
├── **/suffix handler() children=0
├── **/suffix/**/more handler() children=0
├── **/suffix/*/1/:param/2 handler() children=0
└── **/suffix/*/1/:param/2/**/end handler() children=0
`.trim())
result = router.find('GET', '/static')
expect(result.handler).toBeUndefined()
result = router.find('GET', '/static/js')
expect(result.handler).toBeUndefined()
result = router.find('GET', '/static/one/prefix')
expect(result.handler).toBeUndefined()
result = router.find('GET', '/static/one/two/prefix')
expect(result.handler).toBeUndefined()
result = router.find('GET', '/static/one/suffix')
expect(result.handler).toBe(handler)
expect(result.params).toEqual({
$$: 'one'
})
result = router.find('GET', '/static/one/two/suffix')
expect(result.handler).toBe(handler)
expect(result.params).toEqual({
$$: 'one/two'
})
result = router.find('GET', '/static/one/two/suffix/three/four')
expect(result.handler).toBeUndefined()
result = router.find('GET', '/static/one/two/suffix/three/four/more')
expect(result.handler).toBe(handler)
expect(result.params).toEqual({
$$0: 'one/two',
$$1: 'three/four'
})
result = router.find('GET', '/static/one/two/suffix/three/1/bla/2')
expect(result.handler).toBe(handler)
expect(result.params).toEqual({
$$: 'one/two',
$: 'three',
param: 'bla'
})
result = router.find('GET', '/static/one/two/suffix/three/1/bla/3')
expect(result.handler).toBeUndefined()
result = router.find('GET', `/static/one/two/suffix/three/1/bla/2/what/ever/end`)
expect(result.handler).toBe(handler)
expect(result.params).toEqual({
$: 'three',
param: 'bla',
$$0: 'one/two',
$$1: 'what/ever'
})
})
it('handles resources', () => {

@@ -171,3 +267,3 @@ createRoutes(router, [

['/geocoder/:action', 'actionGeocoder'],
['/geocoder/*', 'anyGeocoder']
['/geocoder/**', 'anyGeocoder']
])

@@ -193,3 +289,3 @@

│ └── /echo echoGeocoder() children=0
└── * anyGeocoder() children=0
└── ** anyGeocoder() children=0
`.trim())

@@ -212,3 +308,3 @@ result = router.find('GET', '')

expect(result.handler.name).toBe('anyGeocoder')
expect(result.params).toEqual({ '*': 'delete/any' })
expect(result.params).toEqual({ $$: 'delete/any' })

@@ -218,3 +314,3 @@ result = router.find('GET', '/geocoder/any/action')

expect(result.handler.name).toBe('anyGeocoder')
expect(result.params).toEqual({ '*': 'any/action' })
expect(result.params).toEqual({ $$: 'any/action' })

@@ -281,3 +377,4 @@ result = router.find('GET', '/geocoder/exchange/trekjs')

['/users/new', 'newUser'],
['/users/nnw', 'newUser'],
['/users/noi', 'newUser'],
['/users/nei', 'newUser'],
['/users/:id', 'user'],

@@ -289,3 +386,3 @@ ['/users/:id/edit', 'editUser'],

['/users/:userId/books/:id', 'book'],
['/users/*', 'anyUser']
['/users/**', 'anyUser']
])

@@ -298,4 +395,6 @@

├── n children=2
│ ├── ew newUser() children=0
│ └── nw newUser() children=0
│ ├── e children=2
│ │ ├── w newUser() children=0
│ │ └── i newUser() children=0
│ └── oi newUser() children=0
├── :userId user() children=1

@@ -314,3 +413,3 @@ │ └── / children=4

│ └── :id book() children=0
└── * anyUser() children=0
└── ** anyUser() children=0
`.trim())

@@ -321,3 +420,3 @@

expect(result.handler.name).toBe('anyUser')
expect(result.params).toEqual({ '*': '610/books/987/edit' })
expect(result.params).toEqual({ $$: '610/books/987/edit' })

@@ -324,0 +423,0 @@ result = router.find('GET', '/users/610/books/987')

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc