diff --git a/README.md b/README.md index 85f01044..ec4763a6 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@

Overview

-Routes and middleware are added to a `Router` instance with `.use`, `.addRoute` or `.get/post/put/delete`. +Routes and middleware are added to a `Router` instance with `.use`, `.addRoute` or `.get/post/put/delete`. The router is then used with your web server of choice, e.g. `Deno.serve` or `Bun.serve`. @@ -66,76 +66,116 @@ router.use(Peko.logger(console.log)); router.get("/shorthand-route", () => new Response("Hello world!")); -router.post("/shorthand-route-ext", async (ctx, next) => { await next(); console.log(ctx.request.headers); }, (req) => new Response(req.body)); +router.post( + "/shorthand-route-ext", + async (ctx, next) => { + await next(); + console.log(ctx.request.headers); + }, + (req) => new Response(req.body) +); router.addRoute({ - path: "/object-route", - middleware: async (ctx, next) => { await next(); console.log(ctx.request.headers); }, // can also be array of middleware - handler: () => new Response("Hello world!") -}) + path: "/object-route", + middleware: async (ctx, next) => { + await next(); + console.log(ctx.request.headers); + }, // can also be array of middleware + handler: () => new Response("Hello world!"), +}); -router.addRoutes([ /* array of route objects */ ]) +router.addRoutes([ + /* array of route objects */ +]); -Deno.serve((req) => router.handle(req)) +Deno.serve((req) => router.handle(req)); ```

Types

### [**Router**](https://deno.land/x/peko/mod.ts?s=Router) + The main class/entrypoint of Peko. -The `handle` method generates a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response) from a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) argument via configured routes and middleware. +The `handle` method generates a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response) from a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) argument via configured routes and middleware. ### [**Route**](https://deno.land/x/peko/mod.ts?s=Route) -Routes are added to a `Router` and matched to a `Request` via their `path` property. Once matched, the route's `middleware` and `handlers` are invoked to process the `Request` (after global middleware on the `Router`). + +Routes are added to a `Router` and matched to a `Request` via their `path` property. Once matched, the route's `middleware` and `handlers` are invoked to process the `Request` (after global middleware on the `Router`). Dynamic path parameters are supported in the `/users/:userid` syntax. ### [**RequestContext**](https://deno.land/x/peko/mod.ts?s=RequestContext) + An object containing request data that is passed into middleware and handlers in the `Request` process lifecycle. The `state` property is an object designed to transfer information between middleware/handlers. ### [**Middleware**](https://deno.land/x/peko/mod.ts?s=Middleware) + Functions that receive `RequestContext` and `next`. They are designed to: + - Return a `Response` and end the `Request` processing lifecycle (e.g. returning a `401`) - Call `await next()` to access the final response (e.g. logging) - Edit the context's `state` (e.g. rendering geolocation to HTML) ### [**Handler**](https://deno.land/x/peko/mod.ts?s=Handler) + The final request handling function on a `Route`, receives `RequestContext` argument. Must return/resolve to a `Response` (e.g. Render HTML or return JSON payload).

Recipes

-### Library utilities +### Examples + Check the [examples](https://github.com/sejori/peko/tree/main/example) to see implementations of: + - server-side rendering Preact to HTML - streaming server-sent events to web client - JWT authentication middleware - logging requests - caching responses +### Deno + +- Process 1: `deno task dev:build` +- Process 2: `deno task dev:deno` + +### Wrangler (Node with Cloudflare Worker target): + +- `npm i` +- Process 1: `npm run dev:build` +- Process 2: `npm run dev:wrangler` + +### Bun: + +- `bun install` +- Process 1: `bun dev:build` +- Process 2: `bun dev:bun` + ### Error handling If no matching route is found for a request an empty 404 response is sent. If an error occurs in handling a request an empty 500 response is sent. Both of these behaviours can be overwritten with the following middleware: ```js router.use(async (_, next) => { - const response = await next(); - if (!response) return new Response("Would you look at that? Nothing's here!", { status: 404 }); + const response = await next(); + if (!response) + return new Response("Would you look at that? Nothing's here!", { + status: 404, + }); }); ``` ```js router.use(async (_, next) => { - try { - await next(); - } catch(e) { - console.log(e); - return new Response("Oh no! An error occured :(", { status: 500 }); - } + try { + await next(); + } catch (e) { + console.log(e); + return new Response("Oh no! An error occured :(", { status: 500 }); + } }); ``` @@ -144,29 +184,33 @@ router.use(async (_, next) => { In stateless computing, memory should only be used for source code and disposable cache data. Response caching ensures that we only store data that can be regenerated or refetched. The configurable `cacher` middleware provides drop in handler memoization and response caching for your routes. ```js -router.addRoute("/get-time", Peko.cacher({ itemLifetime: 5000 }), () => new Response(Date.now())); +router.addRoute( + "/get-time", + Peko.cacher({ itemLifetime: 5000 }), + () => new Response(Date.now()) +); ``` The cacher stores response items in memory by default, but it can be extended to use any key value storage by supplying the `store` options parameter (e.g. Cloudflare Workers KV). ```js -import { Router, CacheItem, cacher } from "https://deno.land/x/peko/mod.ts" +import { Router, CacheItem, cacher } from "https://deno.land/x/peko/mod.ts"; const router = new Router(); -const itemMap: Map = new Map() +const itemMap: Map = new Map(); router.addRoute("/get-time", { - middleware: cacher({ - itemLifetime: 5000, - store: { - get: (key) => itemMap.get(key), - set: (key, value) => itemMap.set(key, value), - delete: (key) => itemMap.delete(key) - } - }), - handler: () => new Response(Date.now()) -}) + middleware: cacher({ + itemLifetime: 5000, + store: { + get: (key) => itemMap.get(key), + set: (key, value) => itemMap.set(key, value), + delete: (key) => itemMap.delete(key), + }, + }), + handler: () => new Response(Date.now()), +}); ```

Deployment

@@ -178,12 +222,14 @@ router.addRoute("/get-time", { PR to add your project 🙌 -### [shineponics.org](https://shineponics.org) +### [shineponics.deno.dev](https://shineponics.deno.dev) + - **Stack:** React, Google Cloud Platform - **Features:** Google Sheet analytics, GCP email list, Markdown rendering - [source](https://github.com/shine-systems/shineponics/blob/main/server.ts) -### [thesebsite.com](https://thesebsite.com) +### [thesebsite.deno.dev](https://thesebsite.deno.dev) + - **Stack:** HTML5 - **Features:** UI TS scripts transpiled to JS and cached for browser - [source](https://github.com/sebringrose/peko/blob/main/examples/auth/app.ts) @@ -211,4 +257,5 @@ Because stateless apps can "cold-start" it is important to keep their codebases Note: In reality a single app instance will serve multiple requests, we just can't guarantee it. This is why caching is still an effective optimization strategy but in-memory user sessions are not an effective authentication strategy. ## Credits: + Chick logo from [Twemoji](https://github.com/twitter/twemoji) diff --git a/deno.json b/deno.json index 83335551..2226a64e 100644 --- a/deno.json +++ b/deno.json @@ -4,7 +4,6 @@ "version": "2.2.0", "exports": "./mod.ts", "imports": { - "esbuild": "https://deno.land/x/esbuild@v0.23.0/mod.js", "htm/preact": "https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-render-to-string", "preact": "https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-render-to-string", "preact/hooks": "https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-render-to-string", diff --git a/deno.lock b/deno.lock index 639c8fb0..1e822147 100644 --- a/deno.lock +++ b/deno.lock @@ -2,6 +2,7 @@ "version": "3", "packages": { "specifiers": { + "npm:esbuild@0.23.0": "npm:esbuild@0.23.0", "npm:htm@^3.1.1": "npm:htm@3.1.1", "npm:ts-node@^10.9.2": "npm:ts-node@10.9.2_@types+node@18.16.19_typescript@5.4.2" }, @@ -12,6 +13,102 @@ "@jridgewell/trace-mapping": "@jridgewell/trace-mapping@0.3.9" } }, + "@esbuild/aix-ppc64@0.23.0": { + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "dependencies": {} + }, + "@esbuild/android-arm64@0.23.0": { + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "dependencies": {} + }, + "@esbuild/android-arm@0.23.0": { + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "dependencies": {} + }, + "@esbuild/android-x64@0.23.0": { + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "dependencies": {} + }, + "@esbuild/darwin-arm64@0.23.0": { + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "dependencies": {} + }, + "@esbuild/darwin-x64@0.23.0": { + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "dependencies": {} + }, + "@esbuild/freebsd-arm64@0.23.0": { + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "dependencies": {} + }, + "@esbuild/freebsd-x64@0.23.0": { + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "dependencies": {} + }, + "@esbuild/linux-arm64@0.23.0": { + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "dependencies": {} + }, + "@esbuild/linux-arm@0.23.0": { + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "dependencies": {} + }, + "@esbuild/linux-ia32@0.23.0": { + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "dependencies": {} + }, + "@esbuild/linux-loong64@0.23.0": { + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "dependencies": {} + }, + "@esbuild/linux-mips64el@0.23.0": { + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "dependencies": {} + }, + "@esbuild/linux-ppc64@0.23.0": { + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "dependencies": {} + }, + "@esbuild/linux-riscv64@0.23.0": { + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "dependencies": {} + }, + "@esbuild/linux-s390x@0.23.0": { + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "dependencies": {} + }, + "@esbuild/linux-x64@0.23.0": { + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "dependencies": {} + }, + "@esbuild/netbsd-x64@0.23.0": { + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "dependencies": {} + }, + "@esbuild/openbsd-arm64@0.23.0": { + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "dependencies": {} + }, + "@esbuild/openbsd-x64@0.23.0": { + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "dependencies": {} + }, + "@esbuild/sunos-x64@0.23.0": { + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "dependencies": {} + }, + "@esbuild/win32-arm64@0.23.0": { + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "dependencies": {} + }, + "@esbuild/win32-ia32@0.23.0": { + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "dependencies": {} + }, + "@esbuild/win32-x64@0.23.0": { + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "dependencies": {} + }, "@jridgewell/resolve-uri@3.1.2": { "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dependencies": {} @@ -67,6 +164,35 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dependencies": {} }, + "esbuild@0.23.0": { + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dependencies": { + "@esbuild/aix-ppc64": "@esbuild/aix-ppc64@0.23.0", + "@esbuild/android-arm": "@esbuild/android-arm@0.23.0", + "@esbuild/android-arm64": "@esbuild/android-arm64@0.23.0", + "@esbuild/android-x64": "@esbuild/android-x64@0.23.0", + "@esbuild/darwin-arm64": "@esbuild/darwin-arm64@0.23.0", + "@esbuild/darwin-x64": "@esbuild/darwin-x64@0.23.0", + "@esbuild/freebsd-arm64": "@esbuild/freebsd-arm64@0.23.0", + "@esbuild/freebsd-x64": "@esbuild/freebsd-x64@0.23.0", + "@esbuild/linux-arm": "@esbuild/linux-arm@0.23.0", + "@esbuild/linux-arm64": "@esbuild/linux-arm64@0.23.0", + "@esbuild/linux-ia32": "@esbuild/linux-ia32@0.23.0", + "@esbuild/linux-loong64": "@esbuild/linux-loong64@0.23.0", + "@esbuild/linux-mips64el": "@esbuild/linux-mips64el@0.23.0", + "@esbuild/linux-ppc64": "@esbuild/linux-ppc64@0.23.0", + "@esbuild/linux-riscv64": "@esbuild/linux-riscv64@0.23.0", + "@esbuild/linux-s390x": "@esbuild/linux-s390x@0.23.0", + "@esbuild/linux-x64": "@esbuild/linux-x64@0.23.0", + "@esbuild/netbsd-x64": "@esbuild/netbsd-x64@0.23.0", + "@esbuild/openbsd-arm64": "@esbuild/openbsd-arm64@0.23.0", + "@esbuild/openbsd-x64": "@esbuild/openbsd-x64@0.23.0", + "@esbuild/sunos-x64": "@esbuild/sunos-x64@0.23.0", + "@esbuild/win32-arm64": "@esbuild/win32-arm64@0.23.0", + "@esbuild/win32-ia32": "@esbuild/win32-ia32@0.23.0", + "@esbuild/win32-x64": "@esbuild/win32-x64@0.23.0" + } + }, "htm@3.1.1": { "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", "dependencies": {} diff --git a/example/preactSSR/dist/pages/About.js b/example/preactSSR/dist/pages/About.js index 0e0cce52..7f64e3e2 100644 --- a/example/preactSSR/dist/pages/About.js +++ b/example/preactSSR/dist/pages/About.js @@ -1,6 +1,15 @@ -var Q,m,ct,Ft,E,st,ft,B,X,z,G,Rt,J={},pt=[],Ot=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Y=Array.isArray;function C(e,t){for(var n in t)e[n]=t[n];return e}function ht(e){var t=e.parentNode;t&&t.removeChild(e)}function Z(e,t,n){var _,r,o,s={};for(o in t)o=="key"?_=t[o]:o=="ref"?r=t[o]:s[o]=t[o];if(arguments.length>2&&(s.children=arguments.length>3?Q.call(arguments,2):n),typeof e=="function"&&e.defaultProps!=null)for(o in e.defaultProps)s[o]===void 0&&(s[o]=e.defaultProps[o]);return A(e,s,_,r,null)}function A(e,t,n,_,r){var o={type:e,props:t,key:n,ref:_,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,constructor:void 0,__v:r??++ct,__i:-1,__u:0};return r==null&&m.vnode!=null&&m.vnode(o),o}function tt(e){return e.children}function N(e,t){this.props=e,this.context=t}function $(e,t){if(t==null)return e.__?$(e.__,e.__i+1):null;for(var n;tt&&E.sort(B));M.__r=0}function mt(e,t,n,_,r,o,s,u,a,l,f){var i,p,c,y,k,b=_&&_.__k||pt,h=t.length;for(n.__d=a,Wt(n,t,b),a=n.__d,i=0;i0?A(r.type,r.props,r.key,r.ref?r.ref:null,r.__v):r)!=null?(r.__=e,r.__b=e.__b+1,u=jt(r,n,s,f),r.__i=u,o=null,u!==-1&&(f--,(o=n[u])&&(o.__u|=131072)),o==null||o.__v===null?(u==-1&&i--,typeof r.type!="function"&&(r.__u|=65536)):u!==s&&(u==s-1?i=u-s:u==s+1?i++:u>s?f>a-s?i+=u-s:i--:u(a!=null&&!(131072&a.__u)?1:0))for(;s>=0||u=0){if((a=t[s])&&!(131072&a.__u)&&r==a.key&&o===a.type)return s;s--}if(u=5&&((s||!p&&o===5)&&(a.push(o,0,s,r),o=6),p&&(a.push(o,p,0,r),o=6)),s=""},f=0;f"?(o=1,s=""):s=_+s[0]:u?_===u?u="":s+=_:_==='"'||_==="'"?u=_:_===">"?(l(),o=1):o&&(_==="="?(o=5,r=s,s=""):_==="/"&&(o<5||n[f][i+1]===">")?(l(),o===3&&(a=a[0]),o=a,(a=a[0]).push(2,0,o),o=0):_===" "||_===" "||_===` -`||_==="\r"?(l(),o=2):s+=_),o===3&&s==="!--"&&(o=4,a=a[0])}return l(),a}(e)),t),arguments,[])).length>1?t:t[0]}var x=kt.bind(Z);var zt=({navColor:e,navLink:t,children:n})=>x` -