├── .eslintrc.js ├── .npmignore ├── src ├── classes │ ├── index.js │ ├── StatusError.js │ └── StatusError.spec.js ├── router │ ├── index.js │ ├── ThrowableRouter.js │ └── ThrowableRouter.spec.js ├── response │ ├── text.js │ ├── missing.js │ ├── index.js │ ├── json.js │ ├── error.js │ ├── status.js │ ├── text.spec.js │ ├── createResponseType.spec.js │ ├── json.spec.js │ ├── createResponseType.js │ ├── missing.spec.js │ ├── error.spec.js │ └── status.spec.js ├── index.js ├── middleware │ ├── index.js │ ├── withCookies.js │ ├── withContent.js │ ├── withParams.js │ ├── withContent.spec.js │ ├── withParams.spec.js │ └── withCors.js ├── utils │ ├── retrieve.js │ ├── watch.js │ └── watch.spec.js ├── index.spec.js ├── index.ts └── itty-router-extras.d.ts ├── docs ├── favicon.png ├── pages │ └── example.md ├── style.css ├── favicon.svg ├── index.html └── main.js ├── vite.config.js ├── .travis.yml ├── CHANGELOG.md ├── .github └── FUNDING.yml ├── LICENSE ├── prebuild.js ├── package.json ├── README.md └── .gitignore /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | prebuild.js 3 | src 4 | coverage -------------------------------------------------------------------------------- /src/classes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('./StatusError'), 3 | } 4 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('./ThrowableRouter'), 3 | } 4 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwhitley/itty-router-extras/HEAD/docs/favicon.png -------------------------------------------------------------------------------- /docs/pages/example.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world! -------------------------------------------------------------------------------- /src/response/text.js: -------------------------------------------------------------------------------- 1 | const text = (message, options = {}) => new Response(message, options) 2 | 3 | module.exports = { text } 4 | -------------------------------------------------------------------------------- /src/response/missing.js: -------------------------------------------------------------------------------- 1 | const { error } = require('./error') 2 | 3 | const missing = (message = 'Not found.') => error(404, message) 4 | 5 | module.exports = { missing } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // RESPONSE HANDLERS 2 | module.exports = { 3 | ...require('./middleware'), 4 | ...require('./response'), 5 | ...require('./router'), 6 | ...require('./classes'), 7 | } 8 | -------------------------------------------------------------------------------- /src/middleware/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('./withContent'), 3 | ...require('./withCookies'), 4 | // ...require('./withCors'), 5 | ...require('./withParams'), 6 | } 7 | -------------------------------------------------------------------------------- /src/response/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('./error'), 3 | ...require('./json'), 4 | ...require('./missing'), 5 | ...require('./status'), 6 | ...require('./text'), 7 | } 8 | -------------------------------------------------------------------------------- /src/response/json.js: -------------------------------------------------------------------------------- 1 | const { createResponseType } = require('./createResponseType') 2 | 3 | const json = createResponseType('application/json; charset=utf-8') 4 | 5 | module.exports = { json } 6 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | .itty-logo { 2 | font-size: 1.6rem; 3 | display: flex; 4 | align-items: center; 5 | } 6 | 7 | .itty-logo svg { 8 | width: 24px; 9 | height: 24px; 10 | margin-right: 8px; 11 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | 4 | export default defineConfig({ 5 | root: "./docs", 6 | publicDir: "pages", 7 | plugins: [vue()], 8 | }); 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | install: 5 | - npm install 6 | script: 7 | - npm run build 8 | - npm run test 9 | after_script: 10 | - COVERALLS_REPO_TOKEN=$COVERALLS_REPO_TOKEN npm run coveralls -------------------------------------------------------------------------------- /src/classes/StatusError.js: -------------------------------------------------------------------------------- 1 | class StatusError extends Error { 2 | constructor(status = 500, message = 'Internal Error.') { 3 | super(message) 4 | this.name = 'StatusError' 5 | this.status = status 6 | } 7 | } 8 | 9 | module.exports = { StatusError } 10 | -------------------------------------------------------------------------------- /src/utils/retrieve.js: -------------------------------------------------------------------------------- 1 | const retrieve = (predicate, fn) => request => { 2 | request.proxy = new Proxy(request, { 3 | get: (obj, prop) => predicate(prop, obj) 4 | ? fn(prop, request) 5 | : obj[prop] 6 | }) 7 | } 8 | 9 | module.exports = { retrieve } 10 | -------------------------------------------------------------------------------- /src/response/error.js: -------------------------------------------------------------------------------- 1 | const { json } = require('./json') 2 | 3 | const error = ( 4 | status = 500, 5 | content = 'Internal Server Error.', 6 | ) => json({ 7 | ...(typeof content === 'object' 8 | ? content 9 | : { 10 | status, 11 | error: content, 12 | }), 13 | }, { status }) 14 | 15 | module.exports = { error } 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 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. 3 | 4 | - **v0.3.0** - withParams may now be used as upstream middleware (e.g. router.all('*', withParams)) 5 | -------------------------------------------------------------------------------- /src/response/status.js: -------------------------------------------------------------------------------- 1 | const { json } = require('./json') 2 | 3 | const status = (status, message) => 4 | message 5 | ? json({ 6 | ...(typeof message === 'object' 7 | ? message 8 | : { 9 | status, 10 | message, 11 | }), 12 | }, { status }) 13 | : new Response(null, { status }) 14 | 15 | module.exports = { status } 16 | -------------------------------------------------------------------------------- /src/middleware/withCookies.js: -------------------------------------------------------------------------------- 1 | // withCookies - embeds cookies object into the request 2 | const withCookies = request => { 3 | request.cookies = {} 4 | try { 5 | request.cookies = (request.headers.get('Cookie') || '') 6 | .split(/;\s*/) 7 | .map(pair => pair.split(/=(.+)/)) 8 | .reduce((acc, [key, value]) => { 9 | acc[key] = value 10 | 11 | return acc 12 | }, {}) 13 | } catch (err) {} 14 | } 15 | 16 | module.exports = { withCookies } 17 | -------------------------------------------------------------------------------- /src/utils/watch.js: -------------------------------------------------------------------------------- 1 | const watch = (predicate, fn) => request => { 2 | request.proxy = new Proxy(request.proxy || request, { 3 | set: (obj, prop, value) => { 4 | obj[prop] = value 5 | 6 | const passes = typeof predicate === 'function' 7 | ? predicate(prop, obj) 8 | : obj.hasOwnProperty(prop) 9 | 10 | if (passes) { 11 | fn(value, prop, request) 12 | } 13 | 14 | return true 15 | } 16 | }) 17 | } 18 | 19 | module.exports = { watch } 20 | -------------------------------------------------------------------------------- /src/middleware/withContent.js: -------------------------------------------------------------------------------- 1 | // withContent - embeds any request body as request.content 2 | const withContent = async request => { 3 | let contentType = request.headers.get('content-type') 4 | request.content = undefined 5 | 6 | try { 7 | if (contentType) { 8 | if (contentType.includes('application/json')) { 9 | request.content = await request.json() 10 | } 11 | } 12 | } catch (err) {} // silently fail on error 13 | } 14 | 15 | module.exports = { withContent } 16 | -------------------------------------------------------------------------------- /src/response/text.spec.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch') 2 | 3 | const { text } = require('./text') 4 | 5 | const message = 'Got it!' 6 | 7 | describe('response/text', () => { 8 | describe(`text(content)`, () => { 9 | it('returns a text Response with content', async () => { 10 | const response = text(message) 11 | 12 | expect(response instanceof Response).toBe(true) 13 | expect(response.status).toBe(200) 14 | expect(await response.text()).toEqual(message) 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/response/createResponseType.spec.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch') 2 | 3 | const { createResponseType } = require('./createResponseType') 4 | 5 | describe('response/createResponseType', () => { 6 | 7 | it('can create custom response handlers', () => { 8 | const payload = { foo: 'bar' } 9 | const type = 'application/json; charset=utf-8' 10 | const json = createResponseType(type) 11 | 12 | const response = json(payload) 13 | expect(response.headers.get('content-type')).toBe(type) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/response/json.spec.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch') 2 | 3 | const { json } = require('./json') 4 | 5 | const message = 'Got it!' 6 | 7 | describe('response/json', () => { 8 | describe(`json(content)`, () => { 9 | it('returns a JSON Response with content', async () => { 10 | const response = json({ message }) 11 | 12 | expect(response instanceof Response).toBe(true) 13 | expect(response.status).toBe(200) 14 | expect(await response.json()).toEqual({ message }) 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/response/createResponseType.js: -------------------------------------------------------------------------------- 1 | const createResponseType = (format = 'text/plain; charset=utf-8') => 2 | (body, options = {}) => { 3 | const { headers = {}, ...rest } = options 4 | 5 | if (typeof body === 'object') { 6 | return new Response(JSON.stringify(body), { 7 | headers: { 8 | 'Content-Type': format, 9 | ...headers, 10 | }, 11 | ...rest, 12 | }) 13 | } 14 | 15 | return new Response(body, options) 16 | } 17 | 18 | module.exports = { createResponseType } 19 | -------------------------------------------------------------------------------- /src/classes/StatusError.spec.js: -------------------------------------------------------------------------------- 1 | const { StatusError } = require('./StatusError') 2 | 3 | const message = 'You messed up!' 4 | 5 | describe('class/StatusError', () => { 6 | describe(`StatusError(status = '500', message = 'Internal Error')`, () => { 7 | it('returns a JSON Response with { message } and status', async () => { 8 | const error = new StatusError(400, 'Bad Request') 9 | 10 | expect(error instanceof Error).toBe(true) 11 | expect(error.status).toBe(400) 12 | expect(error.name).toBe('StatusError') 13 | expect(error.message).toBe('Bad Request') 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/middleware/withParams.js: -------------------------------------------------------------------------------- 1 | // withParams - injects route params within request at top level 2 | // const { retrieve } = require('../utils/retrieve') 3 | 4 | // const withParams = retrieve(v => true, (prop, request) => request.params && request.params[prop] 5 | // ? request.params[prop] 6 | // : request[prop]) 7 | 8 | const withParams = request => { 9 | for (const param in request.params || {}) { 10 | request[param] = request.params[param] 11 | } 12 | } 13 | 14 | module.exports = { withParams } 15 | -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('itty-router-extras', () => { 2 | it('returns all exports', async () => { 3 | const exports = require('./index') 4 | const expectedExports = [ 5 | 'StatusError', 6 | 'withContent', 7 | 'withCookies', 8 | 'withParams', 9 | 'error', 10 | 'json', 11 | 'missing', 12 | 'status', 13 | 'text', 14 | 'ThrowableRouter', 15 | ] 16 | 17 | for (const e of expectedExports) { 18 | if (!exports.hasOwnProperty(e)) { 19 | console.log('missing export:', e) 20 | } 21 | expect(exports.hasOwnProperty(e)).toBe(true) 22 | } 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | interface RouteHandler { 2 | (request: TRequest & Request, ...args: any): any 3 | } 4 | 5 | interface Route { 6 | (path: string, ...handlers: RouteHandler[]): Router 7 | } 8 | 9 | type Obj = { 10 | [propName: string]: string 11 | } 12 | 13 | interface Request { 14 | method?: string 15 | url: string 16 | params?: Obj 17 | query?: Obj 18 | } 19 | 20 | type Router = { 21 | handle: (request: Request, ...extra: any) => any 22 | } & { 23 | [any:string]: Route 24 | } 25 | 26 | interface RouterOptions { 27 | base?: string 28 | } 29 | 30 | export function Router(options?:RouterOptions): Router 31 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kwhitley 4 | open_collective: kevinrwhitley 5 | # patreon: # Replace with a single Patreon username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # iberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/router/ThrowableRouter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Router } = require('itty-router') 4 | const { error } = require('../response') 5 | 6 | const ThrowableRouter = (options = {}) => { 7 | const { stack = false } = options 8 | 9 | return new Proxy(Router(options), { 10 | get: (obj, prop) => (...args) => 11 | prop === 'handle' 12 | ? obj[prop](...args).catch(err => error( 13 | err.status || 500, 14 | { 15 | status: err.status || 500, 16 | error: err.message, 17 | stack: stack && err.stack || undefined 18 | }, 19 | )) 20 | : obj[prop](...args) 21 | }) 22 | } 23 | 24 | 25 | module.exports = { ThrowableRouter } 26 | -------------------------------------------------------------------------------- /src/middleware/withContent.spec.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch') 2 | 3 | const { ThrowableRouter } = require('../router/ThrowableRouter') 4 | const { withContent } = require('./withContent') 5 | 6 | describe('middleware/withContent', () => { 7 | it('returns with json payload', async () => { 8 | const router = ThrowableRouter() 9 | const handler = jest.fn(req => req.content) 10 | const payload = { foo: 'bar' } 11 | 12 | router.post('/', withContent, handler) 13 | 14 | const request = new Request('https://example.com/', { 15 | method: 'post', 16 | headers: { 17 | 'content-type': 'application/json', 18 | }, 19 | body: JSON.stringify(payload), 20 | }) 21 | 22 | await router.handle(request) 23 | 24 | expect(handler).toHaveBeenCalled() 25 | expect(handler).toHaveReturnedWith(payload) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/response/missing.spec.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch') 2 | 3 | const { missing } = require('./missing') 4 | 5 | const message = 'We could not find that resource.' 6 | 7 | describe('response/missing', () => { 8 | describe('missing(message)', () => { 9 | it('returns a 404 JSON Response with content', async () => { 10 | const response = missing(message) 11 | 12 | expect(response instanceof Response).toBe(true) 13 | expect(response.status).toBe(404) 14 | expect(await response.json()).toEqual({ error: message, status: 404 }) 15 | }) 16 | 17 | it('will use second param as object payload if given', async () => { 18 | const payload = { message: 'Bad Request', stack: [] } 19 | const response = missing(payload) 20 | 21 | expect(response instanceof Response).toBe(true) 22 | expect(response.status).toBe(404) 23 | expect(await response.json()).toEqual(payload) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/response/error.spec.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch') 2 | 3 | const { error } = require('./error') 4 | 5 | const message = 'You messed up!' 6 | 7 | describe('response/error', () => { 8 | describe(`error(message, status = '500')`, () => { 9 | it('returns a JSON Response with { message } and status', async () => { 10 | const response = error(400, message) 11 | 12 | expect(response instanceof Response).toBe(true) 13 | expect(response.status).toBe(400) 14 | expect(await response.json()).toEqual({ error: message, status: 400 }) 15 | }) 16 | 17 | it('will use second param as object payload if given', async () => { 18 | const expected = { message: 'Bad Request', stack: [] } 19 | const response = error(400, expected) 20 | 21 | expect(response instanceof Response).toBe(true) 22 | expect(response.status).toBe(400) 23 | expect(await response.json()).toEqual(expected) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/response/status.spec.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch') 2 | 3 | const response = require('.') 4 | const { status } = require('./status') 5 | const message = 'Got it!' 6 | 7 | describe('response/error', () => { 8 | describe(`status(code)`, () => { 9 | it('returns an empty Response with status code', async () => { 10 | const response1 = status(400) 11 | 12 | expect(response1 instanceof Response).toBe(true) 13 | expect(response1.status).toBe(400) 14 | }) 15 | 16 | it('returns a simple message if given', async () => { 17 | const response2 = status(204, message) 18 | 19 | expect(response2.status).toBe(204) 20 | expect(await response2.json()).toEqual({ status: 204, message }) 21 | }) 22 | 23 | it('will use second param as object payload if given', async () => { 24 | const payload = { message: 'Bad Request', stack: [] } 25 | const response = status(400, payload) 26 | 27 | expect(response instanceof Response).toBe(true) 28 | expect(response.status).toBe(400) 29 | expect(await response.json()).toEqual(payload) 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/middleware/withParams.spec.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch') 2 | 3 | const { ThrowableRouter } = require('../router/ThrowableRouter') 4 | const { withParams } = require('./withParams') 5 | 6 | describe('middleware/withParams', () => { 7 | it('embeds params as middleware', async () => { 8 | const router = ThrowableRouter() 9 | const handler = jest.fn(req => req.id) 10 | 11 | router.get('/:id', withParams, handler) 12 | 13 | const request = new Request('https://example.com/12') 14 | 15 | await router.handle(request) 16 | 17 | expect(handler).toHaveBeenCalled() 18 | expect(handler).toHaveReturnedWith('12') 19 | }) 20 | 21 | // it('embeds params as upstream middleware', async () => { 22 | // const router = ThrowableRouter() 23 | // const handler = jest.fn(req => req.id) 24 | 25 | // router 26 | // .all('*', withParams) 27 | // .get('/:id', handler) 28 | 29 | // const request = new Request('https://example.com/12') 30 | 31 | // await router.handle(request) 32 | 33 | // expect(handler).toHaveBeenCalled() 34 | // expect(handler).toHaveReturnedWith('12') 35 | // }) 36 | }) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kevin R. Whitley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/itty-router-extras.d.ts: -------------------------------------------------------------------------------- 1 | // Helpers 2 | 3 | interface BaseRouterOptions { 4 | base?: string; 5 | } 6 | 7 | type ThrowableRouterOptions = BaseRouterOptions & { stack?: boolean }; 8 | 9 | interface CorsOptions { 10 | origin?: string; 11 | methods?: string; 12 | headers?: string; 13 | credentials?: boolean; 14 | } 15 | 16 | export function ThrowableRouter(options?: ThrowableRouterOptions): typeof Proxy; 17 | 18 | // Response 19 | export function json(obj: object): Response; 20 | export function status(status: number, message: string | object): Response; 21 | export function error(status?: number, content?: string | object): Response; 22 | export function missing(message?: string | object): Response; 23 | export function text(message: string, options?: ResponseInit): Response; 24 | 25 | // MiddleWare 26 | export function withContent(request: Request): void; 27 | export function withCookies(request: Request): void; 28 | export function withCors(options?: CorsOptions): Response; 29 | export function withParams(request: Request): void; 30 | 31 | export class StatusError { 32 | status?: number; 33 | constructor(status?: number, message?: string); 34 | } 35 | -------------------------------------------------------------------------------- /prebuild.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require('fs-extra') 2 | 3 | const base = readFileSync('./src/index.js', { encoding: 'utf-8' }) 4 | const minifiedBase = base 5 | .replace(/\bhandlers\b/g, 'hs') // Handler(S) 6 | .replace(/\bhandler\b/g, 'h') // Handler 7 | .replace(/([^\.])obj\b/g, '$1t') // Target 8 | .replace(/([^\.])options\b/g, '$1o') // Options 9 | .replace(/([^\.])receiver\b/g, '$1c') // Options 10 | .replace(/([^\.])route\b/g, '$1p') // Path 11 | .replace(/([^\.])\.routes\b/g, '$1\.r') // routes Queue 12 | .replace(/args/g, 'a') // Args 13 | .replace(/([^\.])request\b/g, '$1r') // Request 14 | .replace(/([^\.])response\b/g, '$1s') // reSponse 15 | .replace(/([^\.])match\b/g, '$1m') // Match 16 | .replace(/([^\.])prop\b/g, '$1k') // Key 17 | .replace(/([^\.])url\b/g, '$1u') // Url 18 | writeFileSync('./dist/index.js', minifiedBase) 19 | console.log('minifying variables --> dist/index.js') 20 | 21 | const test = readFileSync('./src/index.spec.js', { encoding: 'utf-8' }) 22 | const minifiedTest = test.replace('index.js', 'index.min.js') 23 | writeFileSync('./dist/index.spec.js', minifiedTest) 24 | console.log('creating dist tests --> dist/index.spec.js') -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Itty Router | tiny, zero-dependency router 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/router/ThrowableRouter.spec.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch') 2 | 3 | const { ThrowableRouter } = require('./ThrowableRouter') 4 | 5 | describe('router/ThrowableRouter', () => { 6 | describe(`ThrowableRouter(options = {})`, () => { 7 | it('is an itty proxy', async () => { 8 | const origin = {} 9 | const router = ThrowableRouter(origin) 10 | 11 | router.get('/foo', () => {}) 12 | 13 | expect(typeof origin.r).toBe('object') 14 | expect(origin.r.length).toBe(1) 15 | }) 16 | 17 | it('captures a throw', async () => { 18 | const router = ThrowableRouter() 19 | 20 | router.get('/breaks', request => request.will.throw) 21 | 22 | const response = await router.handle(new Request('https://slick/breaks')) 23 | 24 | expect(response.status).toBe(500) 25 | 26 | const payload = await response.json() 27 | 28 | expect(payload.error).not.toBeUndefined() 29 | expect(payload.status).toBe(500) 30 | }) 31 | 32 | it('includes a stack trace with option', async () => { 33 | const router = ThrowableRouter({ stack: true }) 34 | 35 | router.get('/breaks', request => request.will.throw) 36 | 37 | const response = await router.handle(new Request('https://slick/breaks')) 38 | const payload = await response.json() 39 | 40 | expect(response.status).toBe(500) 41 | expect(payload.error).not.toBeUndefined() 42 | expect(payload.stack).not.toBeUndefined() 43 | expect(payload.status).toBe(500) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/utils/watch.spec.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch') 2 | 3 | const { ThrowableRouter } = require('../router/ThrowableRouter') 4 | const { watch } = require('./watch') 5 | 6 | describe('utils/watch', () => { 7 | it('creates reactive middleware (string prop)', async () => { 8 | const router = ThrowableRouter() 9 | const watcher = jest.fn((value, prop, request) => ({ prop, value })) 10 | const handler = jest.fn(req => req.foo) 11 | 12 | const watchFoo = watch('foo', watcher) 13 | 14 | const modifyFoo = request => { 15 | request.foo = 'new foo' 16 | } 17 | 18 | router 19 | .all('*', watchFoo, modifyFoo) 20 | .get('/:id', handler) 21 | 22 | const request = new Request('https://example.com/12') 23 | 24 | await router.handle(request) 25 | 26 | expect(handler).toHaveBeenCalled() 27 | expect(handler).toHaveReturnedWith('new foo') 28 | expect(watcher).toHaveReturnedWith({ prop: 'foo', value: 'new foo' }) 29 | }) 30 | 31 | it('creates reactive middleware (function predicate)', async () => { 32 | const router = ThrowableRouter() 33 | const watcher = jest.fn((value, prop, request) => ({ prop, value })) 34 | const handler = jest.fn(req => req.foo) 35 | 36 | const watchFoo = watch(prop => prop === 'foo', watcher) 37 | 38 | const modifyFoo = request => { 39 | request.foo = 'new foo' 40 | } 41 | 42 | router 43 | .all('*', watchFoo, modifyFoo) 44 | .get('/:id', handler) 45 | 46 | const request = new Request('https://example.com/12') 47 | 48 | await router.handle(request) 49 | 50 | expect(handler).toHaveBeenCalled() 51 | expect(handler).toHaveReturnedWith('new foo') 52 | expect(watcher).toHaveReturnedWith({ prop: 'foo', value: 'new foo' }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "itty-router-extras", 3 | "version": "0.4.6", 4 | "description": "An assortment of delicious extras for the calorie-light itty-router.", 5 | "main": "./index.js", 6 | "keywords": [ 7 | "router", 8 | "cloudflare", 9 | "workers", 10 | "serverless", 11 | "regex", 12 | "routing", 13 | "api", 14 | "handler", 15 | "params", 16 | "middleware", 17 | "nested", 18 | "helpers", 19 | "utils", 20 | "cookies", 21 | "content", 22 | "cors" 23 | ], 24 | "scripts": { 25 | "lint": "npx eslint src", 26 | "test": "jest --verbose --coverage src", 27 | "verify": "echo 'verifying module...' && yarn build && yarn test", 28 | "dev": "yarn lint && jest --verbose --watch src", 29 | "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls", 30 | "prerelease": "yarn verify", 31 | "prebuild": "rimraf dist && mkdir dist && cp src/itty-router-extras.d.ts dist", 32 | "build": "terser-folder src -eo dist --pattern '**/*.js,!**/*spec.js' -x .js", 33 | "release": "release --tag --push --patch --src=dist" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/kwhitley/itty-router-extras.git" 38 | }, 39 | "author": "Kevin R. Whitley ", 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/kwhitley/itty-router-extras/issues" 43 | }, 44 | "homepage": "https://github.com/kwhitley/itty-router-extras#readme", 45 | "devDependencies": { 46 | "chalk": "^4.1.0", 47 | "coveralls": "^3.1.0", 48 | "eslint": "^7.22.0", 49 | "eslint-plugin-jest": "^24.3.2", 50 | "fs-extra": "^9.1.0", 51 | "gzip-size": "^6.0.0", 52 | "isomorphic-fetch": "^3.0.0", 53 | "itty-router": "^2.3.2", 54 | "jest": "^26.6.3", 55 | "rimraf": "^3.0.2", 56 | "terser": "^5.6.1", 57 | "terser-folder": "^2.0.0", 58 | "yarn-release": "^1.10.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/middleware/withCors.js: -------------------------------------------------------------------------------- 1 | const withCors = (options = {}) => request => { 2 | const { 3 | origin = '*', 4 | methods = 'GET, POST, PATCH, DELETE', 5 | headers = 'authorization, referer, origin, content-type', 6 | credentials = false, 7 | } = options 8 | const referer = request.headers.get('Referer') 9 | const url = new URL(referer) 10 | const allowedOrigin = url.origin.match(/[^\w](slick\.af)|(localhost:3000)$/) 11 | ? url.origin 12 | : 'https://slick.af' 13 | const corsHeaders = { 14 | 'Access-Control-Allow-Origin': origin, 15 | 'Access-Control-Allow-Methods': methods, 16 | 'Access-Control-Allow-Headers': headers, 17 | } 18 | 19 | if (allowCredentials) { 20 | corsHeaders['Access-Control-Allow-Credentials'] = 'true' 21 | } 22 | 23 | if ( 24 | request.headers.get('Origin') !== null && 25 | request.headers.get('Access-Control-Request-Method') !== null 26 | ) { 27 | // Handle CORS pre-flight request. 28 | return new Response(null, { 29 | status: 204, 30 | headers: corsHeaders 31 | }) 32 | } 33 | 34 | // Handle standard OPTIONS request. 35 | return new Response(null, { 36 | headers: { 37 | 'Allow': `${methods} , HEAD, OPTIONS`, 38 | } 39 | }) 40 | } 41 | 42 | const addCorsHeaders = request => response => { 43 | let allowedOrigin = 'https://slick.af' 44 | const referer = request.headers.get('Referer') 45 | 46 | if (referer) { 47 | const url = new URL(referer) 48 | allowedOrigin = url.origin.match(/[^\w](slick\.af)|(localhost:3000)$/) 49 | ? url.origin 50 | : allowedOrigin 51 | } 52 | 53 | try { 54 | response.headers.set('Access-Control-Allow-Origin', allowedOrigin) 55 | response.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, POST, PATCH, DELETE, OPTIONS') 56 | response.headers.set('Access-Control-Allow-Credentials', 'true') 57 | } catch (err) { 58 | // couldn't modify headers 59 | } 60 | 61 | return response 62 | } 63 | 64 | module.exports = { withCors } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Itty Router][logo-image] 2 | 3 | [![npm package][npm-image]][npm-url] 4 | [![Build Status][travis-image]][travis-url] 5 | [![Coverage Status][coveralls-image]][coveralls-url] 6 | [![Open Issues][issues-image]][issues-url] 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | # Deprecation Notice 15 | _`itty-router-extras` has been fully deprecated in favor of using the built-in helpers within [`itty-router`](https://itty.dev/itty-router) itself._ 16 | 17 | #### Before 18 | ```ts 19 | import { Router } from 'itty-router' 20 | import { json, withParams, withContent, error } from 'itty-router-extras' 21 | ``` 22 | 23 | #### Now 24 | ```ts 25 | import { Router, json, withParams, withContent, error } from 'itty-router' 26 | ``` 27 | 28 | ### v5.x Migration Guide 29 | https://itty.dev/itty-router/migrations/v4-v5 30 | 31 | [twitter-image]:https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fitty-router-extras 32 | [logo-image]:https://user-images.githubusercontent.com/865416/112549341-a4377300-8d8b-11eb-8977-574967dede99.png 33 | [gzip-image]:https://img.shields.io/bundlephobia/minzip/itty-router-extras 34 | [gzip-url]:https://bundlephobia.com/result?p=itty-router-extras 35 | [issues-image]:https://img.shields.io/github/issues/kwhitley/itty-router-extras 36 | [issues-url]:https://github.com/kwhitley/itty-router-extras/issues 37 | [npm-image]:https://img.shields.io/npm/v/itty-router-extras.svg 38 | [npm-url]:http://npmjs.org/package/itty-router-extras 39 | [travis-image]:https://travis-ci.org/kwhitley/itty-router-extras.svg?branch=v0.x 40 | [travis-url]:https://travis-ci.org/kwhitley/itty-router-extras 41 | [david-image]:https://david-dm.org/kwhitley/itty-router-extras/status.svg 42 | [david-url]:https://david-dm.org/kwhitley/itty-router-extras 43 | [coveralls-image]:https://coveralls.io/repos/github/kwhitley/itty-router-extras/badge.svg?branch=v0.x 44 | [coveralls-url]:https://coveralls.io/github/kwhitley/itty-router-extras?branch=v0.x 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS ignores 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Docs ignores 110 | docs/dist 111 | docs/dist-ssr 112 | docs/*.local 113 | docs/pages/README.md -------------------------------------------------------------------------------- /docs/main.js: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | 3 | const logo = ` 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ` 22 | 23 | new Docute({ 24 | target: "#app", 25 | detectSystemDarkTheme: true, 26 | darkThemeToggler: true, 27 | highlight: ["javascript", "json"], 28 | logo: 29 | ``, 30 | nav: [ 31 | { 32 | title: "Home", 33 | link: "/", 34 | }, 35 | { 36 | title: "GitHub", 37 | link: "https://github.com/kwhitley/itty-router", 38 | }, 39 | ], 40 | sidebar: [ 41 | { 42 | title: "Getting Started", 43 | link: "/", 44 | }, 45 | ], 46 | footer: `© ${new Date().getFullYear()} 47 | Kevin Whitley . Released under MIT license.`, 48 | cssVariables: (theme) => { 49 | const light = { 50 | pageBackground: "#fafafa", 51 | accentColor: '#e4009b', 52 | inlineCodeBackground: "#dbdbdb", 53 | inlineCodeColor: "#212121", 54 | }; 55 | 56 | const dark = { 57 | accentColor: '#ff85d8' 58 | }; 59 | 60 | return theme === "default" ? light : dark; 61 | }, 62 | }); 63 | --------------------------------------------------------------------------------