Skip to content

Commit

Permalink
Merge pull request #6 from kwhitley/v1.1
Browse files Browse the repository at this point in the history
v1.1 release
  • Loading branch information
kwhitley committed May 29, 2020
2 parents 2a6b171 + 1f59697 commit ff6382f
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 58 deletions.
83 changes: 56 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ npm install itty-router
- [x] bonus query parsing (e.g. `?page=3`)
- [x] adds params & query to request: `{ params: { foo: 'bar' }, query: { page: '3' }}`
- [x] chainable route declarations (why not?)
- [x] multiple (sync or async) handlers per route for passthrough logic, auth, errors, etc
- [x] multiple (sync or async) [middleware handlers](#multiple-route-handlers-as-middleware) per route for passthrough logic, auth, errors, etc
- [x] handler functions "stop" at the first handler to return [anything]
- [x] supports [nested routers](#nested-routers)
- [x] supports [base path](#base-path) option to prefix all routes
- [ ] have pretty code (yeah right...)

# Examples
Expand All @@ -40,7 +43,7 @@ const router = Router()

// basic GET route
router.get('/todos/:id', console.log)

// first match always wins, so be careful with order of registering routes
router
.get('/todos/oops', () => console.log('you will never see this, thanks to upstream /todos/:id'))
Expand All @@ -64,7 +67,7 @@ router.handle({ method: 'GET', url: 'https://foo.com/todos/13?foo=bar' })
// }
```

# Usage
# Usage
### 1. Create a Router
```js
import { Router } from 'itty-router'
Expand All @@ -80,7 +83,7 @@ The "instantiated" router translates any attribute (e.g. `.get`, `.post`, `.patc
router.get('/todos/:user/:item?', (req) => {
let { params, query, url } = req
let { user, item } = params

console.log('GET TODOS from', url, { user, item })
})
```
Expand Down Expand Up @@ -154,49 +157,75 @@ router
router.handle({ url: 'https://example.com/user' }) // --> STATUS 200: { name: 'Mittens', age: 3 }
```
### Nested Routers
```js
const parentRouter = Router()
const todosRouter = Router()

todosRouter.get('/todos/:id?', ({ params }) => console.log({ id: params.id }))

parentRouter.get('/todos/*', todosRouter.handle) // all /todos/* routes will route through the todosRouter
```
### Base Path
```js
const router = Router({ base: '/api/v0' })

router.get('/todos', indexHandler)
router.get('/todos/:id', itemHandler)

router.handle({ url: 'https://example.com/api/v0/todos' }) // --> fires indexHandler
```
## Testing & Contributing
1. fork repo
2. add code
3. run tests (and add your own) `yarn test`
4. submit PR
5. profit
3. run tests (add your own if needed) `yarn dev`
4. verify tests run once minified `yarn verify`
5. commit files (do not manually modify version numbers)
6. submit PR
7. we'll add you to the credits :)
## Entire Router Code (latest...)
```js
const Router = () =>
const Router = (o = {}) =>
new Proxy({}, {
get: (o, k) => k === 'handle'
get: (t, k) => k === 'handle'
? async (q) => {
for ([p, hs] of o[(q.method || 'GET').toLowerCase()] || []) {
if (m = (u = new URL(q.url)).pathname.match(p)) {
q.params = m.groups
q.query = Object.fromEntries(u.searchParams.entries())

for (h of hs) {
if ((s = await h(q)) !== undefined) return s
for ([p, hs] of t[(q.method || 'GET').toLowerCase()] || []) {
if (m = (u = new URL(q.url)).pathname.match(p)) {
q.params = m.groups
q.query = Object.fromEntries(u.searchParams.entries())

for (h of hs) {
if ((s = await h(q)) !== undefined) return s
}
}
}
}
}
: (p, ...hs) => (o[k] = o[k] || []).push([
`^${p
.replace('*', '.*')
.replace(/(\/:([^\/\?]+)(\?)?)/gi, '/$3(?<$2>[^/]+)$3')}$`,
hs
]) && o
: (p, ...hs) =>
(t[k] = t[k] || []).push([
`^${(o.base || '')+p
.replace(/(\/?)\*/g, '($1.*)?')
.replace(/(\/:([^\/\?]+)(\?)?)/gi, '/$3(?<$2>[^/]+)$3')
}$`,
hs
]) && t
})
```
## Special Thanks
This repo goes out to my past and present colleagues at Arundo - who have brought me such inspiration, fun,
and drive over the last couple years. In particular, the absurd brevity of this code is thanks to a
clever [abuse] of `Proxy`, courtesy of the brilliant [@mvasigh](https://github.com/mvasigh).
This trick allows methods (e.g. "get", "post") to by defined dynamically by the router as they are requested,
This repo goes out to my past and present colleagues at Arundo - who have brought me such inspiration, fun,
and drive over the last couple years. In particular, the absurd brevity of this code is thanks to a
clever [abuse] of `Proxy`, courtesy of the brilliant [@mvasigh](https://github.com/mvasigh).
This trick allows methods (e.g. "get", "post") to by defined dynamically by the router as they are requested,
**drastically** reducing boilerplate.
## Changelog
Until this library makes it to a production release of v1.x, **minor versions may contain breaking changes to the API**. After v1.x, semantic versioning will be honored, and breaking changes will only occur under the umbrella of a major version bump.
- **v1.1.0** - feature: added single option `{ base: '/some/path' }` to `Router` for route prefixing, fix: trailing wildcard issue (e.g. `/foo/*` should match `/foo`)
- **v1.0.0** - production release, stamped into gold from x0.9.7
- **v0.9.0** - added support for multiple handlers (middleware)
- **v0.8.0** - deep minification pass and build steps for final module
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
],
"scripts": {
"test": "jest --verbose --coverage",
"verify": "yarn build && yarn test",
"dev": "yarn test - --watch",
"coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls",
"prerelease": "echo releasing...",
Expand Down
22 changes: 12 additions & 10 deletions prebuild.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
const { readFileSync, writeFileSync } = require('fs-extra')

const base = readFileSync('./src/itty-router.js', { encoding: 'utf-8' })
const minifiedBase = base.replace(/\bhandlers\b/g, 'hs')
.replace(/\bhandler\b/g, 'h')
.replace(/([^\.])obj\b/g, '$1o')
.replace(/([^\.])attr\b/g, '$1a')
.replace(/([^\.])route\b/g, '$1p')
.replace(/([^\.])request\b/g, '$1q')
.replace(/([^\.])response\b/g, '$1s')
.replace(/([^\.])match\b/g, '$1m')
.replace(/([^\.])prop\b/g, '$1k')
.replace(/([^\.])url\b/g, '$1u')
const minifiedBase = base
.replace(/\bhandlers\b/g, 'hs') // Handler(S)
.replace(/\bhandler\b/g, 'h') // Handler
.replace(/([^\.])obj\b/g, '$1t') // Target
.replace(/([^\.])options\b/g, '$1o') // Options
.replace(/([^\.])attr\b/g, '$1a') // Attr
.replace(/([^\.])route\b/g, '$1p') // Path
.replace(/([^\.])request\b/g, '$1q') // reQuest
.replace(/([^\.])response\b/g, '$1s') // reSponse
.replace(/([^\.])match\b/g, '$1m') // Match
.replace(/([^\.])prop\b/g, '$1k') // Key
.replace(/([^\.])url\b/g, '$1u') // Url
writeFileSync('./dist/itty-router.js', minifiedBase)
console.log('minifying variables --> dist/itty-router.js')

Expand Down
36 changes: 18 additions & 18 deletions src/itty-router.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
const Router = () =>
const Router = (options = {}) =>
new Proxy({}, {
get: (obj, prop) => prop === 'handle'
get: (obj, prop) => prop === 'handle'
? async (request) => {
for ([route, handlers] of obj[(request.method || 'GET').toLowerCase()] || []) {
if (match = (url = new URL(request.url)).pathname.match(route)) { // route matched
request.params = match.groups
request.query = Object.fromEntries(url.searchParams.entries())
for ([route, handlers] of obj[(request.method || 'GET').toLowerCase()] || []) {
if (match = (url = new URL(request.url)).pathname.match(route)) {
request.params = match.groups
request.query = Object.fromEntries(url.searchParams.entries())

for (handler of handlers) {
if ((response = await handler(request)) !== undefined) return response
for (handler of handlers) {
if ((response = await handler(request)) !== undefined) return response
}
}
}
}
}
: (route, ...handlers) =>
(obj[prop] = obj[prop] || []).push([
`^${route
.replace('*', '.*')
.replace(/(\/:([^\/\?]+)(\?)?)/gi, '/$3(?<$2>[^/]+)$3')}$`,
handlers
]) && obj
}
)
: (route, ...handlers) =>
(obj[prop] = obj[prop] || []).push([
`^${(options.base || '')+route
.replace(/(\/?)\*/g, '($1.*)?')
.replace(/(\/:([^\/\?]+)(\?)?)/gi, '/$3(?<$2>[^/]+)$3')
}$`,
handlers
]) && obj
})

module.exports = { Router }
64 changes: 61 additions & 3 deletions src/itty-router.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ describe('Router', () => {
{ path: '*', callback: jest.fn(), method: 'get' },
]

for (var route of routes) {
router[route.method](route.path, route.callback)
const applyRoutes = (router, routes) => {
for (var route of routes) {
router[route.method](route.path, route.callback)
}

return router
}

applyRoutes(router, routes)

it(`is exported as { Router } from module`, () => {
expect(typeof Router).toBe('function')
})
Expand Down Expand Up @@ -144,7 +150,7 @@ describe('Router', () => {
}

const handler = jest.fn((req) => req.user.id)

r.get('/middleware/*', middleware)
r.get('/middleware/:id', handler)

Expand All @@ -153,5 +159,57 @@ describe('Router', () => {
expect(handler).toHaveBeenCalled()
expect(handler).toHaveReturnedWith(13)
})

it('can accept a basepath for routes', async () => {
const router = Router({ base: '/api' })
const handler = jest.fn()
router.get('/foo/:id?', handler)

router.handle(buildRequest({ path: '/api/foo' }))
expect(handler).toHaveBeenCalled()

router.handle(buildRequest({ path: '/api/foo/13' }))
expect(handler).toHaveBeenCalledTimes(2)
})

it('gracefully handles trailing slashes', async () => {
const r = Router()

const middleware = req => {
req.user = { id: 13 }
}

const handler = jest.fn((req) => req.user.id)

r.get('/middleware/*', middleware)
r.get('/middleware', handler)

await r.handle(buildRequest({ path: '/middleware' }))

expect(handler).toHaveBeenCalled()
expect(handler).toHaveReturnedWith(13)
})

it('allow wildcards in the middle of paths', async () => {
const r = Router()
const handler = jest.fn()

r.get('/foo/*/end', handler)

await r.handle(buildRequest({ path: '/foo/bar/baz/13/end' }))

expect(handler).toHaveBeenCalled()
})

it('can handle nested routers', async () => {
const router1 = Router({ base: '/api' })
const router2 = Router({ base: '/api/foo' })
const handler = jest.fn()
router1.get('/foo/*', router2.handle)
router2.get('/bar/:id?', handler)

router1.handle(buildRequest({ path: '/api/foo/bar' }))
expect(handler).toHaveBeenCalled()
})
})
})

0 comments on commit ff6382f

Please sign in to comment.