itty-router 4.0.16 → 4.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -22
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +1 -1
- package/text.d.ts +1 -1
- package/text.js +1 -1
- package/text.mjs +1 -1
package/README.md
CHANGED
|
@@ -49,38 +49,29 @@
|
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
52
|
-
Itty
|
|
52
|
+
Itty is arguably the smallest (~450 bytes) feature-rich JavaScript router available, while enabling dead-simple API code.
|
|
53
|
+
|
|
54
|
+
Designed originally for [Cloudflare Workers](https://itty.dev/itty-router/runtimes#Cloudflare%20Workers), itty can be used in browsers, service workers, edge functions, or runtimes like [Node](https://itty.dev/itty-router/runtimes#Node), [Bun](https://itty.dev/itty-router/runtimes#Bun), etc.!
|
|
53
55
|
|
|
54
56
|
## Features
|
|
55
57
|
|
|
56
|
-
- Tiny.
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
- [
|
|
58
|
+
- Tiny, tree-shakeable. [~450](https://deno.bundlejs.com/?q=itty-router/Router) bytes for the Router itself, or [~1.6k](https://bundlephobia.com/package/itty-router) for the entire library (>100x smaller than [express.js](https://www.npmjs.com/package/express)).
|
|
59
|
+
- [Fully-Typed](https://itty.dev/itty-router/typescript).
|
|
60
|
+
- Shorter, simpler route code than most modern routers.
|
|
61
|
+
- Dead-simple [middleware](https://itty.dev/itty-router/middleware) - use ours or write your own.
|
|
62
|
+
- Supports [nested APIs](https://itty.dev/itty-router/nesting).
|
|
63
|
+
- Platform agnostic (based on [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)) - use it [anywhere, in any environment](https://itty.dev/itty-router/runtimes).
|
|
61
64
|
- Parses [route params](https://itty.dev/itty-router/route-patterns#params),
|
|
62
65
|
[optional params](https://itty.dev/itty-router/route-patterns#optional),
|
|
63
66
|
[wildcards](https://itty.dev/itty-router/route-patterns#wildcards),
|
|
64
67
|
[greedy params](https://itty.dev/itty-router/route-patterns#greedy),
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
- Easy [Response](https://itty.dev/itty-router/responses) creation, with helpers for major formats (e.g.
|
|
69
|
-
[json](https://itty.dev/itty-router/api#json),
|
|
70
|
-
[html](https://itty.dev/itty-router/api#html),
|
|
71
|
-
[png](https://itty.dev/itty-router/api#png),
|
|
72
|
-
[jpeg](https://itty.dev/itty-router/api#jpeg), etc.)
|
|
73
|
-
- Deep APIs via [router nesting](https://itty.dev/itty-router/nesting).
|
|
74
|
-
- Full [middleware](https://itty.dev/itty-router/middleware) support. Includes the following by default:
|
|
75
|
-
- [withParams](https://itty.dev/itty-router/api#withParams) - access the params directly off the `Request` (instead of `request.params`).
|
|
76
|
-
- [withCookies](https://itty.dev/itty-router/api#withCookies) - access cookies in a convenient Object format.
|
|
77
|
-
- [withContent](https://itty.dev/itty-router/api#withContent) - auto-parse Request bodies as `request.content`.
|
|
78
|
-
- [CORS](https://itty.dev/itty-router/cors) - because we love you.
|
|
79
|
-
- Fully readable regex... yeah right! 😆
|
|
68
|
+
[file formats](https://itty.dev/itty-router/route-patterns#file-formats)
|
|
69
|
+
and [query strings](https://itty.dev/itty-router/route-patterns#query).
|
|
70
|
+
- Extremely extendable/flexible. We leave you in complete control.
|
|
80
71
|
|
|
81
72
|
## [Full Documentation](https://itty.dev/itty-router)
|
|
82
73
|
|
|
83
|
-
Complete
|
|
74
|
+
Complete API documentation is available at [itty.dev/itty-router](https://itty.dev/itty-router), or join our [Discord](https://discord.com/channels/832353585802903572) channel to chat with community members for quick help!
|
|
84
75
|
|
|
85
76
|
## Installation
|
|
86
77
|
|
|
@@ -128,6 +119,95 @@ export default {
|
|
|
128
119
|
}
|
|
129
120
|
```
|
|
130
121
|
|
|
122
|
+
# What's different about itty? <a name="a-different-kind-of-router"></a>
|
|
123
|
+
Itty does a few things very differently from other routers. This allows itty route code to be shorter and more intuitive than most!
|
|
124
|
+
|
|
125
|
+
### 1. Simpler handler/middleware flow.
|
|
126
|
+
In itty, you simply return (anything) to exit the flow. If any handler ever returns a thing, that's what the `router.handle` returns. If it doesn't, it's considered middleware, and the next handler is called.
|
|
127
|
+
|
|
128
|
+
That's it!
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// not middleware: any handler that returns (anything at all)
|
|
132
|
+
(request) => [1, 4, 5, 1]
|
|
133
|
+
|
|
134
|
+
// middleware: simply doesn't return
|
|
135
|
+
const withUser = (request) => {
|
|
136
|
+
request.user = 'Halsey'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// a middleware that *might* return
|
|
140
|
+
const onlyHalsey = (request) => {
|
|
141
|
+
if (request.user !== 'Halsey') {
|
|
142
|
+
return error(403, 'Only Halsey is allowed to see this!')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// uses middleware, then returns something
|
|
147
|
+
route.get('/secure', withUser, onlyHalsey,
|
|
148
|
+
({ user }) => `Hey, ${user} - welcome back!`
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 2. You don't have to build a response in each route handler.
|
|
153
|
+
We've been stuck in this pattern for over a decade. Almost every router still expects you to build and return a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)... in every single route.
|
|
154
|
+
|
|
155
|
+
We think you should be able to do that once, at the end. In most modern APIs for instance, we're serving JSON in the majority of our routes. So why handle that more than once?
|
|
156
|
+
```ts
|
|
157
|
+
router
|
|
158
|
+
// we can still do it the manual way
|
|
159
|
+
.get('/traditional', (request) => json([1, 2, 3]))
|
|
160
|
+
|
|
161
|
+
// or defer to later
|
|
162
|
+
.get('/easy-mode', (request) => [1, 2, 3])
|
|
163
|
+
|
|
164
|
+
// later, when handling a request
|
|
165
|
+
router
|
|
166
|
+
.handle(request)
|
|
167
|
+
.then(json) // we can turn any non-Response into valid JSON.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 3. It's all Promises.
|
|
171
|
+
We `await` every handler, looking for a return value. If we get one, we break the flow and return your value. If we don't, we continue processing handlers/routes until we do. This means that every handler can either be synchronous or async - it's all the same.
|
|
172
|
+
|
|
173
|
+
When paired with the fact that we can simply return raw data and transform it later, this is AWESOME for working with async APIs, database layers, etc. We don't need to transform anything at the route, we can simply return the Promise (to data) itself!
|
|
174
|
+
|
|
175
|
+
Check this out:
|
|
176
|
+
```ts
|
|
177
|
+
import { myDatabase } from './somewhere'
|
|
178
|
+
|
|
179
|
+
router
|
|
180
|
+
// assumes getItems() returns a Promise to some data
|
|
181
|
+
.get('/items', () => myDatabase.getItems())
|
|
182
|
+
|
|
183
|
+
// later, when handling a request
|
|
184
|
+
router
|
|
185
|
+
.handle(request)
|
|
186
|
+
.then(json) // we can turn any non-Response into valid JSON.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 4. Only one required argument. The rest is up to you.
|
|
190
|
+
We only require one argument in itty - a Request-like object with the following shape: `{ url, method }` (usually a native [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)). Because itty is not opinionated about [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) creation, there is not "response" argument built in. Every other argument you pass to `route.handle` is given to each handler, in the same order.
|
|
191
|
+
|
|
192
|
+
> ### This makes itty one of the most platform-agnostic routers, *period*, as it's able to match up to any platform's signature.
|
|
193
|
+
|
|
194
|
+
Here's an example using [Cloudflare Worker](https://workers.cloudflare.com/) arguments:
|
|
195
|
+
```ts
|
|
196
|
+
router
|
|
197
|
+
.get('/my-route', (request, environment, context) => {
|
|
198
|
+
// we can access anything here that was passed to `router.handle`.
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// Cloudflare gives us 3 arguments: request, environment, and context.
|
|
202
|
+
// Passing them to `route.handle` gives every route handler (above) access to each.
|
|
203
|
+
export default {
|
|
204
|
+
fetch: (request, env, ctx) => router
|
|
205
|
+
.handle(request, env, ctx)
|
|
206
|
+
.then(json)
|
|
207
|
+
.catch(error)
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
131
211
|
## Join the Discussion!
|
|
132
212
|
|
|
133
213
|
Have a question? Suggestion? Complaint? Want to send a gift basket?
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";class e extends Error{status;constructor(e=500,t){super("object"==typeof t?t.error:t),"object"==typeof t&&Object.assign(this,t),this.status=e}}const t=(e="text/plain; charset=utf-8",t)=>(s,o)=>{const{headers:r={},...n}=o||{};return"Response"===s?.constructor.name?s:new Response(t?t(s):s,{headers:{"content-type":e,...r},...n})},s=t("application/json; charset=utf-8",JSON.stringify),o=e=>({400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",500:"Internal Server Error"}[e]||"Unknown Error"),r=t("text/
|
|
1
|
+
"use strict";class e extends Error{status;constructor(e=500,t){super("object"==typeof t?t.error:t),"object"==typeof t&&Object.assign(this,t),this.status=e}}const t=(e="text/plain; charset=utf-8",t)=>(s,o)=>{const{headers:r={},...n}=o||{};return"Response"===s?.constructor.name?s:new Response(t?t(s):s,{headers:{"content-type":e,...r},...n})},s=t("application/json; charset=utf-8",JSON.stringify),o=e=>({400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",500:"Internal Server Error"}[e]||"Unknown Error"),r=t("text/plain; charset=utf-8",String),n=t("text/html"),a=t("image/jpeg"),c=t("image/png"),p=t("image/webp");exports.Router=({base:e="",routes:t=[]}={})=>({__proto__:new Proxy({},{get:(s,o,r,n)=>(s,...a)=>t.push([o.toUpperCase(),RegExp(`^${(n=(e+s).replace(/\/+(\/|$)/g,"$1")).replace(/(\/?\.?):(\w+)\+/g,"($1(?<$2>*))").replace(/(\/?\.?):(\w+)/g,"($1(?<$2>[^$1/]+?))").replace(/\./g,"\\.").replace(/(\/?)\*/g,"($1.*)?")}/*$`),a,n])&&r}),routes:t,async handle(e,...s){let o,r,n=new URL(e.url),a=e.query={__proto__:null};for(let[e,t]of n.searchParams)a[e]=void 0===a[e]?t:[a[e],t].flat();for(let[a,c,p,i]of t)if((a===e.method||"ALL"===a)&&(r=n.pathname.match(c))){e.params=r.groups||{},e.route=i;for(let t of p)if(void 0!==(o=await t(e.proxy||e,...s)))return o}}}),exports.StatusError=e,exports.createCors=(e={})=>{const{origins:t=["*"],maxAge:s,methods:o=["GET"],headers:r={}}=e;let n;const a={"content-type":"application/json","Access-Control-Allow-Methods":o.join(", "),...r};s&&(a["Access-Control-Max-Age"]=s);return{corsify:e=>{if(!e)throw new Error("No fetch handler responded and no upstream to proxy to specified.");const{headers:t,status:s,body:o}=e;return[101,301,302,308].includes(s)||t.get("access-control-allow-origin")?e:new Response(o,{status:s,headers:{...Object.fromEntries(t),...a,...n,"content-type":t.get("content-type")}})},preflight:e=>{const s=[...new Set(["OPTIONS",...o])],r=e.headers.get("origin")||"";if(n=(t.includes(r)||t.includes("*"))&&{"Access-Control-Allow-Origin":r},"OPTIONS"===e.method){const t={...a,"Access-Control-Allow-Methods":s.join(", "),"Access-Control-Allow-Headers":e.headers.get("Access-Control-Request-Headers"),...n};return new Response(null,{headers:e.headers.get("Origin")&&e.headers.get("Access-Control-Request-Method")&&e.headers.get("Access-Control-Request-Headers")?t:{Allow:s.join(", ")}})}}}},exports.createResponse=t,exports.error=(e=500,t)=>{if(e instanceof Error){const{message:s,...r}=e;e=e.status||500,t={error:s||o(e),...r}}return t={status:e,..."object"==typeof t?t:{error:t||o(e)}},s(t,{status:e})},exports.html=n,exports.jpeg=a,exports.json=s,exports.png=c,exports.status=(e,t)=>new Response(null,{...t,status:e}),exports.text=r,exports.webp=p,exports.withContent=async e=>{e.headers.get("content-type")?.includes("json")&&(e.content=await e.json())},exports.withCookies=e=>{e.cookies=(e.headers.get("Cookie")||"").split(/;\s*/).map((e=>e.split(/=(.+)/))).reduce(((e,[t,s])=>s?(e[t]=s,e):e),{})},exports.withParams=e=>{e.proxy=new Proxy(e.proxy||e,{get:(t,s)=>{let o;return void 0!==(o=t[s])?o.bind?.(e)||o:t?.params?.[s]}})};
|
package/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=({base:e="",routes:t=[]}={})=>({__proto__:new Proxy({},{get:(o,s,r,n)=>(o,...a)=>t.push([s.toUpperCase(),RegExp(`^${(n=(e+o).replace(/\/+(\/|$)/g,"$1")).replace(/(\/?\.?):(\w+)\+/g,"($1(?<$2>*))").replace(/(\/?\.?):(\w+)/g,"($1(?<$2>[^$1/]+?))").replace(/\./g,"\\.").replace(/(\/?)\*/g,"($1.*)?")}/*$`),a,n])&&r}),routes:t,async handle(e,...o){let s,r,n=new URL(e.url),a=e.query={__proto__:null};for(let[e,t]of n.searchParams)a[e]=void 0===a[e]?t:[a[e],t].flat();for(let[a,c,l,i]of t)if((a===e.method||"ALL"===a)&&(r=n.pathname.match(c))){e.params=r.groups||{},e.route=i;for(let t of l)if(void 0!==(s=await t(e.proxy||e,...o)))return s}}});class t extends Error{status;constructor(e=500,t){super("object"==typeof t?t.error:t),"object"==typeof t&&Object.assign(this,t),this.status=e}}const o=(e="text/plain; charset=utf-8",t)=>(o,s)=>{const{headers:r={},...n}=s||{};return"Response"===o?.constructor.name?o:new Response(t?t(o):o,{headers:{"content-type":e,...r},...n})},s=o("application/json; charset=utf-8",JSON.stringify),r=e=>({400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",500:"Internal Server Error"}[e]||"Unknown Error"),n=(e=500,t)=>{if(e instanceof Error){const{message:o,...s}=e;e=e.status||500,t={error:o||r(e),...s}}return t={status:e,..."object"==typeof t?t:{error:t||r(e)}},s(t,{status:e})},a=(e,t)=>new Response(null,{...t,status:e}),c=(
|
|
1
|
+
const e=({base:e="",routes:t=[]}={})=>({__proto__:new Proxy({},{get:(o,s,r,n)=>(o,...a)=>t.push([s.toUpperCase(),RegExp(`^${(n=(e+o).replace(/\/+(\/|$)/g,"$1")).replace(/(\/?\.?):(\w+)\+/g,"($1(?<$2>*))").replace(/(\/?\.?):(\w+)/g,"($1(?<$2>[^$1/]+?))").replace(/\./g,"\\.").replace(/(\/?)\*/g,"($1.*)?")}/*$`),a,n])&&r}),routes:t,async handle(e,...o){let s,r,n=new URL(e.url),a=e.query={__proto__:null};for(let[e,t]of n.searchParams)a[e]=void 0===a[e]?t:[a[e],t].flat();for(let[a,c,l,i]of t)if((a===e.method||"ALL"===a)&&(r=n.pathname.match(c))){e.params=r.groups||{},e.route=i;for(let t of l)if(void 0!==(s=await t(e.proxy||e,...o)))return s}}});class t extends Error{status;constructor(e=500,t){super("object"==typeof t?t.error:t),"object"==typeof t&&Object.assign(this,t),this.status=e}}const o=(e="text/plain; charset=utf-8",t)=>(o,s)=>{const{headers:r={},...n}=s||{};return"Response"===o?.constructor.name?o:new Response(t?t(o):o,{headers:{"content-type":e,...r},...n})},s=o("application/json; charset=utf-8",JSON.stringify),r=e=>({400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",500:"Internal Server Error"}[e]||"Unknown Error"),n=(e=500,t)=>{if(e instanceof Error){const{message:o,...s}=e;e=e.status||500,t={error:o||r(e),...s}}return t={status:e,..."object"==typeof t?t:{error:t||r(e)}},s(t,{status:e})},a=(e,t)=>new Response(null,{...t,status:e}),c=o("text/plain; charset=utf-8",String),l=o("text/html"),i=o("image/jpeg"),p=o("image/png"),d=o("image/webp"),u=async e=>{e.headers.get("content-type")?.includes("json")&&(e.content=await e.json())},g=e=>{e.cookies=(e.headers.get("Cookie")||"").split(/;\s*/).map((e=>e.split(/=(.+)/))).reduce(((e,[t,o])=>o?(e[t]=o,e):e),{})},h=e=>{e.proxy=new Proxy(e.proxy||e,{get:(t,o)=>{let s;return void 0!==(s=t[o])?s.bind?.(e)||s:t?.params?.[o]}})},f=(e={})=>{const{origins:t=["*"],maxAge:o,methods:s=["GET"],headers:r={}}=e;let n;const a={"content-type":"application/json","Access-Control-Allow-Methods":s.join(", "),...r};o&&(a["Access-Control-Max-Age"]=o);return{corsify:e=>{if(!e)throw new Error("No fetch handler responded and no upstream to proxy to specified.");const{headers:t,status:o,body:s}=e;return[101,301,302,308].includes(o)||t.get("access-control-allow-origin")?e:new Response(s,{status:o,headers:{...Object.fromEntries(t),...a,...n,"content-type":t.get("content-type")}})},preflight:e=>{const o=[...new Set(["OPTIONS",...s])],r=e.headers.get("origin")||"";if(n=(t.includes(r)||t.includes("*"))&&{"Access-Control-Allow-Origin":r},"OPTIONS"===e.method){const t={...a,"Access-Control-Allow-Methods":o.join(", "),"Access-Control-Allow-Headers":e.headers.get("Access-Control-Request-Headers"),...n};return new Response(null,{headers:e.headers.get("Origin")&&e.headers.get("Access-Control-Request-Method")&&e.headers.get("Access-Control-Request-Headers")?t:{Allow:o.join(", ")}})}}}};export{e as Router,t as StatusError,f as createCors,o as createResponse,n as error,l as html,i as jpeg,s as json,p as png,a as status,c as text,d as webp,u as withContent,g as withCookies,h as withParams};
|
package/package.json
CHANGED
package/text.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const text: (
|
|
1
|
+
export declare const text: import("./createResponse").ResponseFormatter;
|
package/text.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";const t=((t="text/plain; charset=utf-8",e)=>(s,n)=>{const{headers:r={},...c}=n||{};return"Response"===s?.constructor.name?s:new Response(e?e(s):s,{headers:{"content-type":t,...r},...c})})("text/plain; charset=utf-8",String);exports.text=t;
|
package/text.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=(e,n)=>new Response(
|
|
1
|
+
const e=((e="text/plain; charset=utf-8",t)=>(n,s)=>{const{headers:r={},...o}=s||{};return"Response"===n?.constructor.name?n:new Response(t?t(n):n,{headers:{"content-type":e,...r},...o})})("text/plain; charset=utf-8",String);export{e as text};
|