diff --git a/.eslintrc.json b/.eslintrc similarity index 84% rename from .eslintrc.json rename to .eslintrc index a5d491ff..d2f152d3 100644 --- a/.eslintrc.json +++ b/.eslintrc @@ -5,7 +5,8 @@ }, "extends": [ "eslint:recommended", - "plugin:@typescript-eslint/recommended" + "plugin:@typescript-eslint/recommended", + "prettier" ], "overrides": [], "parser": "@typescript-eslint/parser", @@ -13,9 +14,7 @@ "ecmaVersion": "latest", "sourceType": "module" }, - "plugins": [ - "@typescript-eslint" - ], + "plugins": ["@typescript-eslint"], "rules": { "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md index ba816010..1f554d6a 100644 --- a/.github/ISSUE_TEMPLATE/issue_template.md +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -4,42 +4,49 @@ about: Create a report to help us improve title: '' labels: '' assignees: '' - --- ## Describe the Issue + A clear and concise description of what the issue is. ## Example Router Code + Please provide the itty-router code related to the issue. If possible, create a minimal, reproducible example. ```ts // your code here - ``` ## Request Details + - Method: [e.g., GET, POST, PUT, DELETE] - URL: [e.g., /api/v1/users] - Request Body: If applicable, include the request body. - Request Headers: If applicable, include the request headers. ## Steps to Reproduce + Steps to reproduce the behavior: + 1. Run '...' 2. Send request to '....' 3. See error ## Expected Behavior + A clear and concise description of what you expected to happen. ## Actual Behavior + A clear and concise description of what actually happens. Include any error messages or unexpected responses. ## Environment (please complete the following information): + - Environment: [e.g., Node, Bun, Cloudflare Workers, Service Workers, Browser] - itty-router Version: [e.g., 1.0.0] - Other Relevant Libraries and their versions: [e.g., node 14.0.0] ## Additional Context + Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 3bec3ad8..4ccd552a 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -4,11 +4,12 @@ about: Have a question or something to discuss? title: '' labels: '' assignees: '' - --- ## Questions & Discussion -To cut down on long-standing topics that are discussion-based, rather than actual issues, please opt for one of the following paths, when possible. These have the benefit of potentially helping others after the issue (here) would have been closed. + +To cut down on long-standing topics that are discussion-based, rather than actual issues, please opt for one of the following paths, when possible. These have the benefit of potentially helping others after the issue (here) would have been closed. + 1. [Create a Discussion Topic](https://github.com/kwhitley/itty-router/discussions/new/choose) 2. [Discuss on Discord](https://discord.com/channels/832353585802903572) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3531b8aa..6bfc2543 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,23 +1,28 @@ ### Description + Please explain the changes you made here. ### Issue Related -- Link to the related issue: + +- Link to the related issue: ### Type of Change (select one and follow subtasks) + - [ ] Maintenance or repo-level work (e.g. linting, build, tests, refactoring, etc.) - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Is this a mainstream benefit or an edge case? - [ ] Is it worth the bytes? - [ ] Breaking change (fix or feature that would cause existing functionality/userland code to not work as expected) - - [ ] Explain why a breaking change is necessary: + - [ ] Explain why a breaking change is necessary: - [ ] This change requires (or is) a documentation update - [ ] I have added necessary local documentation (if appropriate) - [ ] I have added necessary [itty.dev](https://github.com/kwhitley/itty.dev) documentation (if appropriate) -### Testing +### Testing + Please describe the tests that you ran to verify your changes. ### Checklist + - [ ] I have read the [CONTRIBUTING](../CONTRIBUTING.md) doc diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index d71a5f31..d0e65e14 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -1,25 +1,23 @@ -on: ["push", "pull_request"] +on: ['push', 'pull_request'] name: Coveralls jobs: - build: name: Build runs-on: ubuntu-latest steps: + - uses: actions/checkout@v1 - - uses: actions/checkout@v1 - - - name: Use Node.js 16.x - uses: actions/setup-node@v3 - with: - node-version: 16.x + - name: Use Node.js 16.x + uses: actions/setup-node@v3 + with: + node-version: 16.x - - name: npm install, npm run coverage - run: | - npm install - npm run coverage + - name: npm install, npm run coverage + run: | + npm install + npm run coverage - - name: Coveralls - uses: coverallsapp/github-action@v2 + - name: Coveralls + uses: coverallsapp/github-action@v2 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index d6f46f49..0fc0a5dc 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -2,13 +2,12 @@ name: build on: push: - branches: [ v4.x ] + branches: [v4.x] pull_request: - branches: [ v4.x ] + branches: [v4.x] jobs: build: - runs-on: ubuntu-latest steps: diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..6b9c164a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +trailingComma: 'es5' +tabWidth: 2 +semi: false +singleQuote: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 272acb06..5811333a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,9 @@ - **v4.0.00** - Partial changelog below - BREAKING: heavy TS rewrite for core Router (thank you, ChatGPT) - added: nearly all extras from itty-router-extras - - added: createCors from itty-cors + - added: createCors from itty-cors - added: complete documentation at https://itty.dev - - feature: withParams may be used as upstream middleware (hooray!) + - feature: withParams may be used as upstream middleware (hooray!) - **v3.0.11** - changed environment build to rollup (from tsup) and code golfed the toQuery logic. (credit [@DrLoopFall](https://github.com/DrLoopFall)) - **v3.0.9** - fixes some TS issue, previously requiring you to define Router Methods to chain request definitions. (credit [@jahands](https://github.com/jahands)) - **v3.0.0** - total TS conversion with improved types, adding greedy params (credit [@markusahlstrand](https://github.com/markusahlstrand)) @@ -20,7 +20,7 @@ - **v2.4.9** - fixed the cursed "optional" file format capturing bug - RIP all the bytes lost - **v2.4.6** - fixed README issues - **v2.4.1** - fixed type errors introduced with 2.4.0 -- **v2.4.0** - HUGE internal code-golfing refactor thanks to [@taralx](https://github.com/taralx)! Super cool work on this!!! +- **v2.4.0** - HUGE internal code-golfing refactor thanks to [@taralx](https://github.com/taralx)! Super cool work on this!!! - **v2.3.10** - fix: dots now properly escaped (e.g. /image.jpg should not match /imageXjpg) - **v2.3.9** - dev fixes: [@taralx](https://github.com/taralx) improved QOL issues for test writers and dev installers - **v2.3.7** - fix: :id.:format not resolving (only conditional format would match) @@ -43,6 +43,6 @@ - **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 -- **v0.7.0** - removed { path } from request handler context, travis build fixed, added coveralls, improved README docs +- **v0.7.0** - removed { path } from request handler context, travis build fixed, added coveralls, improved README docs - **v0.6.0** - added types to project for vscode intellisense (thanks [@mvasigh](https://github.com/mvasigh)) - **v0.5.4** - fix: wildcard routes properly supported diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 73517d26..e5b3df39 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb04d4da..8ab8b4e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,8 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of resources for individuals, communities, and companies. These resources help people who want to learn how to run and contribute to open source projects. Contributors and people new to open source alike will find the following guides especially useful: -* [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) -* [Building Welcoming Communities](https://opensource.guide/building-community/) +- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) +- [Building Welcoming Communities](https://opensource.guide/building-community/) ## Bugs diff --git a/README.md b/README.md index 92ed225c..0c09c3e8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@

v4.x Documentation @ itty.dev

-

npm version @@ -50,28 +49,29 @@ --- -Itty aims to be the world's smallest (~440 bytes), feature-rich JavaScript router, enabling beautiful API code with a near-zero bundlesize. Designed originally for [Cloudflare Workers](https://itty.dev/itty-router/runtimes#Cloudflare%20Workers), itty can be used in browsers, Service Workers, edge functions, or standalone runtimes like [Node](https://itty.dev/itty-router/runtimes#Node), [Bun](https://itty.dev/itty-router/runtimes#Bun), etc.! +Itty aims to be the world's smallest (~440 bytes), feature-rich JavaScript router, enabling beautiful API code with a near-zero bundlesize. Designed originally for [Cloudflare Workers](https://itty.dev/itty-router/runtimes#Cloudflare%20Workers), itty can be used in browsers, Service Workers, edge functions, or standalone runtimes like [Node](https://itty.dev/itty-router/runtimes#Node), [Bun](https://itty.dev/itty-router/runtimes#Bun), etc.! ## Features: -- Absurdly tiny. The Router itself is ~440 bytes gzipped, and the **entire** library is under 1.5k! -- Absurdly easy to use. We believe route code should be self-evident, obvious, and read more like poetry than code. + +- Absurdly tiny. The Router itself is ~440 bytes gzipped, and the **entire** library is under 1.5k! +- Absurdly easy to use. We believe route code should be self-evident, obvious, and read more like poetry than code. - Absurdly agnostic. We leave **you** with full control over response types, matching order, upstream/downstream effects, etc. - Works [anywhere, in any environment](https://itty.dev/itty-router/runtimes). - [Fully typed/TypeScript support](https://itty.dev/itty-router/typescript), including hinting. -- Parses [route params](https://itty.dev/itty-router/route-patterns#params), +- Parses [route params](https://itty.dev/itty-router/route-patterns#params), [optional params](https://itty.dev/itty-router/route-patterns#optional), - [wildcards](https://itty.dev/itty-router/route-patterns#wildcards), + [wildcards](https://itty.dev/itty-router/route-patterns#wildcards), [greedy params](https://itty.dev/itty-router/route-patterns#greedy), and [file formats](https://itty.dev/itty-router/route-patterns#file-formats). - Automatic [query parsing](https://itty.dev/itty-router/route-patterns#query). - Easy [error handling](https://itty.dev/itty-router/errors), including throwing errors with HTTP status codes! -- Easy [Response](https://itty.dev/itty-router/responses) creation, with helpers for major formats (e.g. - [json](https://itty.dev/itty-router/api#json), - [html](https://itty.dev/itty-router/api#html), - [png](https://itty.dev/itty-router/api#png), +- Easy [Response](https://itty.dev/itty-router/responses) creation, with helpers for major formats (e.g. + [json](https://itty.dev/itty-router/api#json), + [html](https://itty.dev/itty-router/api#html), + [png](https://itty.dev/itty-router/api#png), [jpeg](https://itty.dev/itty-router/api#jpeg), etc.) - Deep APIs via [router nesting](https://itty.dev/itty-router/nesting). -- Full [middleware](https://itty.dev/itty-router/middleware) support. Includes the following by default: +- Full [middleware](https://itty.dev/itty-router/middleware) support. Includes the following by default: - [withParams](https://itty.dev/itty-router/api#withParams) - access the params directly off the `Request` (instead of `request.params`). - [withCookies](https://itty.dev/itty-router/api#withCookies) - access cookies in a convenient Object format. - [withContent](https://itty.dev/itty-router/api#withContent) - auto-parse Request bodies as `request.content`. @@ -79,35 +79,39 @@ Itty aims to be the world's smallest (~440 bytes), feature-rich JavaScript route - Fully readable regex... yeah right! 😆 ## [Full Documentation](https://itty.dev/itty-router) + Complete documentation/API is available at [itty.dev](https://itty.dev/itty-router), or join our [Discord](https://discord.com/channels/832353585802903572) channel to chat with community members for quick help! ## Installation + ``` npm install itty-router ``` ## Example + ```js -import { - error, // creates error responses - json, // creates JSON responses - Router, // the ~440 byte router itself - withParams, // middleware: puts params directly on the Request +import { + error, // creates error responses + json, // creates JSON responses + Router, // the ~440 byte router itself + withParams, // middleware: puts params directly on the Request } from 'itty-router' import { todos } from './external/todos' // create a new Router -const router = Router() +const router = Router() router // add some middleware upstream on all routes - .all('*', withParams) + .all('*', withParams) // GET list of todos .get('/todos', () => todos) // GET single todo, by ID - .get('/todos/:id', + .get( + '/todos/:id', ({ id }) => todos.getById(id) || error(404, 'That todo was not found') ) @@ -116,19 +120,22 @@ router // Example: Cloudflare Worker module syntax export default { - fetch: (request, ...args) => router - .handle(request, ...args) - .then(json) // send as JSON - .catch(error) // catch errors + fetch: (request, ...args) => + router + .handle(request, ...args) + .then(json) // send as JSON + .catch(error), // catch errors } ``` ## Join the Discussion! + Have a question? Suggestion? Complaint? Want to send a gift basket? Join us on [Discord](https://discord.com/channels/832353585802903572)! ## Testing and Contributing + 1. Fork repo 1. Install dev dependencies via `yarn` 1. Start test runner/dev mode `yarn dev` @@ -138,21 +145,29 @@ Join us on [Discord](https://discord.com/channels/832353585802903572)! 1. I'll add you to the credits! :) ## Special Thanks: Contributors -These folks are the real heroes, making open source the powerhouse that it is! Help out and get your name added to this list! <3 + +These folks are the real heroes, making open source the powerhouse that it is! Help out and get your name added to this list! <3 #### Core Concepts + - [@mvasigh](https://github.com/mvasigh) - proxy hack wizard behind itty, coding partner in crime, maker of the entire doc site, etc, etc. - [@hunterloftis](https://github.com/hunterloftis) - router.handle() method now accepts extra arguments and passed them to route functions - [@SupremeTechnopriest](https://github.com/SupremeTechnopriest) - improved TypeScript support and documentation! :D + #### Code Golfing + - [@taralx](https://github.com/taralx) - router internal code-golfing refactor for performance and character savings - [@DrLoopFall](https://github.com/DrLoopFall) - v4.x re-minification + #### Fixes & Build + - [@taralx](https://github.com/taralx) - QOL fixes for contributing (dev dep fix and test file consistency) <3 -- [@technoyes](https://github.com/technoyes) - three kind-of-a-big-deal errors fixed. Imagine the look on my face... thanks man!! :) +- [@technoyes](https://github.com/technoyes) - three kind-of-a-big-deal errors fixed. Imagine the look on my face... thanks man!! :) - [@roojay520](https://github.com/roojay520) - TS interface fixes - [@jahands](https://github.com/jahands) - v4.x TS fixes + #### Documentation + - [@arunsathiya](https://github.com/arunsathiya), [@poacher2k](https://github.com/poacher2k), [@ddarkr](https://github.com/ddarkr), diff --git a/example/bun.ts b/example/bun.ts index 7b40edab..32d1996d 100644 --- a/example/bun.ts +++ b/example/bun.ts @@ -1,9 +1,4 @@ -import { - Router, - error, - json, - withParams -} from '../dist/index.js' +import { Router, error, json, withParams } from '../dist/index.js' const router = Router() @@ -15,8 +10,6 @@ router export default { port: 3001, - fetch: (request, env, ctx) => router - .handle(request, env, ctx) - .then(json) - .catch(error) + fetch: (request, env, ctx) => + router.handle(request, env, ctx).then(json).catch(error), } diff --git a/example/cors-types.ts b/example/cors-types.ts index e73779bb..0391b93c 100644 --- a/example/cors-types.ts +++ b/example/cors-types.ts @@ -1,9 +1,4 @@ -import { - IRequest, - IRequestStrict, - Router, - createCors, -} from '../src' +import { IRequest, IRequestStrict, Router, createCors } from '../src' type FooRequest = { foo: string @@ -18,10 +13,7 @@ type Env = { KV: string } -type CF = [ - env: Env, - ctx: ExecutionContext -] +type CF = [env: Env, ctx: ExecutionContext] // this router defines a global signature of const custom = Router() @@ -93,12 +85,8 @@ router .handle({ method: 'GET', url: 'foo.bar' }, {}, 'asd') -type CFfetch = [ - request: Request, - env: Env, - ctx: ExecutionContext -] +type CFfetch = [request: Request, env: Env, ctx: ExecutionContext] export default { - fetch: (...args: CFfetch) => router.handle(...args) + fetch: (...args: CFfetch) => router.handle(...args), } diff --git a/example/index.ts b/example/index.ts index 003f0f39..b7b14b9d 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,8 +1,8 @@ import { - Router, // the router itself - IRequest, // lightweight/generic Request type - RouterType, // generic Router type - Route, // generic Route type + Router, // the router itself + IRequest, // lightweight/generic Request type + RouterType, // generic Router type + Route, // generic Route type createCors, json, error, @@ -10,7 +10,7 @@ import { // declare a custom Router type with used methods interface CustomRouter extends RouterType { - puppy: Route, + puppy: Route } // declare a custom Request type to allow request injection from middleware @@ -41,11 +41,7 @@ router // CF ES6 module syntax export default { fetch: (request: IRequest, env: object, context: object) => - router - .handle(request, env, context) - .then(json) - .catch(error) - .then(corsify) + router.handle(request, env, context).then(json).catch(error).then(corsify), } // test traditional eventListener Worker syntax diff --git a/example/node.js b/example/node.js index 005c45ed..dc5d0046 100644 --- a/example/node.js +++ b/example/node.js @@ -1,23 +1,17 @@ import { createServerAdapter } from '@whatwg-node/server' import { createServer } from 'http' import 'isomorphic-fetch' -import { - Router, - error, - json -} from '../dist/index.js' +import { Router, error, json } from '../dist/index.js' const router = Router() -router - .get('/', () => 'Success!') - .all('*', () => error(404)) +router.get('/', () => 'Success!').all('*', () => error(404)) -const ittyServer = createServerAdapter( - (...args) => router - .handle(...args) - .then(json) - .catch(error) +const ittyServer = createServerAdapter((...args) => + router + .handle(...args) + .then(json) + .catch(error) ) // Then use it in any environment diff --git a/example/request-types.ts b/example/request-types.ts index 85b3ab60..45033bb1 100644 --- a/example/request-types.ts +++ b/example/request-types.ts @@ -1,8 +1,4 @@ -import { - IRequest, - IRequestStrict, - Router -} from '../src/Router' +import { IRequest, IRequestStrict, Router } from '../src/Router' type FooRequest = { foo: string @@ -17,16 +13,13 @@ type Env = { KV: string } -type CF = [ - env: Env, - ctx: ExecutionContext -] +type CF = [env: Env, ctx: ExecutionContext] // this router defines a global signature of const custom = Router() custom - .get('/',({ bar, json }) => { + .get('/', ({ bar, json }) => { console.log('bar', bar) }) @@ -45,13 +38,13 @@ router // call custom HTTP method .puppy('/cat/:id', (request) => { // supports standard Request by default - request.headers // valid - request.foo // valid (generic traps) + request.headers // valid + request.foo // valid (generic traps) }) .puppy('/cat/:id', (request) => { // supports standard Request by default - request.headers // valid - request.foo // invalid + request.headers // valid + request.foo // invalid }) // standard request @@ -97,12 +90,8 @@ router .handle({ method: 'GET', url: 'foo.bar' }, {}, 'asd') -type CFfetch = [ - request: Request, - env: Env, - ctx: ExecutionContext -] +type CFfetch = [request: Request, env: Env, ctx: ExecutionContext] export default { - fetch: (...args: CFfetch) => router.handle(...args) + fetch: (...args: CFfetch) => router.handle(...args), } diff --git a/example/runtimes/bun.ts b/example/runtimes/bun.ts index 0ea49c92..5b01bdbf 100644 --- a/example/runtimes/bun.ts +++ b/example/runtimes/bun.ts @@ -1,9 +1,4 @@ -import { - Router, - error, - json, - withParams -} from 'itty-router' +import { Router, error, json, withParams } from 'itty-router' const router = Router() @@ -15,8 +10,6 @@ router export default { port: 3001, - fetch: (request, env, ctx) => router - .handle(request, env, ctx) - .then(json) - .catch(error) + fetch: (request, env, ctx) => + router.handle(request, env, ctx).then(json).catch(error), } diff --git a/example/runtimes/express.js b/example/runtimes/express.js index 581cc372..02065846 100644 --- a/example/runtimes/express.js +++ b/example/runtimes/express.js @@ -1,18 +1,12 @@ import express from 'express' -import { - Router, - error, - json -} from 'itty-router' +import { Router, error, json } from 'itty-router' import 'isomorphic-fetch' const app = express() const router = Router() -router - .get('/', () => 'Success!') - .all('*', () => error(404)) +router.get('/', () => 'Success!').all('*', () => error(404)) const handle = (request) => router.handle(request).then(json).catch(error) diff --git a/example/runtimes/node.js b/example/runtimes/node.js index 005c45ed..dc5d0046 100644 --- a/example/runtimes/node.js +++ b/example/runtimes/node.js @@ -1,23 +1,17 @@ import { createServerAdapter } from '@whatwg-node/server' import { createServer } from 'http' import 'isomorphic-fetch' -import { - Router, - error, - json -} from '../dist/index.js' +import { Router, error, json } from '../dist/index.js' const router = Router() -router - .get('/', () => 'Success!') - .all('*', () => error(404)) +router.get('/', () => 'Success!').all('*', () => error(404)) -const ittyServer = createServerAdapter( - (...args) => router - .handle(...args) - .then(json) - .catch(error) +const ittyServer = createServerAdapter((...args) => + router + .handle(...args) + .then(json) + .catch(error) ) // Then use it in any environment diff --git a/package.json b/package.json index 036f250e..03c83f85 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,16 @@ ], "scripts": { "dev": "yarn test", - "lint": "npx eslint src", + "lint": "yarn run eslint src", + "prettier": "prettier --write src test example", + "format": "yarn lint && yarn prettier", "test": "vitest --coverage --reporter verbose", "test:once": "vitest run", "coverage": "vitest run --coverage", "coveralls": "yarn coverage && cat ./coverage/lcov.info | coveralls", "verify": "echo 'verifying module...' && yarn build && yarn test:once", "prerelease": "yarn verify", - "prebuild": "rimraf dist && mkdir dist && yarn coverage && yarn lint", + "prebuild": "rimraf dist && mkdir dist && yarn coverage && yarn format", "build": "rollup -c", "release": "release --tag --push --patch --src=dist", "runtime:bun": "bun example/bun.ts", @@ -56,6 +58,7 @@ "@whatwg-node/server": "^0.8.1", "coveralls": "^3.1.1", "eslint": "^8.41.0", + "eslint-config-prettier": "^8.8.0", "fetch-mock": "^9.11.0", "fs-extra": "^11.1.1", "globby": "^13.1.4", @@ -65,6 +68,7 @@ "itty-router": "^4.0.6", "jsdom": "^22.1.0", "npm-run-all": "^4.1.5", + "prettier": "^2.8.8", "rimraf": "^5.0.1", "rollup": "^3.23.0", "rollup-plugin-bundle-size": "^1.0.3", diff --git a/rollup.config.mjs b/rollup.config.mjs index 51a67652..52a6c841 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -6,39 +6,37 @@ import copy from 'rollup-plugin-copy' export default async () => { const files = await globby('./src/*.ts', { - ignore: [ - '**/*.spec.ts', - 'example', - ] + ignore: ['**/*.spec.ts', 'example'], }) console.log({ files }) - return files - .map(path => ({ - input: path, - output: [ - { - format: 'esm', - file: path.replace('/src/', '/dist/').replace('.ts', '.js'), - // sourcemap: true, - }, - { - format: 'cjs', - file: path.replace('/src/', '/dist/cjs/').replace('.ts', '.js'), - // sourcemap: true, - }, - ], - plugins: [ - typescript({ sourceMap: false }), - terser(), - bundleSize(), - copy({ - targets: [ - { src: ['CONTRIBUTING.md', 'CODE-OF-CONDUCT.md', 'LICENSE'], dest: 'dist' }, - ] - }) - ], - }) - ) + return files.map((path) => ({ + input: path, + output: [ + { + format: 'esm', + file: path.replace('/src/', '/dist/').replace('.ts', '.js'), + // sourcemap: true, + }, + { + format: 'cjs', + file: path.replace('/src/', '/dist/cjs/').replace('.ts', '.js'), + // sourcemap: true, + }, + ], + plugins: [ + typescript({ sourceMap: false }), + terser(), + bundleSize(), + copy({ + targets: [ + { + src: ['CONTRIBUTING.md', 'CODE-OF-CONDUCT.md', 'LICENSE'], + dest: 'dist', + }, + ], + }), + ], + })) } diff --git a/src/Router.spec.ts b/src/Router.spec.ts index b6ab712a..e148f581 100644 --- a/src/Router.spec.ts +++ b/src/Router.spec.ts @@ -1,6 +1,6 @@ import 'isomorphic-fetch' import { describe, expect, it, vi } from 'vitest' -import { buildRequest, createTestRunner, extract } from '../test-utils' +import { buildRequest, createTestRunner, extract } from '../test' import { Router } from './Router' const ERROR_MESSAGE = 'Error Message' @@ -52,15 +52,15 @@ describe('Router', () => { }) it('allows preloading advanced routes', async () => { - const basicHandler = vi.fn(req => req.params) - const customHandler = vi.fn(req => req.params) + const basicHandler = vi.fn((req) => req.params) + const customHandler = vi.fn((req) => req.params) const router = Router({ - routes: [ - [ 'GET', /^\/test\.(?[^/]+)\/*$/, [basicHandler] ], - [ 'GET', /^\/custom-(?\d{2,4})$/, [customHandler] ], - ] - }) + routes: [ + ['GET', /^\/test\.(?[^/]+)\/*$/, [basicHandler]], + ['GET', /^\/custom-(?\d{2,4})$/, [customHandler]], + ], + }) await router.handle(buildRequest({ path: '/test.a.b' })) expect(basicHandler).toHaveReturnedWith({ x: 'a.b' }) @@ -73,12 +73,12 @@ describe('Router', () => { }) it('allows loading advanced routes after config', async () => { - const handler = vi.fn(req => req.params) + const handler = vi.fn((req) => req.params) const router = Router() // allows manual loading (after config) - router.routes.push([ 'GET', /^\/custom2-(?\w\d{3})$/, [handler] ]) + router.routes.push(['GET', /^\/custom2-(?\w\d{3})$/, [handler]]) await router.handle(buildRequest({ path: '/custom2-a456' })) expect(handler).toHaveReturnedWith({ custom: 'a456' }) // custom route hit @@ -87,13 +87,15 @@ describe('Router', () => { describe('.{method}(route: string, handler1: function, ..., handlerN: function)', () => { it('can accept multiple handlers (each mutates request)', async () => { const r = Router() - const handler1 = vi.fn(req => { req.a = 1 }) - const handler2 = vi.fn(req => { + const handler1 = vi.fn((req) => { + req.a = 1 + }) + const handler2 = vi.fn((req) => { req.b = 2 return req }) - const handler3 = vi.fn(req => ({ c: 3, ...req })) + const handler3 = vi.fn((req) => ({ c: 3, ...req })) r.get('/multi/:id', handler1, handler2, handler3) await r.handle(buildRequest({ path: '/multi/foo' })) @@ -103,7 +105,7 @@ describe('Router', () => { }) }) - describe('.handle({ method = \'GET\', url })', () => { + describe(`.handle({ method = 'GET', url })`, () => { it('always returns a Promise', () => { const syncRouter = Router() syncRouter.get('/foo', () => 3) @@ -115,7 +117,7 @@ describe('Router', () => { }) it('returns { path, query } from match', async () => { - const route = routes.find(r => r.path === '/foo/:id') + const route = routes.find((r) => r.path === '/foo/:id') await router.handle(buildRequest({ path: '/foo/13?foo=bar&cat=dog' })) expect(route?.callback).toHaveReturnedWith({ @@ -125,7 +127,7 @@ describe('Router', () => { }) it('BUG: avoids toString prototype bug', async () => { - const route = routes.find(r => r.path === '/foo/:id') + const route = routes.find((r) => r.path === '/foo/:id') await router.handle(buildRequest({ path: '/foo/13?toString=value' })) expect(route?.callback).toHaveReturnedWith({ @@ -135,7 +137,7 @@ describe('Router', () => { }) it('requires exact route match', async () => { - const route = routes.find(r => r.path === '/') + const route = routes.find((r) => r.path === '/') await router.handle(buildRequest({ path: '/foo' })) @@ -148,9 +150,7 @@ describe('Router', () => { const handler = vi.fn(({ method, route }) => ({ method, route })) const router = Router() - router - .get(route1, handler) - .post(route2, handler) + router.get(route1, handler).post(route2, handler) await router.handle(buildRequest({ path: route1, method: 'GET' })) expect(handler).toHaveReturnedWith({ method: 'GET', route: route1 }) @@ -176,15 +176,17 @@ describe('Router', () => { }) it('honors correct method (e.g. GET, POST, etc)', async () => { - const route = routes.find(r => r.path === '/foo' && r.method === 'post') + const route = routes.find((r) => r.path === '/foo' && r.method === 'post') await router.handle(buildRequest({ method: 'POST', path: '/foo' })) expect(route.callback).toHaveBeenCalled() }) it('passes the entire original request through to the handler', async () => { - const route = routes.find(r => r.path === '/passthrough') - await router.handle(buildRequest({ path: '/passthrough', name: 'miffles' })) + const route = routes.find((r) => r.path === '/passthrough') + await router.handle( + buildRequest({ path: '/passthrough', name: 'miffles' }) + ) expect(route.callback).toHaveReturnedWith({ path: '/passthrough', @@ -200,9 +202,7 @@ describe('Router', () => { const router2 = Router({ base: '/nested' }) router2.get('/foo', matchHandler) - router1 - .all('/nested/*', router2.handle) - .all('*', missingHandler) + router1.all('/nested/*', router2.handle).all('*', missingHandler) await router1.handle(buildRequest({ path: '/foo' })) expect(missingHandler).toHaveBeenCalled() @@ -211,18 +211,20 @@ describe('Router', () => { expect(matchHandler).toHaveBeenCalled() }) - it('won\'t throw on unknown method', () => { - expect(() => router.handle({ method: 'CUSTOM', url: 'https://example.com/foo' })).not.toThrow() + it(`won't throw on unknown method`, () => { + expect(() => + router.handle({ method: 'CUSTOM', url: 'https://example.com/foo' }) + ).not.toThrow() }) it('can match multiple routes if earlier handlers do not return (as middleware)', async () => { const r = Router() - const middleware = req => { + const middleware = (req) => { req.user = { id: 13 } } - const handler = vi.fn(req => req.user.id) + const handler = vi.fn((req) => req.user.id) r.get('/middleware/*', middleware) r.get('/middleware/:id', handler) @@ -256,7 +258,7 @@ describe('Router', () => { it('can pull route params from the basepath as well', async () => { const router = Router({ base: '/:collection' }) - const handler = vi.fn(req => req.params) + const handler = vi.fn((req) => req.params) router.get('/:id', handler) await router.handle(buildRequest({ path: '/todos/13' })) @@ -300,15 +302,15 @@ describe('Router', () => { it('stops at a handler that throws', async () => { const router = Router() const handler1 = vi.fn() - const handler2 = vi.fn(() => { throw new Error() }) + const handler2 = vi.fn(() => { + throw new Error() + }) const handler3 = vi.fn() router.get('/foo', handler1, handler2, handler3) - const escape = err => err + const escape = (err) => err - await router - .handle(buildRequest({ path: '/foo' })) - .catch(escape) + await router.handle(buildRequest({ path: '/foo' })).catch(escape) expect(handler1).toHaveBeenCalled() expect(handler2).toHaveBeenCalled() @@ -317,14 +319,14 @@ describe('Router', () => { it('can throw an error and still handle if using catch', async () => { const router = Router() - const handlerWithError = vi.fn(() => { throw new Error(ERROR_MESSAGE) }) - const errorHandler = vi.fn(err => err.message) + const handlerWithError = vi.fn(() => { + throw new Error(ERROR_MESSAGE) + }) + const errorHandler = vi.fn((err) => err.message) router.get('/foo', handlerWithError) - await router - .handle(buildRequest({ path: '/foo' })) - .catch(errorHandler) + await router.handle(buildRequest({ path: '/foo' })).catch(errorHandler) expect(handlerWithError).toHaveBeenCalled() expect(errorHandler).toHaveBeenCalled() @@ -343,18 +345,17 @@ describe('Router', () => { const middleware = vi.fn() const errorHandler = vi.fn(() => errorResponse) - router - .post('*', middleware, handler) - .all('*', errorHandler) + router.post('*', middleware, handler).all('*', errorHandler) // creates a request (with passed method) with JSON body - const createRequest = method => new Request('https://foo.com/foo', { - method, - headers: { - 'content-type': 'application/json' - }, - body: JSON.stringify({ foo: 'bar' }) - }) + const createRequest = (method) => + new Request('https://foo.com/foo', { + method, + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ foo: 'bar' }), + }) // test POST with JSON body (catch by post handler) let response = await router.handle(createRequest('post')) @@ -375,15 +376,22 @@ describe('Router', () => { it('can easily create a ThrowableRouter', async () => { const error = (status, message) => new Response(message, { status }) - const ThrowableRouter = options => new Proxy(Router(options), { - get: (obj, prop) => (...args) => - prop === 'handle' - ? obj[prop](...args).catch(err => error(err.status || 500, err.message)) - : obj[prop](...args) - }) + const ThrowableRouter = (options) => + new Proxy(Router(options), { + get: + (obj, prop) => + (...args) => + prop === 'handle' + ? obj[prop](...args).catch((err) => + error(err.status || 500, err.message) + ) + : obj[prop](...args), + }) const router = ThrowableRouter() - const handlerWithError = vi.fn(() => { throw new Error(ERROR_MESSAGE) }) + const handlerWithError = vi.fn(() => { + throw new Error(ERROR_MESSAGE) + }) router.get('/foo', handlerWithError) @@ -398,22 +406,22 @@ describe('Router', () => { const router = Router() expect(() => { - router - .get('/foo', vi.fn()) - .get('/foo', vi.fn()) - + router.get('/foo', vi.fn()).get('/foo', vi.fn()) }).not.toThrow() }) }) - describe('.handle({ method = \'GET\', url }, ...args)', () => { + describe(`.handle({ method = 'GET', url }, ...args)`, () => { it('passes extra args to each handler', async () => { const r = Router() - const h = (req, a, b) => { req.a = a; req.b = b } + const h = (req, a, b) => { + req.a = a + req.b = b + } const originalA = 'A' const originalB = {} r.get('*', h) - const req = buildRequest({ path: '/foo', }) + const req = buildRequest({ path: '/foo' }) await r.handle(req, originalA, originalB) @@ -423,10 +431,10 @@ describe('Router', () => { it('will pass request.proxy instead of request if found', async () => { const router = Router() - const handler = vi.fn(req => req) + const handler = vi.fn((req) => req) let proxy - const withProxy = request => { + const withProxy = (request) => { request.proxy = proxy = new Proxy(request, {}) } @@ -439,20 +447,19 @@ describe('Router', () => { it('can handle POST body even if not used', async () => { const router = Router() - const handler = vi.fn(req => req.json()) + const handler = vi.fn((req) => req.json()) const errorHandler = vi.fn() - router - .post('/foo', handler) - .all('*', errorHandler) + router.post('/foo', handler).all('*', errorHandler) - const createRequest = method => new Request('https://foo.com/foo', { - method, - headers: { - 'content-type': 'application/json' - }, - body: JSON.stringify({ foo: 'bar' }) - }) + const createRequest = (method) => + new Request('https://foo.com/foo', { + method, + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ foo: 'bar' }), + }) await router.handle(createRequest('put')) expect(errorHandler).toHaveBeenCalled() @@ -465,28 +472,34 @@ describe('Router', () => { it('can get query params', async () => { const router = Router() - const handler = vi.fn(req => req.query) + const handler = vi.fn((req) => req.query) router.get('/foo', handler) - const request = new Request('https://foo.com/foo?cat=dog&foo=bar&foo=baz&missing=') + const request = new Request( + 'https://foo.com/foo?cat=dog&foo=bar&foo=baz&missing=' + ) await router.handle(request) - expect(handler).toHaveReturnedWith({ cat: 'dog', foo: ['bar', 'baz'], missing: '' }) + expect(handler).toHaveReturnedWith({ + cat: 'dog', + foo: ['bar', 'baz'], + missing: '', + }) }) it('can still get query params with POST or non-GET HTTP methods', async () => { const router = Router() - const handler = vi.fn(req => req.query) + const handler = vi.fn((req) => req.query) router.post('/foo', handler) const request = new Request('https://foo.com/foo?cat=dog&foo=bar&foo=baz', { method: 'POST', headers: { - 'content-type': 'application/json' + 'content-type': 'application/json', }, - body: JSON.stringify({ success: true }) + body: JSON.stringify({ success: true }), }) await router.handle(request) @@ -496,10 +509,8 @@ describe('Router', () => { describe('ROUTE MATCHING', () => { describe('allowed characters', () => { - const chars = '/foo/-.abc!@%&_=:;\',~|/bar' - testRoutes([ - { route: chars, path: chars }, - ]) + const chars = `/foo/-.abc!@%&_=:;',~|/bar` + testRoutes([{ route: chars, path: chars }]) }) describe('dots', () => { @@ -513,23 +524,55 @@ describe('ROUTE MATCHING', () => { testRoutes([ { route: '/foo/:id+', path: '/foo/14', returns: { id: '14' } }, { route: '/foo/:id+', path: '/foo/bar/baz', returns: { id: 'bar/baz' } }, - { route: '/foo/:id+', path: '/foo/https://foo.bar', returns: { id: 'https://foo.bar' } }, + { + route: '/foo/:id+', + path: '/foo/https://foo.bar', + returns: { id: 'https://foo.bar' }, + }, ]) }) describe('formats/extensions', () => { testRoutes([ { route: '/:id.:format', path: '/foo', returns: false }, - { route: '/:id.:format', path: '/foo.jpg', returns: { id: 'foo', format: 'jpg' } }, - { route: '/:id.:format', path: '/foo.bar.jpg', returns: { id: 'foo.bar', format: 'jpg' } }, + { + route: '/:id.:format', + path: '/foo.jpg', + returns: { id: 'foo', format: 'jpg' }, + }, + { + route: '/:id.:format', + path: '/foo.bar.jpg', + returns: { id: 'foo.bar', format: 'jpg' }, + }, { route: '/:id.:format?', path: '/foo', returns: { id: 'foo' } }, - { route: '/:id.:format?', path: '/foo.bar.jpg', returns: { id: 'foo.bar', format: 'jpg' } }, - { route: '/:id.:format?', path: '/foo.jpg', returns: { id: 'foo', format: 'jpg' } }, + { + route: '/:id.:format?', + path: '/foo.bar.jpg', + returns: { id: 'foo.bar', format: 'jpg' }, + }, + { + route: '/:id.:format?', + path: '/foo.jpg', + returns: { id: 'foo', format: 'jpg' }, + }, { route: '/:id.:format?', path: '/foo', returns: { id: 'foo' } }, { route: '/:id.:format.:compress', path: '/foo.gz', returns: false }, - { route: '/:id.:format.:compress', path: '/foo.txt.gz', returns: { id: 'foo', format: 'txt', compress: 'gz' } }, - { route: '/:id.:format.:compress?', path: '/foo.txt', returns: { id: 'foo', format: 'txt' } }, - { route: '/:id.:format?.:compress', path: '/foo.gz', returns: { id: 'foo', compress: 'gz' } }, + { + route: '/:id.:format.:compress', + path: '/foo.txt.gz', + returns: { id: 'foo', format: 'txt', compress: 'gz' }, + }, + { + route: '/:id.:format.:compress?', + path: '/foo.txt', + returns: { id: 'foo', format: 'txt' }, + }, + { + route: '/:id.:format?.:compress', + path: '/foo.gz', + returns: { id: 'foo', compress: 'gz' }, + }, ]) }) diff --git a/src/Router.ts b/src/Router.ts index 8f33db11..53d25c1b 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -3,21 +3,21 @@ export type GenericTraps = { } export type RequestLike = { - method: string, - url: string, + method: string + url: string } & GenericTraps export type IRequestStrict = { - method: string, - url: string, - route: string, + method: string + url: string + route: string params: { - [key: string]: string, - }, + [key: string]: string + } query: { - [key: string]: string | string[] | undefined, - }, - proxy?: any, + [key: string]: string | string[] | undefined + } + proxy?: any } & Request export type IRequest = IRequestStrict & GenericTraps @@ -34,77 +34,112 @@ export type RouteHandler = { export type RouteEntry = [string, RegExp, RouteHandler[], string] // this is the generic "Route", which allows per-route overrides -export type Route = ( +export type Route = < + RequestType = IRequest, + Args extends any[] = any[], + RT = RouterType +>( path: string, ...handlers: RouteHandler[] ) => RT // this is an alternative UniveralRoute, accepting generics (from upstream), but without // per-route overrides -export type UniversalRoute = ( +export type UniversalRoute< + RequestType = IRequest, + Args extends any[] = any[] +> = ( path: string, ...handlers: RouteHandler[] ) => RouterType, Args> // helper function to detect equality in types (used to detect custom Request on router) -type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true : false; +type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y + ? 1 + : 2 + ? true + : false export type CustomRoutes = { - [key: string]: R, + [key: string]: R } export type RouterType = { - __proto__: RouterType, - routes: RouteEntry[], - handle: (request: RequestLike, ...extra: Equal extends true ? A : Args) => Promise - all: R, - delete: R, - get: R, - head: R, - options: R, - patch: R, - post: R, - put: R, + __proto__: RouterType + routes: RouteEntry[] + handle: ( + request: RequestLike, + ...extra: Equal extends true ? A : Args + ) => Promise + all: R + delete: R + get: R + head: R + options: R + patch: R + post: R + put: R } & CustomRoutes export const Router = < RequestType = IRequest, Args extends any[] = any[], - RouteType = Equal extends true ? Route : UniversalRoute ->({ base = '', routes = [] }: RouterOptions = {}): RouterType => + RouteType = Equal extends true + ? Route + : UniversalRoute +>({ base = '', routes = [] }: RouterOptions = {}): RouterType< + RouteType, + Args +> => // @ts-expect-error TypeScript doesn't know that Proxy makes this work ({ - __proto__: new Proxy({}, { - // @ts-expect-error (we're adding an expected prop "path" to the get) - get: (target: any, prop: string, receiver: object, path: string) => (route: string, ...handlers: RouteHandler[]) => - routes.push( - [ - prop.toUpperCase(), - RegExp(`^${(path = (base + '/' + route).replace(/\/+(\/|$)/g, '$1')) // strip double & trailing splash - .replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))') // greedy params - .replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))') // named params and image format - .replace(/\./g, '\\.') // dot in path - .replace(/(\/?)\*/g, '($1.*)?') // wildcard - }/*$`), - handlers, // embed handlers - path, // embed clean route path - ] - ) && receiver - }), + __proto__: new Proxy( + {}, + { + // @ts-expect-error (we're adding an expected prop "path" to the get) + get: + (target: any, prop: string, receiver: object, path: string) => + (route: string, ...handlers: RouteHandler[]) => + routes.push([ + prop.toUpperCase(), + RegExp( + `^${ + (path = (base + '/' + route).replace(/\/+(\/|$)/g, '$1')) // strip double & trailing splash + .replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))') // greedy params + .replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))') // named params and image format + .replace(/\./g, '\\.') // dot in path + .replace(/(\/?)\*/g, '($1.*)?') // wildcard + }/*$` + ), + handlers, // embed handlers + path, // embed clean route path + ]) && receiver, + } + ), routes, - async handle (request: RequestLike, ...args) { - let response, match, url = new URL(request.url), query: any = request.query = { __proto__: null } + async handle(request: RequestLike, ...args) { + let response, + match, + url = new URL(request.url), + query: any = (request.query = { __proto__: null }) for (let [k, v] of url.searchParams) { query[k] = query[k] === undefined ? v : [query[k], v].flat() } for (let [method, regex, handlers, path] of routes) { - if ((method === request.method || method === 'ALL') && (match = url.pathname.match(regex))) { - request.params = match.groups || {} // embed params in request - request.route = path // embed route path in request + if ( + (method === request.method || method === 'ALL') && + (match = url.pathname.match(regex)) + ) { + request.params = match.groups || {} // embed params in request + request.route = path // embed route path in request for (let handler of handlers) { - if ((response = await handler(request.proxy || request, ...args)) !== undefined) return response + if ( + (response = await handler(request.proxy || request, ...args)) !== + undefined + ) + return response } } } - } + }, }) diff --git a/src/StatusError.ts b/src/StatusError.ts index a715eaa1..91e2fcad 100644 --- a/src/StatusError.ts +++ b/src/StatusError.ts @@ -1,10 +1,10 @@ type StatusErrorObject = { - error?: string, + error?: string [key: string]: any } export class StatusError extends Error { - status: number + status: number; [key: string]: any constructor(status = 500, body?: StatusErrorObject | string) { diff --git a/src/createCors.spec.ts b/src/createCors.spec.ts index b8f71c55..06d74dc3 100644 --- a/src/createCors.spec.ts +++ b/src/createCors.spec.ts @@ -15,7 +15,7 @@ describe('createCors(options)', () => { describe('options', () => { it('maxAge', async () => { const { preflight } = createCors({ - maxAge: 60 + maxAge: 60, }) const router = Router().all('*', preflight) const request = new Request('https://foo.bar', { @@ -23,8 +23,8 @@ describe('createCors(options)', () => { headers: { 'Access-Control-Request-Method': 'GET', 'Access-Control-Request-Headers': 'content-type', - 'origin': 'http://localhost:3000', - } + origin: 'http://localhost:3000', + }, }) const response = await router.handle(request) @@ -41,12 +41,14 @@ describe('createCors(options)', () => { headers: { 'Access-Control-Request-Method': 'GET', 'Access-Control-Request-Headers': 'content-type', - 'origin': 'http://localhost:3000', - } + origin: 'http://localhost:3000', + }, }) const response = await router.handle(request) - expect(response.headers.get('Access-Control-Allow-Origin')).toBe('http://localhost:3000') + expect(response.headers.get('Access-Control-Allow-Origin')).toBe( + 'http://localhost:3000' + ) }) it('should handle OPTIONS requests without standard headers via Allow (methods) header', async () => { @@ -64,13 +66,10 @@ describe('createCors(options)', () => { const { preflight, corsify } = createCors() const catchError = vi.fn() const router = Router() - .all('*', preflight) - .get('/foo', () => json(13)) + .all('*', preflight) + .get('/foo', () => json(13)) const request = new Request('https://foo.bar/miss') - await router - .handle(request) - .then(corsify) - .catch(catchError) + await router.handle(request).then(corsify).catch(catchError) expect(catchError).toHaveBeenCalled() }) @@ -78,38 +77,44 @@ describe('createCors(options)', () => { it('should handle options requests', async () => { const { preflight, corsify } = createCors() const router = Router() - .all('*', preflight) - .get('/foo', () => json(13)) + .all('*', preflight) + .get('/foo', () => json(13)) const request = new Request('https://foo.bar/foo', { - headers: { - 'origin': 'http://localhost:3000', - } - }) - const response = await router - .handle(request) - .then(corsify) - - expect(response.headers.get('Access-Control-Allow-Origin')).toBe('http://localhost:3000') - expect((response.headers.get('Access-Control-Allow-Methods') || '').includes('GET')).toBe(true) + headers: { + origin: 'http://localhost:3000', + }, + }) + const response = await router.handle(request).then(corsify) + + expect(response.headers.get('Access-Control-Allow-Origin')).toBe( + 'http://localhost:3000' + ) + expect( + (response.headers.get('Access-Control-Allow-Methods') || '').includes( + 'GET' + ) + ).toBe(true) }) it('will not modify responses with existing CORS headers', async () => { const { preflight, corsify } = createCors() const router = Router() - .all('*', preflight) - .get('/foo', () => new Response(null, { - headers: { - 'access-control-allow-origin': '*' - } - })) + .all('*', preflight) + .get( + '/foo', + () => + new Response(null, { + headers: { + 'access-control-allow-origin': '*', + }, + }) + ) const request = new Request('https://foo.bar/foo', { - headers: { - 'origin': 'http://localhost:3000', - } - }) - const response = await router - .handle(request) - .then(corsify) + headers: { + origin: 'http://localhost:3000', + }, + }) + const response = await router.handle(request).then(corsify) expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*') }) @@ -117,16 +122,14 @@ describe('createCors(options)', () => { it('will not modify redirects or 101 status responses', async () => { const { preflight, corsify } = createCors() const router = Router() - .all('*', preflight) - .get('/foo', () => new Response(null, { status: 101 })) + .all('*', preflight) + .get('/foo', () => new Response(null, { status: 101 })) const request = new Request('https://foo.bar/foo', { - headers: { - 'origin': 'http://localhost:3000', - } - }) - const response = await router - .handle(request) - .then(corsify) + headers: { + origin: 'http://localhost:3000', + }, + }) + const response = await router.handle(request).then(corsify) expect(response.headers.get('Access-Control-Allow-Origin')).toBe(null) }) @@ -136,23 +139,28 @@ describe('createCors(options)', () => { const router = Router().all('*', preflight) const origin = 'http://localhost:3000' - const generateRequest = () => new Request('https://foo.bar', { - method: 'OPTIONS', - headers: { - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': 'content-type', - origin, - } - }) + const generateRequest = () => + new Request('https://foo.bar', { + method: 'OPTIONS', + headers: { + 'Access-Control-Request-Method': 'GET', + 'Access-Control-Request-Headers': 'content-type', + origin, + }, + }) it('will work multiple times in a row', async () => { const response1 = await router.handle(generateRequest()) expect(response1.status).toBe(200) - expect(response1.headers.get('Access-Control-Allow-Origin')).toBe(origin) + expect(response1.headers.get('Access-Control-Allow-Origin')).toBe( + origin + ) const response2 = await router.handle(generateRequest()) expect(response2.status).toBe(200) - expect(response2.headers.get('Access-Control-Allow-Origin')).toBe(origin) + expect(response2.headers.get('Access-Control-Allow-Origin')).toBe( + origin + ) }) }) }) diff --git a/src/createCors.ts b/src/createCors.ts index 746d7eea..40639e64 100644 --- a/src/createCors.ts +++ b/src/createCors.ts @@ -1,21 +1,16 @@ import { IRequest } from './Router' interface CorsOptions { - origins?: string[], - maxAge?: number, - methods?: string[], - headers?: any, + origins?: string[] + maxAge?: number + methods?: string[] + headers?: any } // Create CORS function with default options. export const createCors = (options: CorsOptions = {}) => { // Destructure and set defaults for options. - const { - origins = ['*'], - maxAge, - methods = ['GET'], - headers = {} - } = options + const { origins = ['*'], maxAge, methods = ['GET'], headers = {} } = options let allowOrigin: any @@ -23,7 +18,7 @@ export const createCors = (options: CorsOptions = {}) => { const rHeaders = { 'content-type': 'application/json', 'Access-Control-Allow-Methods': methods.join(', '), - ...headers + ...headers, } // Set max age if provided. @@ -36,35 +31,48 @@ export const createCors = (options: CorsOptions = {}) => { const origin = r.headers.get('origin') || '' // Set allowOrigin globally. - allowOrigin = (origins.includes(origin) || origins.includes('*')) && - { 'Access-Control-Allow-Origin': origin } + allowOrigin = (origins.includes(origin) || origins.includes('*')) && { + 'Access-Control-Allow-Origin': origin, + } // Check if method is OPTIONS. if (r.method === 'OPTIONS') { const reqHeaders = { ...rHeaders, 'Access-Control-Allow-Methods': useMethods.join(', '), - 'Access-Control-Allow-Headers': r.headers.get('Access-Control-Request-Headers'), - ...allowOrigin + 'Access-Control-Allow-Headers': r.headers.get( + 'Access-Control-Request-Headers' + ), + ...allowOrigin, } // Handle CORS pre-flight request. return new Response(null, { - headers: r.headers.get('Origin') && + headers: + r.headers.get('Origin') && r.headers.get('Access-Control-Request-Method') && - r.headers.get('Access-Control-Request-Headers') ? reqHeaders : { Allow: useMethods.join(', '), } + r.headers.get('Access-Control-Request-Headers') + ? reqHeaders + : { Allow: useMethods.join(', ') }, }) } } // Corsify function. const corsify = (response: Response): Response => { - if (!response) throw new Error('No fetch handler responded and no upstream to proxy to specified.') + if (!response) + throw new Error( + 'No fetch handler responded and no upstream to proxy to specified.' + ) const { headers, status, body } = response // Bypass for protocol shifts or redirects, or if CORS is already set. - if ([101, 301, 302, 308].includes(status) || headers.get('access-control-allow-origin')) return response + if ( + [101, 301, 302, 308].includes(status) || + headers.get('access-control-allow-origin') + ) + return response // Return new response with CORS headers. return new Response(body, { @@ -74,7 +82,7 @@ export const createCors = (options: CorsOptions = {}) => { ...rHeaders, ...allowOrigin, 'content-type': headers.get('content-type'), - } + }, }) } diff --git a/src/createResponse.spec.ts b/src/createResponse.spec.ts index e2d4e542..4346a598 100644 --- a/src/createResponse.spec.ts +++ b/src/createResponse.spec.ts @@ -36,7 +36,7 @@ describe('createResponse(mimeType: string, transform?: Function)', () => { }) it('can pass in custom body transform function', async () => { - const stars = createResponse('text/plain', s => s.replace(/./g, '*')) + const stars = createResponse('text/plain', (s) => s.replace(/./g, '*')) const response = stars('foo') const body = await response.text() diff --git a/src/createResponse.ts b/src/createResponse.ts index a7db7e81..ba9188d1 100644 --- a/src/createResponse.ts +++ b/src/createResponse.ts @@ -10,7 +10,11 @@ type ResponseFormatterOptions = { headers?: object } & ResponseInit -export const createResponse = (format = 'text/plain; charset=utf-8', transform?: BodyTransformer): ResponseFormatter => +export const createResponse = + ( + format = 'text/plain; charset=utf-8', + transform?: BodyTransformer + ): ResponseFormatter => (body, options: ResponseFormatterOptions = {}) => { const { headers = {}, ...rest } = options diff --git a/src/error.ts b/src/error.ts index a237b72c..0459e8e7 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,8 +1,8 @@ import { json } from './json' interface ErrorLike extends Error { - status?: number, - [any: string]: any, + status?: number + [any: string]: any } export type ErrorBody = string | object @@ -13,13 +13,15 @@ export interface ErrorFormatter { } const getMessage = (code: number): string => { - return { - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 500: 'Internal Server Error' - }[code] || 'Unknown Error' + return ( + { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 500: 'Internal Server Error', + }[code] || 'Unknown Error' + ) } export const error: ErrorFormatter = (a = 500, b?: ErrorBody) => { @@ -29,13 +31,13 @@ export const error: ErrorFormatter = (a = 500, b?: ErrorBody) => { a = a.status || 500 b = { error: message || getMessage(a), - ...err + ...err, } } b = { status: a, - ...(typeof b === 'object' ? b : { error: b || getMessage(a) }) + ...(typeof b === 'object' ? b : { error: b || getMessage(a) }), } return json(b, { status: a }) diff --git a/src/json.ts b/src/json.ts index d3324443..e9667ce7 100644 --- a/src/json.ts +++ b/src/json.ts @@ -2,5 +2,5 @@ import { createResponse } from './createResponse' export const json = createResponse( 'application/json; charset=utf-8', - JSON.stringify, + JSON.stringify ) diff --git a/src/status.ts b/src/status.ts index d9f1d9f6..ea08eedc 100644 --- a/src/status.ts +++ b/src/status.ts @@ -1,2 +1 @@ -export const status = (status: number) => - new Response(null, { status }) +export const status = (status: number) => new Response(null, { status }) diff --git a/src/withContent.spec.ts b/src/withContent.spec.ts index 970c91b8..cad8d6d5 100644 --- a/src/withContent.spec.ts +++ b/src/withContent.spec.ts @@ -10,14 +10,12 @@ describe('withContent (middleware)', () => { const request = new Request('https://foo.bar', { method: 'POST', headers: { - 'content-type': 'application/json' + 'content-type': 'application/json', }, - body: JSON.stringify({ foo: 'bar' }) + body: JSON.stringify({ foo: 'bar' }), }) - await router - .post('/', withContent, handler) - .handle(request) + await router.post('/', withContent, handler).handle(request) expect(handler).toHaveReturnedWith({ foo: 'bar' }) }) diff --git a/src/withCookies.spec.ts b/src/withCookies.spec.ts index f8444fe5..3d028985 100644 --- a/src/withCookies.spec.ts +++ b/src/withCookies.spec.ts @@ -9,13 +9,11 @@ describe('withCookies (middleware)', () => { const handler = vi.fn(({ cookies }) => cookies) const request = new Request('https://foo.bar', { headers: { - 'cookie': 'empty=; foo=bar' - } + cookie: 'empty=; foo=bar', + }, }) - await router - .get('/', withCookies, handler) - .handle(request) + await router.get('/', withCookies, handler).handle(request) expect(handler).toHaveReturnedWith({ foo: 'bar' }) }) @@ -26,9 +24,7 @@ describe('withCookies (middleware)', () => { const request = new Request('https://foo.bar') expect(async () => { - await router - .get('/', withCookies, handler) - .handle(request) + await router.get('/', withCookies, handler).handle(request) }).not.toThrow() }) }) diff --git a/src/withCookies.ts b/src/withCookies.ts index 9a14c5ed..5bbc2485 100644 --- a/src/withCookies.ts +++ b/src/withCookies.ts @@ -9,5 +9,5 @@ export const withCookies = (r: IRequest): void => { r.cookies = (r.headers.get('Cookie') || '') .split(/;\s*/) .map((p: string): KVPair => p.split(/=(.+)/) as KVPair) - .reduce((a: CookieObject, [k, v]: KVPair) => (v ? (a[k] = v, a) : a), {}) + .reduce((a: CookieObject, [k, v]: KVPair) => (v ? ((a[k] = v), a) : a), {}) } diff --git a/src/withParams.spec.ts b/src/withParams.spec.ts index 7af618ac..0ecc0286 100644 --- a/src/withParams.spec.ts +++ b/src/withParams.spec.ts @@ -6,27 +6,67 @@ describe('withParams (middleware)', () => { it('allows accessing route params from the request itself', async () => { const router = Router() const handler = vi.fn(({ id, method }) => ({ id, method })) - const request = { method: 'GET', url: 'https://foo.bar/baz' } - await router - .get('/:id', withParams, handler) - .handle(request) + await router.get('/:id', withParams, handler).handle(request) expect(handler).toHaveReturnedWith({ id: 'baz', method: 'GET' }) }) + it('will not interfere with existing props', async () => { + const router = Router() + const handler = vi.fn(({ id, method, foo, testParam }) => ({ + id, + method, + foo, + testParam, + })) + const request = { method: 'GET', url: 'https://foo.bar/baz', foo: 'bar' } + + await router.get('/:foo', withParams, handler).handle(request) + + // foo should be bar (from the original request), not baz (from the params) + expect(handler).toHaveReturnedWith({ + method: 'GET', + foo: 'bar', + testParam: undefined, + }) + }) + it('can be used as global upstream middleware', async () => { const router = Router() const handler = vi.fn(({ id, method }) => ({ id, method })) - const request = { method: 'GET', url: 'https://foo.bar/baz' } - await router - .all('*', withParams) - .get('/:id', handler) - .handle(request) + await router.all('*', withParams).get('/:id', handler).handle(request) expect(handler).toHaveReturnedWith({ id: 'baz', method: 'GET' }) }) + + it('binds a function property of request to the request object', async () => { + const router = Router() + + const myFunction = function () { + return this.testParam + } + + const handler = vi.fn(({ id, method, myFunction }) => { + return { id, method, testParam: myFunction() } + }) + + const request = { + method: 'GET', + url: 'https://foo.bar/baz', + myFunction, + testParam: 'testValue', + } + + await router.get('/:id', withParams, handler).handle(request) + + expect(handler).toHaveReturnedWith({ + id: 'baz', + method: 'GET', + testParam: 'testValue', + }) + }) }) diff --git a/src/withParams.ts b/src/withParams.ts index b3757460..3bfad31f 100644 --- a/src/withParams.ts +++ b/src/withParams.ts @@ -7,6 +7,6 @@ export const withParams = (request: IRequest): void => { if ((p = obj[prop]) !== undefined) return p.bind?.(request) || p return obj?.params?.[prop] - } + }, }) } diff --git a/test-utils/index.ts b/test/index.ts similarity index 60% rename from test-utils/index.ts rename to test/index.ts index 47caf3b9..5dc43e79 100644 --- a/test-utils/index.ts +++ b/test/index.ts @@ -10,16 +10,13 @@ export const buildRequest = ({ export const extract = ({ params, query }) => ({ params, query }) -const testRoute = async ({ - route, - path, - method = 'get', - returns = true, - log = false, -}, Router) => { +const testRoute = async ( + { route, path, method = 'get', returns = true, log = false }, + Router +) => { const routes = [] const router = Router({ routes }) - const handler = vi.fn(req => req.params) + const handler = vi.fn((req) => req.params) // register route router[method](route, handler) @@ -47,15 +44,14 @@ export const runTests = (tests, Router) => { for (let test of tests) { let { route, path, returns = true, description } = test const matchNote = returns - ? typeof returns === 'object' - ? `returns params ${JSON - .stringify(returns) - .replace('{', '{ ') - .replace('}', ' }') - .replace(/"(\w+)":/g, '$1: ') - .replace(',', ', ')} from` - : 'matches' - : 'does NOT match' + ? typeof returns === 'object' + ? `returns params ${JSON.stringify(returns) + .replace('{', '{ ') + .replace('}', ' }') + .replace(/"(\w+)":/g, '$1: ') + .replace(',', ', ')} from` + : 'matches' + : 'does NOT match' description = description || `route "${route}" ${matchNote} path "${path}"` it(description, async () => { @@ -64,4 +60,7 @@ export const runTests = (tests, Router) => { } } -export const createTestRunner = Router => (...args) => runTests(...args, Router) +export const createTestRunner = + (Router) => + (...args) => + runTests(...args, Router) diff --git a/tsconfig.json b/tsconfig.json index bec82f88..30544cfa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,11 +7,7 @@ "sourceMap": true, "esModuleInterop": true, "inlineSourceMap": false, - "lib": [ - "esnext", - "dom", - "dom.iterable" - ], + "lib": ["esnext", "dom", "dom.iterable"], "listEmittedFiles": false, "listFiles": false, "moduleResolution": "node", @@ -25,16 +21,8 @@ "outDir": "", "target": "esnext", "module": "esnext", - "types": [ - "@cloudflare/workers-types", - "@types/node" - ], + "types": ["@cloudflare/workers-types", "@types/node"] }, - "exclude": [ - "node_modules", - "dist", - "**/*.spec.ts", - "**/example.ts", - ], + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/example.ts"], "include": ["src", "example"] } diff --git a/yarn.lock b/yarn.lock index 3a871b2c..70ac70a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1527,6 +1527,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-config-prettier@^8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" + integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== + eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" @@ -3092,6 +3097,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@^2.8.8: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + pretty-bytes@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz"