├── .gitignore ├── Readme.md ├── package.json ├── pnpm-lock.yaml ├── src └── app.tsx ├── static ├── app.css ├── favicon.ico └── og │ └── card.png └── util ├── build.mjs ├── handler.tsx ├── start-shim.ts └── start.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | .out 2 | node_modules 3 | .vercel 4 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Preact on the Edge 2 | 3 | [`react-on-the-edge`](https://react-on-the-edge.vercel.app/) but on top of Preact. It uses `esbuild` for bundling and [Vercel Edge Functions](https://vercel.com/edge) for SSR. 4 | 5 | This example is for framework builders and advanced usage of the low-level Vercel [Build Output API](https://vercel.com/docs/build-output-api/v3). If you're looking to develop a Preact application with dynamic Edge capabilities, we recommend [Next.js Middleware](https://nextjs.org/docs/advanced-features/middleware) and [Vercel Edge Functions](https://vercel.com/edge), with [`preact/compat`](https://preactjs.com/guide/v10/switching-to-preact). 6 | 7 | ## How to use 8 | 9 | Run `pnpm i` then: 10 | 11 | - To build: `pnpm build` 12 | - To run a local server: `pnpm start` 13 | 14 | After building, `.vercel/output` will be created which you can deploy via `vc --prebuilt`. 15 | 16 | ## Architecture 17 | 18 | - `util/build.mjs` implements the build process on top of `esbuild` that bundles `src/app` into an Edge Function. 19 | - `util/start.mjs` implements a local server using the `edge-runtime` package that can locally run the build outputs. 20 | 21 | ## Developing 22 | 23 | Due to the absence of a dev server, [`watchexec`](https://github.com/watchexec/watchexec) can be used as a replacement. Use `brew install watchexec` to install. 24 | 25 | ```bash 26 | watchexec -c -r --no-meta 'node util/build.mjs; node util/start.mjs' 27 | ``` 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "node util/build.mjs", 4 | "start": "node util/start.mjs" 5 | }, 6 | "devDependencies": { 7 | "bytes": "^3.1.2", 8 | "edge-runtime": "1.1.0-beta.7", 9 | "esbuild": "^0.14.54", 10 | "ms": "^2.1.3", 11 | "picocolors": "^1.0.0", 12 | "preact": "^10.10.1", 13 | "preact-render-to-string": "^5.2.1", 14 | "sirv": "^2.0.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | bytes: ^3.1.2 5 | edge-runtime: 1.1.0-beta.7 6 | esbuild: ^0.14.54 7 | ms: ^2.1.3 8 | picocolors: ^1.0.0 9 | preact: ^10.10.1 10 | preact-render-to-string: ^5.2.1 11 | sirv: ^2.0.2 12 | 13 | devDependencies: 14 | bytes: 3.1.2 15 | edge-runtime: 1.1.0-beta.7 16 | esbuild: 0.14.54 17 | ms: 2.1.3 18 | picocolors: 1.0.0 19 | preact: 10.10.1 20 | preact-render-to-string: 5.2.1_preact@10.10.1 21 | sirv: 2.0.2 22 | 23 | packages: 24 | 25 | /@edge-runtime/format/1.0.0: 26 | resolution: {integrity: sha512-mxi0n00nJwnjaXUQIfTS7l64yvwGXs435gEjuDhzTHZyrmnCYdELXdJr3Q+6v4DO1mmmtNTYFHUvIlpCmMUC6g==} 27 | dev: true 28 | 29 | /@edge-runtime/primitives/1.1.0-beta.7: 30 | resolution: {integrity: sha512-ZwuSMpmrf2mAj/O7EWxKOXrC03YMkU64N+CgvVFOtJGfhydk4Db/392Zama3BjNYAMOr/oY9L7HxfPutAFesKw==} 31 | dev: true 32 | 33 | /@edge-runtime/vm/1.1.0-beta.7: 34 | resolution: {integrity: sha512-biH/Uxgql+PshksqThvCojd0luA9mnua3s8fvEeCwanPsNa0arajG7uwugNQ/7SOFTT0F/LY81wVZ89QFC2H4Q==} 35 | dependencies: 36 | '@edge-runtime/primitives': 1.1.0-beta.7 37 | dev: true 38 | 39 | /@esbuild/linux-loong64/0.14.54: 40 | resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} 41 | engines: {node: '>=12'} 42 | cpu: [loong64] 43 | os: [linux] 44 | requiresBuild: true 45 | dev: true 46 | optional: true 47 | 48 | /@polka/url/1.0.0-next.21: 49 | resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} 50 | dev: true 51 | 52 | /bytes/3.1.2: 53 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 54 | engines: {node: '>= 0.8'} 55 | dev: true 56 | 57 | /convert-hrtime/3.0.0: 58 | resolution: {integrity: sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==} 59 | engines: {node: '>=8'} 60 | dev: true 61 | 62 | /edge-runtime/1.1.0-beta.7: 63 | resolution: {integrity: sha512-DrLuKeADNuQ5JN9+wl74tDwNOrTusEKL81lz3NRup1qV/sUml+517TUCw3O+h6RxDaTiEpMbAjds9fzbeas8eQ==} 64 | hasBin: true 65 | dependencies: 66 | '@edge-runtime/format': 1.0.0 67 | '@edge-runtime/vm': 1.1.0-beta.7 68 | exit-hook: 2.2.1 69 | http-status: 1.5.2 70 | mri: 1.2.0 71 | picocolors: 1.0.0 72 | pretty-bytes: 5.6.0 73 | pretty-ms: 7.0.1 74 | time-span: 4.0.0 75 | dev: true 76 | 77 | /esbuild-android-64/0.14.54: 78 | resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} 79 | engines: {node: '>=12'} 80 | cpu: [x64] 81 | os: [android] 82 | requiresBuild: true 83 | dev: true 84 | optional: true 85 | 86 | /esbuild-android-arm64/0.14.54: 87 | resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} 88 | engines: {node: '>=12'} 89 | cpu: [arm64] 90 | os: [android] 91 | requiresBuild: true 92 | dev: true 93 | optional: true 94 | 95 | /esbuild-darwin-64/0.14.54: 96 | resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} 97 | engines: {node: '>=12'} 98 | cpu: [x64] 99 | os: [darwin] 100 | requiresBuild: true 101 | dev: true 102 | optional: true 103 | 104 | /esbuild-darwin-arm64/0.14.54: 105 | resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} 106 | engines: {node: '>=12'} 107 | cpu: [arm64] 108 | os: [darwin] 109 | requiresBuild: true 110 | dev: true 111 | optional: true 112 | 113 | /esbuild-freebsd-64/0.14.54: 114 | resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} 115 | engines: {node: '>=12'} 116 | cpu: [x64] 117 | os: [freebsd] 118 | requiresBuild: true 119 | dev: true 120 | optional: true 121 | 122 | /esbuild-freebsd-arm64/0.14.54: 123 | resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} 124 | engines: {node: '>=12'} 125 | cpu: [arm64] 126 | os: [freebsd] 127 | requiresBuild: true 128 | dev: true 129 | optional: true 130 | 131 | /esbuild-linux-32/0.14.54: 132 | resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} 133 | engines: {node: '>=12'} 134 | cpu: [ia32] 135 | os: [linux] 136 | requiresBuild: true 137 | dev: true 138 | optional: true 139 | 140 | /esbuild-linux-64/0.14.54: 141 | resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} 142 | engines: {node: '>=12'} 143 | cpu: [x64] 144 | os: [linux] 145 | requiresBuild: true 146 | dev: true 147 | optional: true 148 | 149 | /esbuild-linux-arm/0.14.54: 150 | resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} 151 | engines: {node: '>=12'} 152 | cpu: [arm] 153 | os: [linux] 154 | requiresBuild: true 155 | dev: true 156 | optional: true 157 | 158 | /esbuild-linux-arm64/0.14.54: 159 | resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} 160 | engines: {node: '>=12'} 161 | cpu: [arm64] 162 | os: [linux] 163 | requiresBuild: true 164 | dev: true 165 | optional: true 166 | 167 | /esbuild-linux-mips64le/0.14.54: 168 | resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} 169 | engines: {node: '>=12'} 170 | cpu: [mips64el] 171 | os: [linux] 172 | requiresBuild: true 173 | dev: true 174 | optional: true 175 | 176 | /esbuild-linux-ppc64le/0.14.54: 177 | resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} 178 | engines: {node: '>=12'} 179 | cpu: [ppc64] 180 | os: [linux] 181 | requiresBuild: true 182 | dev: true 183 | optional: true 184 | 185 | /esbuild-linux-riscv64/0.14.54: 186 | resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} 187 | engines: {node: '>=12'} 188 | cpu: [riscv64] 189 | os: [linux] 190 | requiresBuild: true 191 | dev: true 192 | optional: true 193 | 194 | /esbuild-linux-s390x/0.14.54: 195 | resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} 196 | engines: {node: '>=12'} 197 | cpu: [s390x] 198 | os: [linux] 199 | requiresBuild: true 200 | dev: true 201 | optional: true 202 | 203 | /esbuild-netbsd-64/0.14.54: 204 | resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} 205 | engines: {node: '>=12'} 206 | cpu: [x64] 207 | os: [netbsd] 208 | requiresBuild: true 209 | dev: true 210 | optional: true 211 | 212 | /esbuild-openbsd-64/0.14.54: 213 | resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} 214 | engines: {node: '>=12'} 215 | cpu: [x64] 216 | os: [openbsd] 217 | requiresBuild: true 218 | dev: true 219 | optional: true 220 | 221 | /esbuild-sunos-64/0.14.54: 222 | resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} 223 | engines: {node: '>=12'} 224 | cpu: [x64] 225 | os: [sunos] 226 | requiresBuild: true 227 | dev: true 228 | optional: true 229 | 230 | /esbuild-windows-32/0.14.54: 231 | resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} 232 | engines: {node: '>=12'} 233 | cpu: [ia32] 234 | os: [win32] 235 | requiresBuild: true 236 | dev: true 237 | optional: true 238 | 239 | /esbuild-windows-64/0.14.54: 240 | resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} 241 | engines: {node: '>=12'} 242 | cpu: [x64] 243 | os: [win32] 244 | requiresBuild: true 245 | dev: true 246 | optional: true 247 | 248 | /esbuild-windows-arm64/0.14.54: 249 | resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} 250 | engines: {node: '>=12'} 251 | cpu: [arm64] 252 | os: [win32] 253 | requiresBuild: true 254 | dev: true 255 | optional: true 256 | 257 | /esbuild/0.14.54: 258 | resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} 259 | engines: {node: '>=12'} 260 | hasBin: true 261 | requiresBuild: true 262 | optionalDependencies: 263 | '@esbuild/linux-loong64': 0.14.54 264 | esbuild-android-64: 0.14.54 265 | esbuild-android-arm64: 0.14.54 266 | esbuild-darwin-64: 0.14.54 267 | esbuild-darwin-arm64: 0.14.54 268 | esbuild-freebsd-64: 0.14.54 269 | esbuild-freebsd-arm64: 0.14.54 270 | esbuild-linux-32: 0.14.54 271 | esbuild-linux-64: 0.14.54 272 | esbuild-linux-arm: 0.14.54 273 | esbuild-linux-arm64: 0.14.54 274 | esbuild-linux-mips64le: 0.14.54 275 | esbuild-linux-ppc64le: 0.14.54 276 | esbuild-linux-riscv64: 0.14.54 277 | esbuild-linux-s390x: 0.14.54 278 | esbuild-netbsd-64: 0.14.54 279 | esbuild-openbsd-64: 0.14.54 280 | esbuild-sunos-64: 0.14.54 281 | esbuild-windows-32: 0.14.54 282 | esbuild-windows-64: 0.14.54 283 | esbuild-windows-arm64: 0.14.54 284 | dev: true 285 | 286 | /exit-hook/2.2.1: 287 | resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} 288 | engines: {node: '>=6'} 289 | dev: true 290 | 291 | /http-status/1.5.2: 292 | resolution: {integrity: sha512-HzxX+/hV/8US1Gq4V6R6PgUmJ5Pt/DGATs4QhdEOpG8LrdS9/3UG2nnOvkqUpRks04yjVtV5p/NODjO+wvf6vg==} 293 | engines: {node: '>= 0.4.0'} 294 | dev: true 295 | 296 | /mri/1.2.0: 297 | resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 298 | engines: {node: '>=4'} 299 | dev: true 300 | 301 | /mrmime/1.0.1: 302 | resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} 303 | engines: {node: '>=10'} 304 | dev: true 305 | 306 | /ms/2.1.3: 307 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 308 | dev: true 309 | 310 | /parse-ms/2.1.0: 311 | resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} 312 | engines: {node: '>=6'} 313 | dev: true 314 | 315 | /picocolors/1.0.0: 316 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 317 | dev: true 318 | 319 | /preact-render-to-string/5.2.1_preact@10.10.1: 320 | resolution: {integrity: sha512-Wp3ner1aIVBpKg02C4AoLdBiw4kNaiFSYHr4wUF+fR7FWKAQzNri+iPfPp31sEhAtBfWoJrSxiEFzd5wp5zCgQ==} 321 | peerDependencies: 322 | preact: '>=10' 323 | dependencies: 324 | preact: 10.10.1 325 | pretty-format: 3.8.0 326 | dev: true 327 | 328 | /preact/10.10.1: 329 | resolution: {integrity: sha512-cXljG59ylGtSLismoLojXPAGvnh2ipQr3BYz9KZQr+1sdASCT+sR/v8dSMDS96xGCdtln2wHfAHCnLJK+XcBNg==} 330 | dev: true 331 | 332 | /pretty-bytes/5.6.0: 333 | resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} 334 | engines: {node: '>=6'} 335 | dev: true 336 | 337 | /pretty-format/3.8.0: 338 | resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} 339 | dev: true 340 | 341 | /pretty-ms/7.0.1: 342 | resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} 343 | engines: {node: '>=10'} 344 | dependencies: 345 | parse-ms: 2.1.0 346 | dev: true 347 | 348 | /sirv/2.0.2: 349 | resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} 350 | engines: {node: '>= 10'} 351 | dependencies: 352 | '@polka/url': 1.0.0-next.21 353 | mrmime: 1.0.1 354 | totalist: 3.0.0 355 | dev: true 356 | 357 | /time-span/4.0.0: 358 | resolution: {integrity: sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g==} 359 | engines: {node: '>=10'} 360 | dependencies: 361 | convert-hrtime: 3.0.0 362 | dev: true 363 | 364 | /totalist/3.0.0: 365 | resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==} 366 | engines: {node: '>=6'} 367 | dev: true 368 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | export default function App({ req, isCold }) { 2 | const parsedCity = decodeURIComponent(req.headers.get('x-vercel-ip-city')); 3 | // from vercel we get the string `null` when it can't decode the IP 4 | const city = parsedCity === 'null' ? null : parsedCity; 5 | const ip = (req.headers.get('x-forwarded-for') ?? '127.0.0.1').split(',')[0]; 6 | 7 | return ( 8 | 9 | 10 | 11 |
12 | 13 | 14 |
15 |

16 | Hello from the edge! 17 |

18 | 19 |
20 |
21 |
22 | Your city 23 | 31 | {city === null ? 'N/A' : city} 32 | 33 |
34 |
35 | 36 |
37 |
38 | Your IP address 39 | {ip} 40 |
41 |
42 |
43 |
44 |
45 | Generated at {new Date().toISOString()} ({isCold ? 'cold' : 'hot'}) 46 |
47 |
48 | 49 |