├── .eslintrc.js
├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── LICENSE
├── README.md
├── assets
├── code
│ └── roadmap.png
└── social
│ └── banner.png
├── bun.lockb
├── deno_dist
├── core
│ ├── constants
│ │ ├── codes.ts
│ │ ├── index.ts
│ │ └── mime
│ │ │ ├── application.ts
│ │ │ ├── audio.ts
│ │ │ ├── font.ts
│ │ │ ├── image.ts
│ │ │ ├── index.ts
│ │ │ ├── text.ts
│ │ │ └── video.ts
│ ├── context.ts
│ ├── errors
│ │ ├── config.ts
│ │ ├── error.ts
│ │ ├── handler.ts
│ │ └── index.ts
│ ├── middleware.ts
│ ├── response.ts
│ ├── response
│ │ ├── created.ts
│ │ └── creator.ts
│ ├── server.ts
│ ├── types.ts
│ └── utils
│ │ ├── app.ts
│ │ ├── choose.ts
│ │ ├── console
│ │ └── colors.ts
│ │ ├── is.ts
│ │ ├── mime.ts
│ │ ├── parsers
│ │ ├── base64.ts
│ │ ├── buffer.ts
│ │ ├── form-data.ts
│ │ ├── json.ts
│ │ ├── query-string.ts
│ │ └── req-body.ts
│ │ ├── sanitize.ts
│ │ └── types.ts
├── index.ts
├── middlewares
│ ├── README.md
│ ├── index.ts
│ ├── mw-body-parser.ts
│ ├── mw-cors.ts
│ ├── mw-request-id.ts
│ └── mw-uploads.ts
└── mod.ts
├── example
├── app-with-adv-route.ts
├── app-with-middleware.ts
├── app-with-mounting.ts
├── app-with-mw-body-parser.ts
├── app-with-mw-cors.ts
├── app-with-mw-request-id.ts
├── app-with-mw-uploads.ts
├── app-with-route-grouping.ts
├── app.ts
├── deno
│ ├── .vscode
│ │ └── settings.json
│ └── app.ts
└── deps
│ ├── mw-logger.ts
│ └── sub-app.ts
├── nodemon.json
├── package.json
├── src
├── core
│ ├── constants
│ │ ├── codes.ts
│ │ ├── index.ts
│ │ └── mime
│ │ │ ├── application.ts
│ │ │ ├── audio.ts
│ │ │ ├── font.ts
│ │ │ ├── image.ts
│ │ │ ├── index.ts
│ │ │ ├── text.ts
│ │ │ └── video.ts
│ ├── context.ts
│ ├── errors
│ │ ├── config.ts
│ │ ├── error.ts
│ │ ├── handler.ts
│ │ └── index.ts
│ ├── middleware.ts
│ ├── response.ts
│ ├── response
│ │ ├── created.ts
│ │ └── creator.ts
│ ├── server.ts
│ ├── types.ts
│ └── utils
│ │ ├── app.ts
│ │ ├── choose.ts
│ │ ├── console
│ │ └── colors.ts
│ │ ├── is.ts
│ │ ├── mime.ts
│ │ ├── oss
│ │ └── escape_html.ts
│ │ ├── parsers
│ │ ├── base64.ts
│ │ ├── buffer.ts
│ │ ├── form-data.ts
│ │ ├── html.ts
│ │ ├── json.ts
│ │ ├── query-string.ts
│ │ └── req-body.ts
│ │ ├── sanitize.ts
│ │ └── types.ts
├── index.ts
└── middlewares
│ ├── README.md
│ ├── index.ts
│ ├── mw-body-parser.ts
│ ├── mw-cors.ts
│ ├── mw-request-id.ts
│ └── mw-uploads.ts
├── tests
├── .gitkeep
├── mime.test.ts
└── server.test.ts
├── tsconfig.json
├── tsup.config.ts
└── vite.config.ts
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es2021: true,
4 | node: true
5 | },
6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
7 | parser: '@typescript-eslint/parser',
8 | parserOptions: {
9 | ecmaVersion: 'latest',
10 | sourceType: 'module'
11 | },
12 | plugins: ['@typescript-eslint'],
13 | rules: {
14 | '@typescript-eslint/ban-types': 'off',
15 | '@typescript-eslint/no-explicit-any': 'off',
16 | 'no-mixed-spaces-and-tabs': 'off'
17 | },
18 | ignorePatterns: ['example/*', 'tests/**/*']
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 |
16 | - uses: actions/setup-node@v3
17 | with:
18 | node-version: 16.x
19 |
20 | - run: npx changelogithub
21 | env:
22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | _.log
7 | npm-debug.log_
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Diagnostic reports (https://nodejs.org/api/report.html)
14 |
15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
16 |
17 | # Runtime data
18 |
19 | pids
20 | _.pid
21 | _.seed
22 | \*.pid.lock
23 |
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 |
26 | lib-cov
27 |
28 | # Coverage directory used by tools like istanbul
29 |
30 | coverage
31 | \*.lcov
32 |
33 | # nyc test coverage
34 |
35 | .nyc_output
36 |
37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
38 |
39 | .grunt
40 |
41 | # Bower dependency directory (https://bower.io/)
42 |
43 | bower_components
44 |
45 | # node-waf configuration
46 |
47 | .lock-wscript
48 |
49 | # Compiled binary addons (https://nodejs.org/api/addons.html)
50 |
51 | build/Release
52 |
53 | # Dependency directories
54 |
55 | node_modules/
56 | jspm_packages/
57 |
58 | # Snowpack dependency directory (https://snowpack.dev/)
59 |
60 | web_modules/
61 |
62 | # TypeScript cache
63 |
64 | \*.tsbuildinfo
65 |
66 | # Optional npm cache directory
67 |
68 | .npm
69 |
70 | # Optional eslint cache
71 |
72 | .eslintcache
73 |
74 | # Optional stylelint cache
75 |
76 | .stylelintcache
77 |
78 | # Microbundle cache
79 |
80 | .rpt2_cache/
81 | .rts2_cache_cjs/
82 | .rts2_cache_es/
83 | .rts2_cache_umd/
84 |
85 | # Optional REPL history
86 |
87 | .node_repl_history
88 |
89 | # Output of 'npm pack'
90 |
91 | \*.tgz
92 |
93 | # Yarn Integrity file
94 |
95 | .yarn-integrity
96 |
97 | # dotenv environment variable files
98 |
99 | .env
100 | .env.development.local
101 | .env.test.local
102 | .env.production.local
103 | .env.local
104 |
105 | # parcel-bundler cache (https://parceljs.org/)
106 |
107 | .cache
108 | .parcel-cache
109 |
110 | # Next.js build output
111 |
112 | .next
113 | out
114 |
115 | # Nuxt.js build / generate output
116 |
117 | .nuxt
118 | dist
119 |
120 | # Gatsby files
121 |
122 | .cache/
123 |
124 | # Comment in the public line in if your project uses Gatsby and not Next.js
125 |
126 | # https://nextjs.org/blog/next-9-1#public-directory-support
127 |
128 | # public
129 |
130 | # vuepress build output
131 |
132 | .vuepress/dist
133 |
134 | # vuepress v2.x temp and cache directory
135 |
136 | .temp
137 | .cache
138 |
139 | # Docusaurus cache and generated files
140 |
141 | .docusaurus
142 |
143 | # Serverless directories
144 |
145 | .serverless/
146 |
147 | # FuseBox cache
148 |
149 | .fusebox/
150 |
151 | # DynamoDB Local files
152 |
153 | .dynamodb/
154 |
155 | # TernJS port file
156 |
157 | .tern-port
158 |
159 | # Stores VSCode versions used for testing VSCode extensions
160 |
161 | .vscode-test
162 |
163 | # yarn v2
164 |
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.\*
170 |
171 | # WIP resources
172 | .idea
173 | local_store.sqlite
174 |
175 | # Deno
176 | # deno_dist
177 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example/
2 | docs/
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true,
6 | "trailingComma": "none"
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Aftab Alam
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Zarf
2 | Fast, Bun-powered, and Bun-only(for now) Web API framework with full Typescript support.
3 |
4 | ## Quickstart
5 | Starting with `Zarf` is as simple as instantiating the `Zarf` class, attaching route handlers and finally starting the server
6 | ```ts
7 | import { Zarf } from "@zarfjs/zarf"
8 |
9 | const app = new Zarf()
10 |
11 | app.get("/hello", (ctx) => {
12 | return ctx.json({
13 | hello: "hello"
14 | })
15 | })
16 |
17 | app.get("/", (ctx) => {
18 | return ctx.html(`Welcome to Zarf App server`)
19 | })
20 |
21 | app.listen({
22 | port: 3000
23 | }, (server) => {
24 | console.log(`Server started on ${server.port}`)
25 | })
26 | ```
27 | ## App and Routing
28 | Routes are how you tell where/when/what to respond when somebody visits your app's URLs, and `@zarfjs/zarf` lets you easily register routes, with all the commonly used HTTP verbs like `GET`, `POST`, `PUT`, `DELETE`, etc.
29 |
30 | Here's how you'd define your app routes -
31 |
32 | ```ts
33 | // GET
34 | app.get("/posts", (ctx) => {
35 | return ctx.json({
36 | posts: [/* all of the posts */]
37 | })
38 | })
39 |
40 | // POST
41 | app.post("/posts", async(ctx) => {
42 | const { request } = ctx
43 | const body = await request?.json()
44 | // ... validate the post body
45 | // ... create a post entry
46 | return ctx.json(body)
47 | })
48 |
49 | // PUT
50 | app.put("/posts/:id", async(ctx, params) => {
51 | const { request } = ctx
52 | const id = params.id
53 | const body = await request?.json()
54 | // ... validate the post body
55 | // ... upadte the post entry
56 | return ctx.json(body)
57 | })
58 |
59 | // DELETE
60 | app.del("/posts/:id", async(ctx, params) => {
61 | const id = params.id
62 | // ... validate the del op
63 | // ... delete the post entry
64 | return ctx.json({ deleted: 1 })
65 | })
66 |
67 | ```
68 | ## Routing: Context
69 | `Context` available as the first argument to your route handlers is a special object made available to all the route handlers which
70 | - lets you access vaious details w.r.t `Request` object
71 | - provides convenience methods like `json`, `text`, `html` to send `Response` to the client
72 |
73 | The most accessed/useful object could be the `Request` object itself(available at `ctx.request`), but it offers few other methods too
74 | - `setHeader`
75 | - `setType`
76 | - `setVary`
77 | - `isType`
78 | - `accepts`
79 | to determine things about the current request, or change few things about the response that's send to the client.
80 |
81 | ## Routing: Params
82 | `Params` is the second argument available to your route handlers, that lets you access the route parameters easily.
83 | ```ts
84 | app.get("/products/:id", (ctx, params) => {
85 | // params.id ? //
86 | // Pull the details
87 | return ctx.json({
88 | product: {/* all of the posts */}
89 | })
90 | })
91 | ```
92 | `@zarfjs/zarf` supports all the common URL patterns you'd expect in a Web-App/API framework
93 | ```ts
94 | app.get("/user/:name/books/:title", (ctx, params) => {
95 | const { name, title } = params
96 | return ctx.json({
97 | name,
98 | title
99 | })
100 | })
101 |
102 | app.get("/user/:name?", (ctx, params) => {
103 | return ctx.json({
104 | name: params.name || 'No name found'
105 | })
106 | })
107 |
108 | // /admin/feature/path/goes/here
109 | app.get("/admin/*all", (ctx, params) => {
110 | return ctx.json({
111 | supPath: params.all // -> /feature/path/goes/here
112 | })
113 | })
114 |
115 | // /v1/nike/shop/uk
116 | // /v1/nike/uk/shop/shop-at...
117 | app.get("/v1/*brand/shop/*name", (ctx, params) => {
118 | return ctx.json({
119 | params // -> { brand: 'nike', ...}, { brand: 'nike/uk', ...}
120 | })
121 | })
122 | ```
123 |
124 | ## RoadMap
125 | A lot of great stuff is actually planned for the project. The Alpha version is majorly focussing on making the core stable and provide all the essential features. Here's snapshot of the [roadmap](https://github.com/users/one-aalam/projects/3/views/1).(private)
126 |
127 |
128 |
129 |
130 |
131 | ## Meta
132 | The project is developed on
133 | - OS - MacOS Monterey
134 | - Bun - v0.1.13
135 |
--------------------------------------------------------------------------------
/assets/code/roadmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zarfjs/zarf/56bd2fafd61729b85a7b054654da1fba2ff62f3d/assets/code/roadmap.png
--------------------------------------------------------------------------------
/assets/social/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zarfjs/zarf/56bd2fafd61729b85a7b054654da1fba2ff62f3d/assets/social/banner.png
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zarfjs/zarf/56bd2fafd61729b85a7b054654da1fba2ff62f3d/bun.lockb
--------------------------------------------------------------------------------
/deno_dist/core/constants/codes.ts:
--------------------------------------------------------------------------------
1 | import type { Replace } from '../utils/types.ts'
2 | export const HTTP_STATUS_CODES = {
3 | // Informational
4 | 100: 'Continue',
5 | 101: 'Switching Protocols',
6 | // Successful
7 | 200: 'OK',
8 | 201: 'Created',
9 | 202: 'Accepted',
10 | 203: 'Non-Authoritative Information',
11 | 204: 'No Content',
12 | 205: 'Reset Content',
13 | 206: 'Partial Content',
14 | // Redirection
15 | 301: 'Moved Permanently',
16 | 302: 'Found',
17 | 303: 'See Other',
18 | 304: 'Not Modified',
19 | 307: 'Temporary Redirect',
20 | 308: 'Permanent Redirect',
21 | // Client Error
22 | 400: 'Bad Request',
23 | 401: 'Unauthorized',
24 | 403: 'Forbidden',
25 | 404: 'Not Found',
26 | 405: 'Method Not Allowed',
27 | 406: 'Not Acceptable',
28 | 407: 'Proxy Authentication Required',
29 | 408: 'Request Timeout',
30 | 409: 'Conflict',
31 | 410: 'Gone',
32 | 422: 'Unprocessable Entity',
33 | // Server Error
34 | 500: 'Internal Server Error',
35 | 501: 'Not Implemented',
36 | 502: 'Bad Gateway',
37 | 503: 'Service Unavailable',
38 | 504: 'Gateway Timeout'
39 | } as const
40 |
41 | export type HTTPStatusCode = keyof typeof HTTP_STATUS_CODES
42 | export type HTTPStatusCodeMesssage = typeof HTTP_STATUS_CODES[HTTPStatusCode]
43 | export type HTTPStatusCodeMesssageKey = Replace, '-', '', { all: true }>
44 |
--------------------------------------------------------------------------------
/deno_dist/core/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const ROUTE_OPTION_DEFAULT = Symbol('default')
2 |
--------------------------------------------------------------------------------
/deno_dist/core/constants/mime/application.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from './index.ts'
2 |
3 | export type AppType = 'application'
4 | export type AppMimeType = `${AppType}/${string}`
5 |
6 | export const MIME_APP: Record = {
7 | 'application/atom+xml': {
8 | cmp: true,
9 | ext: ['atom']
10 | },
11 | 'application/calendar+json': {
12 | cmp: true
13 | },
14 | 'application/calendar+xml': {
15 | cmp: true,
16 | ext: ['xcs']
17 | },
18 | 'application/epub+zip': {
19 | cmp: false,
20 | ext: ['epub']
21 | },
22 | 'application/font-woff': {
23 | cmp: false
24 | },
25 | 'application/geo+json': {
26 | cmp: true,
27 | ext: ['geojson']
28 | },
29 | 'application/inkml+xml': {
30 | cmp: true,
31 | ext: ['ink', 'inkml']
32 | },
33 |
34 | 'application/java-archive': {
35 | cmp: false,
36 | ext: ['jar', 'war', 'ear']
37 | },
38 | 'application/java-serialized-object': {
39 | cmp: false,
40 | ext: ['ser']
41 | },
42 | 'application/java-vm': {
43 | cmp: false,
44 | ext: ['class']
45 | },
46 | 'application/javascript': {
47 | charset: 'UTF-8',
48 | cmp: true,
49 | ext: ['js', 'mjs']
50 | },
51 | 'application/jf2feed+json': {
52 | cmp: true
53 | },
54 | 'application/json': {
55 | charset: 'UTF-8',
56 | cmp: true,
57 | ext: ['json', 'map']
58 | },
59 | 'application/json-patch+json': {
60 | cmp: true
61 | },
62 | 'application/json5': {
63 | ext: ['json5']
64 | },
65 | 'application/jsonml+json': {
66 | cmp: true,
67 | ext: ['jsonml']
68 | },
69 | 'application/ld+json': {
70 | cmp: true,
71 | ext: ['jsonld']
72 | },
73 | 'application/manifest+json': {
74 | charset: 'UTF-8',
75 | cmp: true,
76 | ext: ['webmanifest']
77 | },
78 | 'application/msword': {
79 | cmp: false,
80 | ext: ['doc', 'dot']
81 | },
82 | 'application/node': {
83 | ext: ['cjs']
84 | },
85 | 'application/octet-stream': {
86 | cmp: false,
87 | ext: [
88 | 'bin',
89 | 'dms',
90 | 'lrf',
91 | 'mar',
92 | 'so',
93 | 'dist',
94 | 'distz',
95 | 'pkg',
96 | 'bpk',
97 | 'dump',
98 | 'elc',
99 | 'deploy',
100 | 'exe',
101 | 'dll',
102 | 'deb',
103 | 'dmg',
104 | 'iso',
105 | 'img',
106 | 'msi',
107 | 'msp',
108 | 'msm',
109 | 'buffer'
110 | ]
111 | },
112 | 'application/pdf': {
113 | cmp: false,
114 | ext: ['pdf']
115 | },
116 | 'application/gzip': {
117 | cmp: false,
118 | ext: ['gz']
119 | },
120 | 'application/x-bzip': {
121 | cmp: false,
122 | ext: ['bz']
123 | },
124 | 'application/x-bzip2': {
125 | cmp: false,
126 | ext: ['bz2', 'boz']
127 | },
128 |
129 | 'application/x-mobipocket-ebook': {
130 | ext: ['prc', 'mobi']
131 | },
132 | 'application/x-mpegurl': {
133 | cmp: false
134 | },
135 | 'application/x-ms-application': {
136 | ext: ['application']
137 | },
138 | 'application/x-msdos-program': {
139 | ext: ['exe']
140 | },
141 | 'application/x-msdownload': {
142 | ext: ['exe', 'dll', 'com', 'bat', 'msi']
143 | },
144 | 'application/x-rar-compressed': {
145 | cmp: false,
146 | ext: ['rar']
147 | },
148 | 'application/x-shockwave-flash': {
149 | cmp: false,
150 | ext: ['swf']
151 | },
152 | 'application/x-sql': {
153 | ext: ['sql']
154 | },
155 | 'application/x-web-app-manifest+json': {
156 | cmp: true,
157 | ext: ['webapp']
158 | },
159 | 'application/x-www-form-urlencoded': {
160 | cmp: true
161 | },
162 | 'application/xhtml+xml': {
163 | cmp: true,
164 | ext: ['xhtml', 'xht']
165 | },
166 | 'application/xml': {
167 | cmp: true,
168 | ext: ['xml', 'xsl', 'xsd', 'rng']
169 | },
170 | 'application/zip': {
171 | cmp: false,
172 | ext: ['zip']
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/deno_dist/core/constants/mime/audio.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from "./index.ts"
2 | export type AudioType = 'audio'
3 | export type AudioMimeType = `${AudioType}/${string}`
4 |
5 | export const MIME_AUDIO: Record = {
6 | "audio/3gpp": {
7 | "cmp": false,
8 | "ext": ["3gpp"]
9 | },
10 | "audio/3gpp2": {
11 | },
12 | "audio/aac": {
13 | },
14 | "audio/ac3": {
15 | },
16 | "audio/midi": {
17 | "ext": ["mid","midi","kar","rmi"]
18 | },
19 | "audio/mp3": {
20 | "cmp": false,
21 | "ext": ["mp3"]
22 | },
23 | "audio/mp4": {
24 | "cmp": false,
25 | "ext": ["m4a","mp4a"]
26 | },
27 | "audio/mpeg": {
28 | "cmp": false,
29 | "ext": ["mpga","mp2","mp2a","mp3","m2a","m3a"]
30 | },
31 | "audio/ogg": {
32 | "cmp": false,
33 | "ext": ["oga","ogg","spx","opus"]
34 | },
35 |
36 | "audio/wav": {
37 | "cmp": false,
38 | "ext": ["wav"]
39 | },
40 | "audio/wave": {
41 | "cmp": false,
42 | "ext": ["wav"]
43 | },
44 | "audio/webm": {
45 |
46 | "cmp": false,
47 | "ext": ["weba"]
48 | },
49 | "audio/x-aac": {
50 |
51 | "cmp": false,
52 | "ext": ["aac"]
53 | },
54 | "audio/x-flac": {
55 | "ext": ["flac"]
56 | },
57 | "audio/x-m4a": {
58 | "ext": ["m4a"]
59 | },
60 | "audio/x-matroska": {
61 | "ext": ["mka"]
62 | },
63 | "audio/x-mpegurl": {
64 | "ext": ["m3u"]
65 | },
66 | "audio/x-ms-wma": {
67 | "ext": ["wma"]
68 | },
69 | "audio/x-wav": {
70 | "ext": ["wav"]
71 | },
72 | "audio/xm": {
73 | "ext": ["xm"]
74 | },
75 | }
76 |
--------------------------------------------------------------------------------
/deno_dist/core/constants/mime/font.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from './index.ts'
2 |
3 | export type FontType = 'font'
4 | export type FontMimeType = `${FontType}/${string}`
5 |
6 | export const MIME_FONT: Record = {
7 | 'font/collection': {
8 | ext: ['ttc']
9 | },
10 | 'font/otf': {
11 | cmp: true,
12 | ext: ['otf']
13 | },
14 | 'font/ttf': {
15 | cmp: true,
16 | ext: ['ttf']
17 | },
18 | 'font/woff': {
19 | ext: ['woff']
20 | },
21 | 'font/woff2': {
22 | ext: ['woff2']
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/deno_dist/core/constants/mime/image.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from './index.ts'
2 |
3 | export type ImageType = 'image'
4 | export type ImageMimeType = `${ImageType}/${string}`
5 |
6 | export const MIME_IMAGE: Record = {
7 | 'image/apng': {
8 | cmp: false,
9 | ext: ['apng']
10 | },
11 | 'image/avci': {
12 | ext: ['avci']
13 | },
14 | 'image/avcs': {
15 | ext: ['avcs']
16 | },
17 | 'image/avif': {
18 | cmp: false,
19 | ext: ['avif']
20 | },
21 | 'image/bmp': {
22 | cmp: true,
23 | ext: ['bmp']
24 | },
25 |
26 | 'image/gif': {
27 | cmp: false,
28 | ext: ['gif']
29 | },
30 |
31 | 'image/jp2': {
32 | cmp: false,
33 | ext: ['jp2', 'jpg2']
34 | },
35 | 'image/jpeg': {
36 | cmp: false,
37 | ext: ['jpeg', 'jpg', 'jpe']
38 | },
39 |
40 | 'image/pjpeg': {
41 | cmp: false
42 | },
43 | 'image/png': {
44 | cmp: false,
45 | ext: ['png']
46 | },
47 |
48 | 'image/svg+xml': {
49 | cmp: true,
50 | ext: ['svg', 'svgz']
51 | },
52 | 'image/t38': {
53 | ext: ['t38']
54 | },
55 | 'image/tiff': {
56 | cmp: false,
57 | ext: ['tif', 'tiff']
58 | },
59 | 'image/tiff-fx': {
60 | ext: ['tfx']
61 | },
62 | 'image/vnd.adobe.photoshop': {
63 | cmp: true,
64 | ext: ['psd']
65 | },
66 |
67 | 'image/vnd.microsoft.icon': {
68 | cmp: true,
69 | ext: ['ico']
70 | },
71 |
72 | 'image/vnd.ms-photo': {
73 | ext: ['wdp']
74 | },
75 | 'image/vnd.net-fpx': {
76 | ext: ['npx']
77 | },
78 |
79 | 'image/webp': {
80 | ext: ['webp']
81 | },
82 | 'image/wmf': {
83 | ext: ['wmf']
84 | },
85 |
86 | 'image/x-icon': {
87 | cmp: true,
88 | ext: ['ico']
89 | },
90 |
91 | 'image/x-ms-bmp': {
92 | cmp: true,
93 | ext: ['bmp']
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/deno_dist/core/constants/mime/index.ts:
--------------------------------------------------------------------------------
1 | import { TextMimeType, MIME_TEXT } from './text.ts'
2 | import { FontMimeType, MIME_FONT } from './font.ts'
3 | import { ImageMimeType, MIME_IMAGE } from './image.ts'
4 | import { AudioMimeType, MIME_AUDIO } from './audio.ts'
5 | import { VideoMimeType, MIME_VIDEO } from './video.ts'
6 | import { AppMimeType , MIME_APP } from './application.ts'
7 |
8 | export type MultipartMimeType = `multipart/${string}`
9 | export type MimeType = TextMimeType | FontMimeType | ImageMimeType | AudioMimeType | VideoMimeType | MultipartMimeType | AppMimeType
10 | export type MimeTypeConfig = {
11 | ext?: Array,
12 | cmp?: boolean,
13 | charset?: "UTF-8"
14 | }
15 |
16 | export const MIME_TYPES_CONFIG: Record = {
17 | ...MIME_TEXT,
18 | ...MIME_FONT,
19 | ...MIME_IMAGE,
20 | ...MIME_AUDIO,
21 | ...MIME_VIDEO,
22 | ...MIME_APP,
23 | "multipart/form-data": {
24 | "cmp": false
25 | }
26 | }
27 |
28 | function populateExtensionsMimeTypes() {
29 | Object.entries(MIME_TYPES_CONFIG).forEach(([mimeType, mimeConfig]) => {
30 | if(mimeConfig.ext && mimeConfig.ext.length) {
31 | MIME_TYPE_EXT[mimeType as MimeType] = mimeConfig.ext
32 | mimeConfig.ext.forEach(ext => {
33 | EXT_MIME_TYPES[ext] = mimeType as MimeType
34 | })
35 | }
36 | })
37 | }
38 |
39 | // Runtime consts
40 | export const EXT_MIME_TYPES: Record = Object.create(null);
41 | export const MIME_TYPE_EXT: Record> = Object.create(null);
42 |
43 | populateExtensionsMimeTypes()
44 |
--------------------------------------------------------------------------------
/deno_dist/core/constants/mime/text.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from './index.ts'
2 |
3 | export type TextType = 'text'
4 | export type TextMimeType = `${TextType}/${string}`
5 |
6 | export const MIME_TEXT: Record = {
7 | 'text/cache-manifest': {
8 | cmp: true,
9 | ext: ['appcache', 'manifest']
10 | },
11 | 'text/calendar': {
12 | ext: ['ics', 'ifb']
13 | },
14 | 'text/calender': {
15 | cmp: true
16 | },
17 | 'text/cmd': {
18 | cmp: true
19 | },
20 | 'text/coffeescript': {
21 | ext: ['coffee', 'litcoffee']
22 | },
23 | 'text/css': {
24 | charset: 'UTF-8',
25 | cmp: true,
26 | ext: ['css']
27 | },
28 | 'text/csv': {
29 | cmp: true,
30 | ext: ['csv']
31 | },
32 | 'text/html': {
33 | cmp: true,
34 | ext: ['html', 'htm', 'shtml']
35 | },
36 | 'text/jade': {
37 | ext: ['jade']
38 | },
39 | 'text/javascript': {
40 | cmp: true
41 | },
42 | 'text/jsx': {
43 | cmp: true,
44 | ext: ['jsx']
45 | },
46 | 'text/less': {
47 | cmp: true,
48 | ext: ['less']
49 | },
50 | 'text/markdown': {
51 | cmp: true,
52 | ext: ['markdown', 'md']
53 | },
54 | 'text/mdx': {
55 | cmp: true,
56 | ext: ['mdx']
57 | },
58 | 'text/plain': {
59 | cmp: true,
60 | ext: ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini']
61 | },
62 | 'text/richtext': {
63 | cmp: true,
64 | ext: ['rtx']
65 | },
66 | 'text/rtf': {
67 | cmp: true,
68 | ext: ['rtf']
69 | },
70 | 'text/stylus': {
71 | ext: ['stylus', 'styl']
72 | },
73 | 'text/tab-separated-values': {
74 | cmp: true,
75 | ext: ['tsv']
76 | },
77 | 'text/uri-list': {
78 | cmp: true,
79 | ext: ['uri', 'uris', 'urls']
80 | },
81 | 'text/vcard': {
82 | cmp: true,
83 | ext: ['vcard']
84 | },
85 | 'text/vnd.dvb.subtitle': {
86 | ext: ['sub']
87 | },
88 | 'text/vtt': {
89 | charset: 'UTF-8',
90 | cmp: true,
91 | ext: ['vtt']
92 | },
93 | 'text/x-handlebars-template': {
94 | ext: ['hbs']
95 | },
96 | 'text/xml': {
97 | cmp: true,
98 | ext: ['xml']
99 | },
100 | 'text/yaml': {
101 | ext: ['yaml', 'yml']
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/deno_dist/core/constants/mime/video.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from './index.ts'
2 |
3 | export type VideoType = 'video'
4 | export type VideoMimeType = `${VideoType}/${string}`
5 |
6 | export const MIME_VIDEO: Record = {
7 | 'video/h264': {
8 | ext: ['h264']
9 | },
10 | 'video/h265': {},
11 | 'video/mp4': {
12 | cmp: false,
13 | ext: ['mp4', 'mp4v', 'mpg4']
14 | },
15 | 'video/mpeg': {
16 | cmp: false,
17 | ext: ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v']
18 | },
19 | 'video/ogg': {
20 | cmp: false,
21 | ext: ['ogv']
22 | },
23 | 'video/quicktime': {
24 | cmp: false,
25 | ext: ['qt', 'mov']
26 | },
27 | 'video/vnd.mpegurl': {
28 | ext: ['mxu', 'm4u']
29 | },
30 | 'video/webm': {
31 | cmp: false,
32 | ext: ['webm']
33 | },
34 | 'video/x-f4v': {
35 | ext: ['f4v']
36 | },
37 | 'video/x-fli': {
38 | ext: ['fli']
39 | },
40 | 'video/x-flv': {
41 | cmp: false,
42 | ext: ['flv']
43 | },
44 | 'video/x-m4v': {
45 | ext: ['m4v']
46 | },
47 | 'video/x-matroska': {
48 | cmp: false,
49 | ext: ['mkv', 'mk3d', 'mks']
50 | },
51 | 'video/x-msvideo': {
52 | ext: ['avi']
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/deno_dist/core/context.ts:
--------------------------------------------------------------------------------
1 | import type { ContextMeta, ZarfConfig, RouteMethod, HeaderVaryContent, HeaderTypeContent } from './types.ts'
2 | import type { ParsedBody } from './utils/parsers/req-body.ts'
3 | import { json, text, head, send, html } from './response.ts'
4 | import { getContentType } from './utils/mime.ts'
5 | import { MiddlewareFunction } from './middleware.ts'
6 |
7 | // @ts-ignore
8 | const NEEDS_WARMUP = globalThis && globalThis.process && globalThis.process.isBun ? true : false
9 |
10 | /**
11 | * Execution context for handlers and all the middlewares
12 | */
13 | export class AppContext = {}> {
14 | private _response: Response | null
15 | private _config: ZarfConfig = {}
16 | private _error: any
17 | private _code: number | undefined;
18 | private _middlewares: Array> = []
19 |
20 | private readonly _request: Request | null
21 | readonly url: URL;
22 | readonly method: RouteMethod
23 | readonly host: string | undefined;
24 | readonly path: string;
25 | readonly query: URLSearchParams | undefined;
26 | readonly headers: Request["headers"]
27 |
28 | private _locals: S = {} as S
29 | readonly meta: ContextMeta = {
30 | startTime: 0
31 | }
32 | private _isImmediate: boolean = false
33 |
34 | public body: ParsedBody | null = null
35 |
36 | constructor(req: Request, config: ZarfConfig) {
37 | this.meta.startTime = Date.now()
38 | this._config = config
39 | this._request = req
40 | this._response = new Response('')
41 |
42 | this.method = req.method.toLowerCase() as RouteMethod
43 | this.url = new URL(req.url)
44 | this.host = this.url.host
45 | this.path = this._config?.strictRouting || this.url.pathname === '/' ?
46 | this.url.pathname :
47 | this.url.pathname.endsWith('/') ?
48 | this.url.pathname.substring(0, this.url.pathname.length -1) :
49 | this.url.pathname
50 |
51 | this.query = new Proxy(new URLSearchParams(req.url), {
52 | get: (params, param) => params.get(param as string),
53 | })
54 | this.headers = new Proxy(this._request.headers, {
55 | get: (headers, header) => headers.get(header as string),
56 | })
57 |
58 | if(this._config?.serverHeader) {
59 | this._response.headers.set('Server', this._config.serverHeader)
60 | }
61 |
62 | /**
63 | * Currently needed for reading request body using `json`, `text` or `arrayBuffer`
64 | * without taking forever to resolve when any of them are accessed later
65 | *
66 | * But, needed only for `Bun`. If left applied for all the cases, this creates an issue with Node.js, Deno, etc.
67 | */
68 | if(NEEDS_WARMUP) {
69 | this._request.blob();
70 | }
71 | }
72 |
73 | /**
74 | * Get the current request in the raw form
75 | */
76 | get request() {
77 | return this._request
78 | }
79 |
80 | /**
81 | * Get the `Response` if any
82 | */
83 | get response() {
84 | return this._response
85 | }
86 | /**
87 | * Set the `Response`
88 | */
89 | set response(resp) {
90 | this._response = resp
91 | }
92 |
93 | /**
94 | * Get the current status code
95 | */
96 | get status() {
97 | return this._code!
98 | }
99 | /**
100 | * Set the current status code
101 | */
102 | set status(code: number) {
103 | this._code = code;
104 | }
105 |
106 | get isImmediate() {
107 | return this._isImmediate
108 | }
109 |
110 | /// HEADER HELPERS ///
111 | setHeader(headerKey: string, headerVal: string) {
112 | return this._response?.headers.set(headerKey, headerVal)
113 | }
114 |
115 | setType(headerVal: HeaderTypeContent) {
116 | const contentType = getContentType(headerVal)
117 | if(contentType) {
118 | this.setHeader('Content-Type', getContentType(headerVal) as string)
119 | }
120 | return
121 | }
122 |
123 | isType(headerVal: HeaderTypeContent) {
124 | return this._request?.headers.get('Content-Type') === getContentType(headerVal)
125 | }
126 |
127 | accepts(headerVal: HeaderTypeContent) {
128 | return this._request?.headers.get('Accepts')?.includes(getContentType(headerVal) || '')
129 | }
130 |
131 | // https://www.smashingmagazine.com/2017/11/understanding-vary-header/
132 | setVary(...headerVals: Array) {
133 | if(headerVals.length) {
134 | const varies = (this._response?.headers.get('Vary') || '').split(',')
135 | this._response?.headers.set('Vary', [...new Set([...varies ,...headerVals])].join(','))
136 | }
137 | }
138 |
139 | /**
140 | * Get Error
141 | */
142 | get error() {
143 | return this._error
144 | }
145 | /**
146 | * Set Error
147 | */
148 | set error(err) {
149 | this._error = err
150 | }
151 |
152 | // Getter/Setter for App-specific details
153 | /**
154 | * Get available App-specific details
155 | */
156 | get locals() {
157 | return this._locals as S
158 | }
159 | /**
160 | * Set App-specific details
161 | */
162 | set locals(value: S) {
163 | this._locals = value
164 | }
165 |
166 | // Context helpers to send the `Response` in all the supported formats
167 | /**
168 | * Send the response as string, json, etc. - One sender to rule `em all!
169 | * @param body
170 | * @returns
171 | */
172 | async send(body: any, args: ResponseInit = {}): Promise {
173 | if(this._request?.method === 'HEAD') return this.head()
174 | return await send(body, {...this.getResponseInit(), ...args})
175 | }
176 |
177 | /**
178 | * Send the provided values as `json`
179 | * @param body
180 | * @returns
181 | */
182 | json(body: any, args: ResponseInit = {}): Response {
183 | return json(body, {...this.getResponseInit(), ...args})
184 | }
185 |
186 | /**
187 | * Send the provided value as `text`
188 | * @param _text
189 | * @returns
190 | */
191 | text(_text: string, args: ResponseInit = {}): Response {
192 | return text(_text, {...this.getResponseInit(), ...args})
193 | }
194 |
195 | /**
196 | * Send the provided value as `html`
197 | * @param _text
198 | * @returns
199 | */
200 | html(text: string, args: ResponseInit = {}): Response {
201 | return html(text, {...this.getResponseInit(), ...args})
202 | }
203 |
204 | /**
205 | * Just return with `head` details
206 | * @returns
207 | */
208 | head(args: ResponseInit = {}): Response {
209 | return head({...this.getResponseInit(), ...args})
210 | }
211 |
212 |
213 | /**
214 | * Halt flow, and return with currently provided status
215 | *
216 | * @param statusCode status code to use
217 | * @param body the body to send in response. Could be `json`, `string`, etc.
218 | * @returns
219 | */
220 | async halt(statusCode: number, body: any): Promise {
221 | this._isImmediate = true
222 | return this.send(body, { ...this.getResponseInit(), status: statusCode })
223 | }
224 |
225 | /**
226 | * Redirect to the given URL
227 | *
228 | * @param url
229 | * @param statusCode
230 | * @returns
231 | */
232 | redirect(url: string, statusCode: number = 302): Response {
233 | this._isImmediate = true
234 | let loc = url
235 | if(loc ==='back') loc = this._request?.headers.get('Referrer') || '/'
236 | return Response.redirect(encodeURI(loc), statusCode)
237 | }
238 |
239 | /**
240 | * Use the settings from available `Response` details, if there's one (as an outcome of handler processing and middleware execution)
241 | * @returns
242 | */
243 | private getResponseInit(): ResponseInit {
244 | if(this._response) {
245 | const { status, statusText, headers } = this._response
246 |
247 | const _headers: Record = {}
248 | headers.forEach((val, key) => {
249 | _headers[key] = val
250 | })
251 |
252 | return {
253 | headers: _headers,
254 | status,
255 | statusText
256 | }
257 | }
258 | else {
259 | return {}
260 | }
261 | }
262 |
263 | after(func: MiddlewareFunction) {
264 | this._middlewares.push(func)
265 | }
266 |
267 | get afterMiddlewares() {
268 | return this._middlewares
269 | }
270 |
271 | }
272 |
--------------------------------------------------------------------------------
/deno_dist/core/errors/config.ts:
--------------------------------------------------------------------------------
1 | import { badRequest, internalServerError, methodNotAllowed, notFound, unprocessableEntity } from "../response/created.ts";
2 | import { RespCreatorRequestInit } from "../response/creator.ts";
3 | import { HTTPStatusCodeMesssageKey } from '../constants/codes.ts'
4 |
5 | export const serverErrorFns: Partial Response>> = {
6 | 'InternalServerError': internalServerError,
7 | 'NotFound': notFound,
8 | 'BadRequest': badRequest,
9 | 'UnprocessableEntity': unprocessableEntity,
10 | 'MethodNotAllowed': methodNotAllowed
11 | }
12 |
--------------------------------------------------------------------------------
/deno_dist/core/errors/error.ts:
--------------------------------------------------------------------------------
1 | import type { Errorlike } from "bun DENOIFY: UNKNOWN NODE BUILTIN";
2 | import { pick } from '../utils/choose.ts'
3 | import { HTTPStatusCodeMesssageKey } from '../constants/codes.ts'
4 | import { serverErrorFns } from './config.ts'
5 |
6 | type ErrorData = { [key: string]: any };
7 |
8 | export class ZarfCustomError extends Error implements Errorlike {
9 | constructor(
10 | readonly message: string,
11 | readonly code: HTTPStatusCodeMesssageKey = 'InternalServerError',
12 | readonly data: ErrorData = {},
13 | ) {
14 | super();
15 | }
16 | }
17 |
18 | export class ZarfNotFoundError extends ZarfCustomError {
19 | constructor(originalUrl: string) {
20 | super(`Route '${originalUrl}' does not exist.`, 'NotFound');
21 | this.name = this.constructor.name;
22 | }
23 | }
24 |
25 | export class ZarfMethodNotAllowedError extends ZarfCustomError {
26 | constructor() {
27 | super(`Requested method is not allowed for the server.`, 'MethodNotAllowed');
28 | this.name = this.constructor.name;
29 | }
30 | }
31 |
32 | export class ZarfBadRequestError extends ZarfCustomError {
33 | constructor(errorData: ErrorData) {
34 | super('There were validation errors.', 'BadRequest', errorData);
35 | this.name = this.constructor.name;
36 | }
37 | }
38 |
39 | export class ZarfUnprocessableEntityError extends ZarfCustomError {
40 | constructor(errorData: ErrorData) {
41 | super('Unprocessable Entity', 'UnprocessableEntity', errorData);
42 | this.name = this.constructor.name;
43 | }
44 | }
45 |
46 | export const sendError = (error: Errorlike) => {
47 | const isErrorSafeForClient = error instanceof ZarfCustomError;
48 | const send = error.code ? serverErrorFns[error.code as HTTPStatusCodeMesssageKey] : serverErrorFns['InternalServerError']
49 | const { message, data } = isErrorSafeForClient
50 | ? pick(error, ['message', 'data'])
51 | : {
52 | message: 'Something went wrong!',
53 | data: {},
54 | };
55 | return send?.(JSON.stringify({ message, errors: data }, null, 0))
56 | }
57 |
--------------------------------------------------------------------------------
/deno_dist/core/errors/handler.ts:
--------------------------------------------------------------------------------
1 | import type { Errorlike } from 'bun DENOIFY: UNKNOWN NODE BUILTIN'
2 | import { AppContext } from '../context.ts';
3 | import { sendError } from './error.ts'
4 | /**
5 | * Special handlers
6 | * /
7 | /*
8 | * Default error handler
9 | * @param ctx
10 | * @param err
11 | * @returns
12 | */
13 | export function defaultErrorHandler = {}>(ctx: AppContext, err: Errorlike): Response {
14 | return sendError(err)!
15 | }
16 | /**
17 | * Not Found Error handler
18 | * @param ctx
19 | * @returns
20 | */
21 | export function notFoundHandler = {}>(ctx: AppContext): Response | Promise{
22 | return new Response(`No matching ${ctx.method.toUpperCase()} routes discovered for the path: ${ctx.path}`, {
23 | status: 404,
24 | });
25 | }
26 |
27 | /**
28 | * Not found verb error handler
29 | * @param ctx
30 | * @returns
31 | */
32 | export function notFoundVerbHandler = {}>(ctx: AppContext): Response | Promise {
33 | return new Response(`No implementations found for the verb: ${ctx.method.toUpperCase()}`, {
34 | status: 404,
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/deno_dist/core/errors/index.ts:
--------------------------------------------------------------------------------
1 | export * from './error.ts'
2 | export * from './handler.ts'
3 |
--------------------------------------------------------------------------------
/deno_dist/core/middleware.ts:
--------------------------------------------------------------------------------
1 | import type { AppContext } from "./context.ts";
2 |
3 | // `Middleware` configs
4 | export type MiddlewareType = 'before' | 'after' | 'error'
5 | export const NoOpMiddleware: MiddlewareFunction = (_, next) => next();
6 | export type MiddlewareMeta = {
7 | isFirst: boolean,
8 | isLast: boolean
9 | }
10 |
11 | // `Middleware` return types
12 | export type MiddlewareFuncResp = void | Response;
13 | export type MiddlewareNextFunc = () => Promise;
14 |
15 | // `Middleware` function types
16 | export type MiddlewareFunction = {}> = (
17 | context: AppContext,
18 | next: MiddlewareNextFunc,
19 | meta?: MiddlewareMeta
20 | ) => Promise;
21 | export type MiddlewareFunctionInitializer<
22 | T extends Record = {},
23 | S extends Record = {}> = (options?: T) => MiddlewareFunction
24 |
25 | /**
26 | * Execute a sequence of middlewares
27 | * @param context
28 | * @param middlewares
29 | * @returns
30 | */
31 | export async function exec = {}>(context: AppContext, middlewares: Array>) {
32 | let prevIndexAt: number = -1;
33 |
34 | async function runner(indexAt: number): Promise {
35 | if (indexAt <= prevIndexAt) {
36 | throw new Error(`next() called multiple times by middleware #${indexAt}`)
37 | }
38 |
39 | prevIndexAt = indexAt;
40 | const middleware = middlewares[indexAt];
41 | if (middleware) {
42 | const resp = await middleware(context, () => {
43 | return runner(indexAt + 1);
44 | }, {
45 | isFirst: indexAt === 0,
46 | isLast: indexAt === middlewares.length - 1,
47 | });
48 | if (resp) return resp;
49 | }
50 | }
51 |
52 | return runner(0)
53 | }
54 |
--------------------------------------------------------------------------------
/deno_dist/core/response.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from "https://deno.land/std@0.158.0/node/buffer.ts";
2 | import { getContentType } from './utils/mime.ts'
3 | import { HTTP_STATUS_CODES } from './constants/codes.ts'
4 |
5 | /**
6 | * Response helper: Sends the provided data as `json` to the client
7 | *
8 | * @param data `any` or provided type which could be JSON, string, etc.
9 | * @param args
10 | * @returns
11 | */
12 | export function json(data: T, args: ResponseInit = {}): Response {
13 | const headers = new Headers(args.headers || {})
14 | headers.set('Content-Type', getContentType('json') as string)
15 | const status = args.status || 200
16 | const statusText = args.statusText || HTTP_STATUS_CODES[status]
17 | if(typeof data === 'object' && data != null) {
18 | return new Response( JSON.stringify(data, null, 0), { ...args, status, statusText, headers } )
19 | } else if(typeof data === 'string') {
20 | return text(data, { ...args, status, statusText, headers} )
21 | } else {
22 | headers.delete('Content-Length')
23 | headers.delete('Transfer-Encoding')
24 | return text("", { ...args, status, statusText, headers} )
25 | }
26 | }
27 |
28 | /**
29 | * Response helper: Sends the provided data as `text` to the client
30 | * @param text
31 | * @param args
32 | * @returns
33 | */
34 | export function text(text: string, args: ResponseInit = {}): Response {
35 | const headers = new Headers(args.headers || {})
36 | headers?.set('Content-Type', getContentType('text') as string)
37 | const status = args.status || 200
38 | const statusText = args.statusText || HTTP_STATUS_CODES[status]
39 | return new Response(text.toString(), { ...args, status, statusText, headers });
40 | }
41 |
42 | export function html(text: string, args: ResponseInit = {}): Response {
43 | const headers = new Headers(args.headers || {})
44 | headers?.set('Content-Type', getContentType('html') as string)
45 | const status = args.status || 200
46 | const statusText = args.statusText || HTTP_STATUS_CODES[status]
47 | return new Response(text.toString(), { ...args, status, statusText, headers });
48 | }
49 |
50 | export function head(args: ResponseInit = {}): Response {
51 | const status = args.status || 204
52 | const statusText = args.statusText || HTTP_STATUS_CODES[status]
53 | return new Response('', {...args, status, statusText })
54 | }
55 |
56 | export async function send(body: any, args: ResponseInit = {}): Promise {
57 | let sendable = body
58 | const headers = new Headers(args.headers || {})
59 |
60 | // Do any required header tweaks
61 | if(Buffer.isBuffer(body)) {
62 | sendable = body
63 | } else if(typeof body === 'object' && body !== null) {
64 | // `json` updates its header, so no changes required
65 | } else if(typeof body === 'string') {
66 | headers.set('Content-Type', getContentType('text') as string)
67 | } else {
68 | headers.set('Content-Type', getContentType('html') as string)
69 | }
70 |
71 | // @TODO: populate Etag
72 |
73 | // strip irrelevant headers
74 | if (args?.status === 204 || args?.status === 304) {
75 | headers.delete('Content-Type')
76 | headers.delete('Content-Length')
77 | headers.delete('Transfer-Encoding')
78 | sendable = ''
79 | }
80 |
81 | // if(this._request?.method === 'HEAD') {
82 | // return head({
83 | // ...args,
84 | // headers
85 | // })
86 | // }
87 |
88 | if (typeof sendable === 'object') {
89 | if (sendable == null) {
90 | return new Response('', {
91 | ...args,
92 | headers
93 | })
94 | } else {
95 | return json(sendable, {
96 | ...args,
97 | headers
98 | })
99 | }
100 | }
101 | else if(Buffer.isBuffer(sendable)){
102 | if(!headers.get('Content-Type')) {
103 | headers.set('Content-Type', getContentType('octet-stream') as string)
104 | }
105 | return new Response(sendable, {
106 | ...args,
107 | headers
108 | })
109 | } else if(typeof sendable === 'string') {
110 | return text(sendable, {
111 | ...args,
112 | headers
113 | })
114 | } else if(sendable instanceof Blob) {
115 | if(sendable.type.includes('json')) {
116 | // @ts-ignore
117 | return json(await sendable.json(), {
118 | ...args,
119 | headers
120 | })
121 | } else if (sendable.type.includes('text')) {
122 | return text(await sendable.text(), {
123 | ...args,
124 | headers
125 | })
126 | } else {
127 |
128 | }
129 | } else {
130 |
131 | if(typeof sendable !== 'string') sendable = sendable.toString()
132 | return new Response(sendable, {
133 | ...args,
134 | headers
135 | })
136 | }
137 | return new Response(sendable, { ...args, headers })
138 | }
139 |
140 | export async function sendFile(path: string, args: ResponseInit = {}): Promise {
141 | const headers = new Headers(args.headers || {})
142 | const file = Bun.file(path)
143 | const fileName = path.substring(path.lastIndexOf('/') + 1, path.length)
144 | headers.set('Content-Disposition', `attachment; filename=${fileName}`)
145 | headers.set('Content-Transfer-Encoding', 'binary')
146 | headers.set('Content-Type', getContentType('octet-stream') as string)
147 | return new Response(new Blob([
148 | await file.arrayBuffer()
149 | ], {
150 | type: file.type
151 | }), {
152 | ...args,
153 | headers
154 | })
155 | }
156 |
--------------------------------------------------------------------------------
/deno_dist/core/response/created.ts:
--------------------------------------------------------------------------------
1 | import { createResp, createRedirect, createNotModified, createUnauthorized } from "./creator.ts";
2 | import { HTTP_STATUS_CODES } from '../constants/codes.ts'
3 |
4 | export const informContinue = createResp(100, HTTP_STATUS_CODES['100'])
5 | export const informSwitchingProtocols = createResp(101, HTTP_STATUS_CODES['101'])
6 |
7 | export const ok = createResp(200, HTTP_STATUS_CODES['200'])
8 | export const created = createResp(201, HTTP_STATUS_CODES['201'])
9 | export const accepted = createResp(202, HTTP_STATUS_CODES['202'])
10 | export const nonAuthorititiveInfo = createResp(203, HTTP_STATUS_CODES['203'])
11 | export const noContent = createResp(204, HTTP_STATUS_CODES['204'])
12 | export const resetContent = createResp(205, HTTP_STATUS_CODES['205'])
13 | export const partialContent = createResp(206, HTTP_STATUS_CODES['206'])
14 |
15 |
16 | export const movedPermanently = createRedirect(301, HTTP_STATUS_CODES['301'])
17 | export const found = createRedirect(302, HTTP_STATUS_CODES['302'])
18 | export const seeOther = createRedirect(303, HTTP_STATUS_CODES['303'])
19 | export const notModified = createNotModified(304, HTTP_STATUS_CODES['304'])
20 | export const tempRedirect = createRedirect(307, HTTP_STATUS_CODES['307'])
21 | export const permRedirect = createRedirect(308, HTTP_STATUS_CODES['308'])
22 |
23 | export const badRequest = createResp(400, HTTP_STATUS_CODES['400'])
24 | export const unauthorized = createUnauthorized(401, HTTP_STATUS_CODES['401'])
25 | export const forbidden = createResp(403, HTTP_STATUS_CODES['403'])
26 | export const notFound = createResp(404, HTTP_STATUS_CODES['404'])
27 | export const methodNotAllowed = createResp(405, HTTP_STATUS_CODES['405'])
28 | export const notAcceptable = createResp(406, HTTP_STATUS_CODES['406'])
29 | export const proxyAuthRequired = createResp(407, HTTP_STATUS_CODES['407'])
30 | export const requestTimeout = createResp(408, HTTP_STATUS_CODES['408'])
31 | export const conflict = createResp(409, HTTP_STATUS_CODES['409'])
32 | export const gone = createResp(410, HTTP_STATUS_CODES['410'])
33 | export const unprocessableEntity = createResp(422, HTTP_STATUS_CODES['422'])
34 |
35 | export const internalServerError = createResp(500, HTTP_STATUS_CODES['500'])
36 | export const notImplemented = createResp(501, HTTP_STATUS_CODES['501'])
37 | export const badGateway = createResp(502, HTTP_STATUS_CODES['502'])
38 | export const serviceUnavailable = createResp(503, HTTP_STATUS_CODES['503'])
39 | export const gatewayTimeout = createResp(504, HTTP_STATUS_CODES['504'])
40 |
--------------------------------------------------------------------------------
/deno_dist/core/response/creator.ts:
--------------------------------------------------------------------------------
1 | export type RespCreatorRequestInit= Omit
2 |
3 | /**
4 | * Create a Response
5 | * @param status number
6 | * @param statusText string
7 | * @returns Response
8 | */
9 | export const createResp = (status: number, statusText: string) => (body: BodyInit | null = null, init: RespCreatorRequestInit = {}) => new Response(body, {
10 | ...init,
11 | status,
12 | statusText,
13 | })
14 |
15 | /**
16 | * Create a redirect response
17 | * @param status number
18 | * @param statusText string
19 | * @returns Response
20 | */
21 | export const createRedirect = (status: number, statusText: string) => (location: string | URL, init: RespCreatorRequestInit = {}): Response => new Response(null, {
22 | ...init,
23 | status,
24 | statusText,
25 | headers: [
26 | // @ts-ignore
27 | ...init?.headers ? Array.isArray(init.headers) ? init.headers : new Headers(init.headers) : [],
28 | ['Location', location.toString()]
29 | ],
30 | });
31 |
32 | /**
33 | * Create Unauthorized Response
34 | * @param status number
35 | * @param statusText string
36 | * @returns Response
37 | */
38 | export const createUnauthorized = (status: number, statusText: string) => (realm = '', init: RespCreatorRequestInit = {}): Response => new Response(null, {
39 | ...init,
40 | status,
41 | statusText,
42 | headers: [
43 | ...init?.headers ? Array.isArray(init.headers) ? init.headers : new Headers(init.headers) : [],
44 | ['WWW-Authenticate', `Basic realm="${realm}", charset="UTF-8"`],
45 | ],
46 | });
47 |
48 |
49 |
50 | type CreateNotModifiedOptions = {
51 | ifNoneMatch: string,
52 | ifModifiedSince: Date
53 | }
54 |
55 | /**
56 | * Create `NotModified` Response
57 | * @param status number
58 | * @param statusText string
59 | * @returns Response
60 | */
61 | export const createNotModified = (status: number, statusText: string) => (options: CreateNotModifiedOptions, init: RespCreatorRequestInit = {}): Response => new Response(null, {
62 | ...init,
63 | status,
64 | statusText,
65 | headers: [
66 | ...init?.headers ? Array.isArray(init.headers) ? init.headers : new Headers(init.headers) : [],
67 | ['If-None-Match', options.ifNoneMatch],
68 | ['If-Modified-Since', options.ifModifiedSince],
69 | ],
70 | })
71 |
--------------------------------------------------------------------------------
/deno_dist/core/server.ts:
--------------------------------------------------------------------------------
1 | import type { Errorlike, Server } from 'bun DENOIFY: UNKNOWN NODE BUILTIN'
2 | import type { ZarfConfig, ZarfOptions, RouteStack, RouteHandler, Route, RouteMethod, RouteParams } from './types.ts'
3 |
4 | import { AppContext } from './context.ts'
5 | import { MiddlewareFunction, exec, MiddlewareType } from './middleware.ts'
6 |
7 | import { isObject } from './utils/is.ts'
8 | import { getMountPath } from './utils/app.ts'
9 |
10 | import { ROUTE_OPTION_DEFAULT } from './constants/index.ts'
11 | import {
12 | ZarfCustomError, ZarfMethodNotAllowedError, ZarfUnprocessableEntityError,
13 | sendError,
14 | defaultErrorHandler, notFoundHandler, notFoundVerbHandler
15 | } from './errors/index.ts'
16 |
17 | export class Zarf = {}, M extends Record = {}> {
18 | /**
19 | * App Config
20 | */
21 | private readonly config: ZarfConfig
22 | private server?: Server
23 | /**
24 | * App stores
25 | */
26 | private appList: Record> = {}
27 | /**
28 | * Route stores
29 | */
30 | private routes: Partial>>> = {}
31 |
32 | /**
33 | * Middleware stores
34 | */
35 | private middlewares: Record>> = {
36 | 'before': [],
37 | 'after': [],
38 | 'error': []
39 | }
40 |
41 | private pathMiddlewares: Record>>> = {}
42 | private pathWithMiddlewares: Array = []
43 |
44 | /**
45 | * App Constructor
46 | * @param param0
47 | */
48 | constructor({
49 | appName = 'ZarfRoot',
50 | serverHeader = `Bun-Tea`,
51 | strictRouting = false,
52 | getOnly = false,
53 | errorHandler = defaultErrorHandler
54 | }: ZarfConfig = {}){
55 | this.config = {
56 | appName,
57 | serverHeader,
58 | strictRouting,
59 | getOnly,
60 | errorHandler
61 | }
62 | this.handle = this.handle.bind(this)
63 | }
64 |
65 | // wrapper around the application's error handler method.
66 | // It maps a set of errors to app errors before calling the application's error handler method.
67 | // @TODO: Intercept all the error types
68 | serverErrorHandler = (error: Errorlike): Response | Promise | Promise | undefined => {
69 | // check for errors like
70 | // - Header fields too large
71 | // - Request/Processing Timeout
72 | // - Request Body/Entity too large
73 | // - Method not allowed (for GET only requests)
74 | if(error instanceof ZarfMethodNotAllowedError) {
75 | // do something...
76 | return sendError(error)
77 | }
78 | // - Or, just Bad request
79 | else if(error instanceof ZarfUnprocessableEntityError) {
80 | // do something
81 | return sendError(error)
82 | } else {
83 | return sendError(error)
84 | }
85 | }
86 |
87 | /**
88 | * Central error handler
89 | * @param ctx
90 | * @param error
91 | * @returns
92 | */
93 | errorHandler = (ctx: AppContext, error: Errorlike): Response | Promise | Promise | undefined => {
94 | const path = ctx.request?.url || ''
95 | const basePath = path.substring(0, path.lastIndexOf('/'))
96 | if(basePath && this.appList[basePath]) {
97 | return this.appList[basePath].errorHandler(ctx, error)
98 | } else {
99 | if(error instanceof ZarfCustomError) {
100 | return this.serverErrorHandler(error)
101 | } else {
102 | return this.config.errorHandler?.(ctx, error)
103 | }
104 | }
105 | }
106 |
107 | /// ROUTE METHODS ///
108 |
109 | /**
110 | * Register a new route
111 | * @param method the `verb` to listen to
112 | * @param url the relative path to listen to
113 | * @param args all the other arguments like
114 | * @returns
115 | */
116 | private register(method: RouteMethod, url: string, ...args: any): Error | undefined {
117 |
118 | let id = url.toString()
119 |
120 | // Prepare route options
121 | let options: Record> = {}
122 | if(isObject(args.at(-1))) {
123 | options = args.pop()
124 | }
125 | options[ROUTE_OPTION_DEFAULT] = options[ROUTE_OPTION_DEFAULT] || {}
126 |
127 | // Identify Handler and and/or middlewares
128 | let handler: RouteHandler<{}, S>;
129 | let middlewares: Array> = []
130 | if(args.length === 1) {
131 | handler = args.pop()
132 | } else if(args.length === 2) {
133 | handler = args.pop()
134 | middlewares = args.pop()
135 | } else {
136 | return new Error('too many arguments provided')
137 | }
138 |
139 | // Prepare RegExp for path matching
140 | const parts = url.split("/").filter(part => part !== "")
141 | const vars: Array = []
142 |
143 | // @TODO: Optimize RegExp creation approach
144 | const regExpParts = parts.map(part => {
145 | if(part[0] === ':') {
146 | if(part[part.length - 1] === '?') {
147 | part = part.replace("?", '')
148 | vars.push(part.slice(1))
149 | return `([a-zA-Z0-9_-]*)`
150 | } else if(part.includes('.')) {
151 | const subParts = part.split('.')
152 | if(subParts[1][0] === ':') {
153 | vars.push(`${subParts[0].slice(1)}_${subParts[1].slice(1)}`)
154 | return `([a-zA-Z0-9_-]+.[a-zA-Z0-9_-]+)`
155 | } else {
156 | vars.push(subParts[0])
157 | return `([a-zA-Z0-9_-]+.${subParts[1].slice(1)})`
158 | }
159 | } else if(part.includes('-')) {
160 | const subParts = part.split('-')
161 | if(subParts[1][0] === ':') {
162 | vars.push(`${subParts[0].slice(1)}_${subParts[1].slice(1)}`)
163 | return `([a-zA-Z0-9_-]+-[a-zA-Z0-9_-]+)`
164 | } else {
165 | vars.push(subParts[0].slice(1))
166 | return `([a-zA-Z0-9_-]+-${subParts[1]})`
167 | }
168 | } else {
169 | vars.push(part.slice(1))
170 | return `([a-zA-Z0-9_-]+)`
171 | }
172 | } else if (part[0] === '*') {
173 | vars.push(part.slice(1))
174 | return `(.*)`
175 | } else if (part.includes('.')) {
176 | const subParts = part.split('.')
177 | vars.push(subParts[1].slice(1))
178 | return `(${subParts[0]}.[a-zA-Z0-9_-]+)`
179 | } else if(part.includes('::')) {
180 | const subParts = part.split('::')
181 | vars.push(subParts[0])
182 | return `(${subParts[0]}:[a-zA-Z0-9_-]+)`
183 | } else {
184 | return part
185 | }
186 | })
187 | const regExp = regExpParts.join("/")
188 |
189 |
190 | if(!this.routes[method]) this.routes[method] = []
191 | this.routes[method]!.push({
192 | id,
193 | matcher: new RegExp(`^/${regExp}$`),
194 | vars,
195 | handler,
196 | options,
197 | middlewares
198 | })
199 | }
200 |
201 | /**
202 | * Resolve, or identify a matching route
203 | * @param method
204 | * @param path
205 | * @returns
206 | */
207 | private resolve(method: RouteMethod, path: string) {
208 | const route = this.routes[method]!.find(route => route.matcher.test(path))
209 | if(!route) return undefined
210 |
211 | const matches = route.matcher.exec(path)
212 |
213 | const options = route.options
214 | const params: {[key: string]: string } = options[ROUTE_OPTION_DEFAULT] || {}
215 |
216 | if(matches) {
217 | route.vars.forEach((val, index) => {
218 | const match = matches[index + 1]
219 | if(match){
220 | if(val.includes('_')) {
221 | const matchParts = match.includes('-') ? match.split('-') : match.includes('.') ? match.split('.') : [ match ]
222 | const valParts = val.split('_')
223 | if(valParts.length === matchParts.length) {
224 | valParts.forEach((val, index) => {
225 | params[val] = matchParts[index]
226 | })
227 | }
228 | } else {
229 | params[val] = decodeURIComponent(
230 | match.startsWith(`${val}:`) ? match.replace(`${val}:`, '') :
231 | match.includes('.') ? match.substring(match.lastIndexOf('.') + 1, match.length) : match
232 | )
233 | }
234 | }
235 | })
236 | }
237 |
238 | return {
239 | ...route,
240 | params
241 | }
242 | }
243 | public async fetch(req: RequestInfo, requestInit?: RequestInit): Promise {
244 | const request = req instanceof Request ? req : new Request(req, requestInit)
245 | return await this.handle(request)
246 | }
247 |
248 | /**
249 | * Handle a route
250 | * @param req
251 | * @returns
252 | */
253 | public async handle(req: Request, adapterCtx?: any): Promise {
254 | const ctx = new AppContext(req, this.config)
255 | const { path , method } = ctx
256 | if(this.config.getOnly && method !== 'get') {
257 | return this.errorHandler(ctx, new ZarfMethodNotAllowedError()) as Response
258 | }
259 | /**
260 | * Global "App-level" middlewares
261 | */
262 | try {
263 | if(this.middlewares?.before && this.middlewares?.before.length) {
264 | const _response = await exec(ctx, this.middlewares.before)
265 | if(ctx.isImmediate || _response) return _response!
266 | }
267 | } catch (error: unknown) {
268 | if (error instanceof Error) {
269 | return this.errorHandler(ctx, error)
270 | }
271 | }
272 | /**
273 | * Group-level middlewars. Could halt execution, or respond
274 | */
275 | try {
276 | const basePath = path.substring(0, path.lastIndexOf('/'))
277 | if(this.pathMiddlewares[path] && this.pathMiddlewares[path].before.length) {
278 | const _response = await exec(ctx, this.pathMiddlewares[path].before)
279 | if(ctx.isImmediate || _response) return _response!
280 | } else if(basePath && this.pathMiddlewares[basePath] && this.pathMiddlewares[basePath].before.length) {
281 | // middlewares discovered for a base path on a route cannot halt execution
282 | await exec(ctx, this.pathMiddlewares[basePath].before)
283 | }
284 | } catch (error: unknown) {
285 | if (error instanceof Error) {
286 | return this.errorHandler(ctx, error)
287 | }
288 | }
289 |
290 | if(!this.routes[method]) return notFoundVerbHandler(ctx)
291 |
292 | /**
293 | * Identify if there's a handler
294 | */
295 | const route = this.resolve(method, path)
296 | if(!route) return notFoundHandler(ctx)
297 | // use middlewares
298 | if(route.middlewares && route.middlewares.length) {
299 | try {
300 | const resp = await exec(ctx, route.middlewares)
301 | if(resp || ctx.isImmediate) return resp as Response
302 | } catch (error: unknown) {
303 | if (error instanceof Error) {
304 | return this.errorHandler(ctx, error)
305 | }
306 | }
307 | }
308 | if(route.handler) {
309 | try {
310 | const response = await route.handler(ctx as AppContext, {...route.params, ...{}})
311 | if(ctx.isImmediate) {
312 | return response
313 | } else if(this.middlewares.after.length) {
314 | ctx.response = response
315 | const _response = await exec(ctx, this.middlewares.after)
316 | if(_response && !ctx.afterMiddlewares.length) return _response
317 | }
318 | if(ctx.afterMiddlewares.length) {
319 | ctx.response = response
320 | const _response = await exec(ctx, ctx.afterMiddlewares)
321 | return _response ? _response : ctx.response
322 | }
323 | return response
324 | } catch (error: unknown) {
325 | if (error instanceof Error) {
326 | return this.errorHandler(ctx, error)
327 | }
328 | }
329 | }
330 | }
331 |
332 | /**
333 | *
334 | * @param path
335 | * @param handler
336 | */
337 | get(path: Path, handler: RouteHandler, S>): void;
338 | get(path: Path, middlewares: Array>>, handler: RouteHandler, S>): void;
339 | get(path: Path, ...args: Array, S> | Array>>>) {
340 | this.register('get', path, ...args);
341 | return this
342 | }
343 |
344 | /**
345 | *
346 | * @param path
347 | * @param handler
348 | */
349 | post(path: Path, handler: RouteHandler, S>): void;
350 | post(path: Path, middlewares: Array>>, handler: RouteHandler, S>): void;
351 | post(path: Path, ...args: Array, S> | Array>>>) {
352 | this.register('post', path, ...args);
353 | return this
354 | }
355 |
356 | /**
357 | *
358 | * @param path
359 | * @param handler
360 | */
361 | put(path: Path, handler: RouteHandler, S>): void;
362 | put(path: Path, middlewares: Array>>, handler: RouteHandler, S>): void;
363 | put(path: Path, ...args: Array, S> | Array>>>) {
364 | this.register('put', path, ...args)
365 | return this
366 | }
367 |
368 | /**
369 | *
370 | * @param path
371 | * @param handler
372 | */
373 | patch(path: Path, handler: RouteHandler, S>): void;
374 | patch(path: Path, middlewares: Array>>, handler: RouteHandler, S>): void;
375 | patch(path: Path, ...args: Array, S> | Array>>>) {
376 | this.register('patch', path, ...args)
377 | return this
378 | }
379 |
380 | /**
381 | *
382 | * @param path
383 | * @param handler
384 | */
385 | del(path: Path, handler: RouteHandler, S>): void;
386 | del(path: Path, middlewares: Array>>, handler: RouteHandler, S>): void;
387 | del(path: Path, ...args: Array, S> | Array>>>) {
388 | this.register('delete', path, ...args)
389 | return this
390 | }
391 |
392 | /**
393 | *
394 | * @param path
395 | * @param handler
396 | */
397 | opt(path: Path, handler: RouteHandler, S>): void;
398 | opt(path: Path, middlewares: Array>>, handler: RouteHandler, S>): void;
399 | opt(path: Path, ...args: Array, S> | Array>>>) {
400 | this.register('options', path, ...args)
401 | return this
402 | }
403 |
404 | // ADVANCED: ROUTE ORCHESTRATION
405 | /**
406 | * Mount a sub-app routes, on the parent app routes
407 | *
408 | * This method just copies over all the routes, from the mounted app to the root app, and re-registers all the handlers and
409 | * middlewares on the newly formed path
410 | * @param prefix
411 | * @param app
412 | */
413 | mount = {}>(prefix: string, app: Zarf) {
414 | for(const routeType in app.routes as RouteStack) {
415 | for(const route of app.routes[routeType as RouteMethod] as Array>) {
416 | this.register(
417 | routeType as RouteMethod,
418 | getMountPath(prefix, route.id),
419 | route.middlewares, route.handler
420 | )
421 | }
422 | }
423 | this.appList[getMountPath(prefix, '')] = app
424 | }
425 |
426 | /**
427 | * Create and return Route groups
428 | * @param prefix
429 | * @param args
430 | * @returns
431 | */
432 | group(prefix: string = '/', ...args: Array>) {
433 | return new ZarfRouteGroup(this.register.bind(this), this.useOnPath.bind(this), prefix, ...args)
434 | }
435 |
436 | /// MIDDLEWARES ///
437 |
438 | /**
439 | * Register app middlewares
440 | * @param middleware
441 | * @param type
442 | * @returns
443 | */
444 | use (middleware: MiddlewareFunction, type: MiddlewareType = 'before') {
445 | this.middlewares[type].push(middleware as unknown as MiddlewareFunction)
446 | return this
447 | }
448 |
449 | private useOnPath(path: string, ...args: Array>) {
450 | if(!this.pathMiddlewares[path]) this.pathMiddlewares[path] = { before: [], after: [], error: [] }
451 | this.pathMiddlewares[path].before.push(...args)
452 | this.pathWithMiddlewares.push(path)
453 | }
454 |
455 | /// SERVER METHODS ///
456 |
457 | shutdown() {
458 | // do any pre-shutdown process
459 | if(!this.server) {
460 | return console.error(`Can't shutdown as server isn't up currently`)
461 | }
462 | if(this.server?.pendingRequests) {
463 | return console.error(`Can't shutdown as there are pending requests. You might wanna wait or forcibly shut the server instance?`)
464 | }
465 | return this.server?.stop()
466 | }
467 |
468 | // async fetch(req: Request, env?: any, ctx?: any) {
469 | // return await this.handle(req, {
470 | // waitUntil: ctx?.waitUntil?.bind(ctx) ?? ((_f: any) => { }),
471 | // env,
472 | // ctx,
473 | // })
474 | // }
475 |
476 | // async request(input: RequestInfo, requestInit?: RequestInit) {
477 | // const req = input instanceof Request ? input : new Request(input, requestInit)
478 | // return await this.fetch(req)
479 | // }
480 |
481 | listen({
482 | port = 3333,
483 | development = false,
484 | hostname = '0.0.0.0'
485 | }: ZarfOptions, startedCb?: (server: Server) => void) {
486 | const self = this
487 | if (!Bun) throw new Error('Bun-Tea requires Bun to run')
488 |
489 | // do things, before initializing the server
490 | this.server = Bun.serve({
491 | port,
492 | hostname,
493 | development: process.env.NODE_ENV !== "production" || development,
494 | // @ts-ignore
495 | fetch: this.handle.bind(this),
496 | error(err: Errorlike) {
497 | return self.serverErrorHandler(err)
498 | }
499 | })
500 |
501 | if(startedCb && typeof startedCb === 'function') {
502 | startedCb(this.server!)
503 | }
504 |
505 | return this.server
506 | }
507 | }
508 |
509 | class ZarfRouteGroup> {
510 | private prefix = ''
511 | private register: (method: RouteMethod, url: string, ...args: any) => void
512 | private registerMw: (path: string, ...args: Array>) => void
513 |
514 | constructor(
515 | register: (method: RouteMethod, url: string, ...args: any) => void,
516 | registerMw: (path: string, ...args: Array>) => void,
517 | prefix: string = '/',
518 | ...args: Array>
519 |
520 | ) {
521 | this.prefix = prefix === '' ? '/' : prefix
522 | this.register = register
523 | this.registerMw = registerMw
524 |
525 | if(args.length) registerMw(this.prefix, ...args)
526 | }
527 |
528 | get(path: Path, handler: RouteHandler, S>) {
529 | this.register('get', getMountPath(this.prefix, path), handler);
530 | return this as Omit
531 | }
532 | post(path: string, handler: RouteHandler, S>) {
533 | this.register('post', getMountPath(this.prefix, path), handler);
534 | return this as Omit
535 | }
536 | put(path: string, handler: RouteHandler, S>) {
537 | this.register('put', getMountPath(this.prefix, path), handler);
538 | return this as Omit
539 | }
540 | patch(path: string, handler: RouteHandler, S>) {
541 | this.register('patch', getMountPath(this.prefix, path), handler);
542 | return this as Omit
543 | }
544 | del(path: string, handler: RouteHandler, S>) {
545 | this.register('delete', getMountPath(this.prefix, path), handler);
546 | return this as Omit
547 | }
548 | all(path: string, handler: RouteHandler, S>) {
549 | ['get', 'post', 'put', 'patch', 'delete'].forEach(verb => {
550 | this.register(verb as RouteMethod, getMountPath(this.prefix, path), handler);
551 | })
552 | return this as Omit
553 | }
554 | group(path: string = '/', ...args: Array>) {
555 | return new ZarfRouteGroup(this.register, this.registerMw, getMountPath(this.prefix, path), ...args)
556 | }
557 | }
558 |
--------------------------------------------------------------------------------
/deno_dist/core/types.ts:
--------------------------------------------------------------------------------
1 | import type { Errorlike } from "bun DENOIFY: UNKNOWN NODE BUILTIN";
2 | import { AppContext as PrivateAppContext } from './context.ts'
3 | import { MiddlewareFunction } from './middleware.ts';
4 | import type { Replace } from './utils/types.ts'
5 |
6 | // Global Type aliases
7 | export type RouteMethod = "get" | "post" | "put" | "patch" | "delete" | "head" | "options"
8 | export type HeaderVaryContent = 'Origin' | 'User-Agent' | 'Accept-Encoding' | 'Accept' | 'Accept-Language'
9 | export type HeaderTypeContent = 'text' | 'json' | 'html'
10 |
11 | // Context interfaces/types
12 | // The context that could be shared to `RouteHandler`
13 | export type AppContext = {}> = Omit,
14 | 'after' |
15 | 'afterMiddlewares'
16 | >
17 | // Context-internal interfaces/types
18 | export interface ContextMeta {
19 | startTime: number
20 | }
21 |
22 | // `Zarf` Application config
23 | export interface ZarfConfig = {}> {
24 | appName?: string
25 | serverHeader?: string
26 | strictRouting?: boolean
27 | getOnly?: boolean
28 | errorHandler?: (ctx: PrivateAppContext, error: Errorlike) => Response | Promise | Promise | undefined
29 | }
30 |
31 | // `Zarf` lister options
32 | export interface ZarfOptions {
33 | port?: number,
34 | development?: boolean,
35 | hostname?: string
36 | }
37 |
38 | // `Route` types
39 | export interface Route = {}> {
40 | id: string
41 | matcher: RegExp
42 | handler: RouteHandler<{}, S>
43 | vars: Array
44 | options: any,
45 | middlewares?: Array>
46 | }
47 | export type RouteStack = {}> = Record>>
48 | export interface ResolvedRoute {
49 | handler: RouteHandler
50 | params: Record
51 | }
52 | export interface RouteProps {
53 | context: PrivateAppContext;
54 | request: Request;
55 | params: Record;
56 | }
57 |
58 | type RouteParamNames =
59 | string extends Route
60 | ? string
61 | : Route extends `${string}:${infer Param}-:${infer Rest}`
62 | ? (RouteParamNames<`/:${Param}/:${Rest}`>)
63 | : Route extends `${string}:${infer Param}.:${infer Rest}`
64 | ? (RouteParamNames<`/:${Param}/:${Rest}`>)
65 | : Route extends `${string}:${infer Param}/${infer Rest}`
66 | ? (Replace | RouteParamNames)
67 | : Route extends `${string}*${infer Param}/${infer Rest}`
68 | ? (Replace | RouteParamNames)
69 | : (
70 | Route extends `${string}:${infer LastParam}?` ?
71 | Replace, '?', ''> :
72 | Route extends `${string}:${infer LastParam}` ?
73 | Replace :
74 | Route extends `${string}*${infer LastParam}` ?
75 | LastParam :
76 | Route extends `${string}:${infer LastParam}?` ?
77 | Replace :
78 | never
79 | );
80 |
81 |
82 | export type RouteParams = {
83 | [key in RouteParamNames]: string
84 | }
85 |
86 | // `Route Handler` types
87 |
88 | type Path = string;
89 | export type RegisterRoute = {}> = ( method: RouteMethod, path: Path, handler: RouteHandler ) => void;
90 | export type RouteHandler = {}, S extends Record = {}> = (context: AppContext, params: T) => Response | Promise
91 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/app.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export function getMountPath(prefix: string, path: string): string {
4 | if(prefix.length === 0 || prefix === '/') {
5 | return path[0] === '/' ? path : `/${path}`
6 | } else {
7 | return `${prefix}${path}`
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/choose.ts:
--------------------------------------------------------------------------------
1 | type O = { [key: string]: any };
2 |
3 | export const omit = (obj: O, keys: string[]) => {
4 | return Object.keys(obj).reduce((target: O, key) => {
5 | if (!keys.includes(key)) {
6 | target[key] = obj[key];
7 | }
8 | return target;
9 | }, {});
10 | };
11 |
12 | export const pick = (obj: O, keys: string[]) => {
13 | return Object.keys(obj).reduce((target: O, key) => {
14 | if (keys.includes(key)) {
15 | target[key] = obj[key];
16 | }
17 | return target;
18 | }, {});
19 | };
20 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/console/colors.ts:
--------------------------------------------------------------------------------
1 | // @TODO: Read through the env vars
2 | const enabled = true
3 |
4 | interface Code {
5 | open: string;
6 | close: string;
7 | regexp: RegExp;
8 | }
9 |
10 | const code = (open: number[], close: number): Code => {
11 | return {
12 | open: `\x1b[${open.join(";")}m`,
13 | close: `\x1b[${close}m`,
14 | regexp: new RegExp(`\\x1b\\[${close}m`, "g"),
15 | };
16 | }
17 | const style = (str: string, code: Code) => enabled ? `${code.open}${str.replace(code.regexp, code.open)}${code.close}` : str;
18 |
19 | /** Color presets for `console` */
20 | export const bold = (str: string) => style(str, code([1], 22))
21 | export const dim = (str: string) => style(str, code([2], 22))
22 | export const italic = (str: string) => style(str, code([3], 23))
23 | export const underline = (str: string) => style(str, code([4], 24))
24 | export const inverse = (str: string) => style(str, code([7], 27))
25 | export const hidden = (str: string) => style(str, code([8], 28))
26 | export const strikethrough = (str: string) => style(str, code([9], 29))
27 | export const black = (str: string) => style(str, code([30], 39))
28 | export const red = (str: string) => style(str, code([31], 39))
29 | export const green = (str: string) => style(str, code([32], 39))
30 | export const yellow = (str: string) => style(str, code([33], 39))
31 | export const blue = (str: string) => style(str, code([34], 39))
32 | export const magenta = (str: string) => style(str, code([35], 39))
33 | export const cyan = (str: string) => style(str, code([36], 39))
34 | export const white = (str: string) => style(str, code([37], 39))
35 | export const gray = (str: string) => brightBlack(str)
36 |
37 | export const brightBlack = (str: string) => style(str, code([90], 39))
38 | export const brightRed = (str: string) => style(str, code([91], 39))
39 | export const brightGreen = (str: string) => style(str, code([92], 39))
40 | export const brightYellow = (str: string) => style(str, code([93], 39))
41 | export const brightBlue = (str: string) => style(str, code([94], 39))
42 | export const brightMagenta = (str: string) => style(str, code([95], 39))
43 | export const brightCyan = (str: string) => style(str, code([96], 39))
44 | export const brightWhite = (str: string) => style(str, code([97], 39))
45 |
46 | export const bgBlack = (str: string) => style(str, code([40], 49))
47 | export const bgRed = (str: string) => style(str, code([41], 49))
48 | export const bgGreen = (str: string) => style(str, code([42], 49))
49 | export const bgYellow = (str: string) => style(str, code([43], 49))
50 | export const bgBlue = (str: string) => style(str, code([44], 49))
51 | export const bgMagenta = (str: string) => style(str, code([45], 49))
52 | export const bgCyan = (str: string) => style(str, code([46], 49))
53 | export const bgWhite = (str: string) => style(str, code([47], 49))
54 | export const bgBrightBlack = (str: string) => style(str, code([100], 49))
55 | export const bgBrightRed = (str: string) => style(str, code([101], 49))
56 | export const bgBrightGreen = (str: string) => style(str, code([102], 49))
57 | export const bgBrightYellow = (str: string) => style(str, code([103], 49))
58 | export const bgBrightBlue = (str: string) => style(str, code([104], 49))
59 | export const bgBrightMagenta = (str: string) => style(str, code([105], 49))
60 | export const bgBrightCyan = (str: string) => style(str, code([106], 49))
61 | export const bgBrightWhite = (str: string) => style(str, code([107], 49))
62 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/is.ts:
--------------------------------------------------------------------------------
1 | export const isPromise = (promise: any) => typeof promise?.then === 'function'
2 | export const isObject = (item: any) => typeof item === "object" && !Array.isArray(item) && item !== null
3 | export const isAsyncFn = (fn: Function) => isFn(fn) && fn.constructor.name === 'AsyncFunction'
4 | export const isFn = (fn: Function) => typeof fn === 'function'
5 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/mime.ts:
--------------------------------------------------------------------------------
1 | import { extname } from 'https://deno.land/std@0.158.0/node/path.ts';
2 | import { MimeType, MIME_TYPES_CONFIG, EXT_MIME_TYPES } from '../constants/mime/index.ts'
3 |
4 | export function getContentType(str: string) {
5 | if(str && typeof str === 'string') {
6 | const isMimeType = str.indexOf('/') !== -1
7 | if(isMimeType) {
8 | const mimeConfig = MIME_TYPES_CONFIG[str as MimeType]
9 | return mimeConfig?.charset ? `${str}; charset=${mimeConfig.charset.toLowerCase()}` : false
10 | } else {
11 | const mimeType = getMimeType(str)
12 | if(mimeType && typeof mimeType !== 'boolean') {
13 | const mimeConfig = MIME_TYPES_CONFIG[mimeType as MimeType]
14 | return mimeConfig && mimeConfig.charset ?
15 | `${mimeType}; charset=${mimeConfig.charset.toLowerCase()}` :
16 | mimeType.startsWith('text/') ?
17 | `${mimeType}; charset=utf-8` :
18 | false
19 | }else{
20 | return false
21 | }
22 | }
23 | }
24 | return false
25 | }
26 |
27 | export function getMimeExt(str: MimeType): string | boolean {
28 | if(str && typeof str === 'string') {
29 | const config = MIME_TYPES_CONFIG[str as MimeType]
30 | return config?.ext?.length ? config.ext[0] : false
31 | }
32 | return false
33 | }
34 |
35 | export function getMimeType(path: string): string | boolean {
36 | return path && typeof path === 'string' && path.trim() !== '' ? EXT_MIME_TYPES[extname('x.' + path.trim()).toLowerCase().substring(1)] || false : false
37 | }
38 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/parsers/base64.ts:
--------------------------------------------------------------------------------
1 | import { asBuffer } from './buffer.ts'
2 |
3 | export const encodeBase64 = (buffer: Uint8Array): Uint8Array => {
4 | return Buffer.from(asBuffer(buffer).toString('base64'), 'utf8');
5 | }
6 |
7 | export const decodeBase64 = (buffer: Uint8Array): Uint8Array => {
8 | return Buffer.from(asBuffer(buffer).toString('utf8'), 'base64');
9 | }
10 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/parsers/buffer.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from "https://deno.land/std@0.158.0/node/buffer.ts";
2 | /**
3 | * Converts `ArrayBuffer` to string (currently the default one)
4 | * @param buffer ArrayBuffer
5 | * @returns
6 | */
7 | export function asString(buffer: ArrayBuffer): string {
8 | return String.fromCharCode.apply(null, Array.from(new Uint16Array(buffer)));
9 | }
10 |
11 | /**
12 | * Consistent `Buffer`
13 | * @param input
14 | * @returns
15 | */
16 | export function asBuffer(input: Buffer | Uint8Array | ArrayBuffer): Buffer {
17 | if (Buffer.isBuffer(input)) {
18 | return input;
19 | } else if (input instanceof ArrayBuffer) {
20 | return Buffer.from(input);
21 | } else {
22 | // Offset & length allow us to support all sorts of buffer views:
23 | return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
24 | }
25 | };
26 |
27 | /**
28 | * Converts `ArrayBuffer` to string (currently not used)
29 | * @param buffer ArrayBuffer
30 | * @returns
31 | */
32 | function arrayBufferToString(buffer: Buffer){
33 |
34 | var bufView = new Uint16Array(buffer);
35 | var length = bufView.length;
36 | var result = '';
37 | var addition = Math.pow(2,16)-1;
38 |
39 | for(var i = 0;i length){
42 | addition = length - i;
43 | }
44 | result += String.fromCharCode.apply(null, Array.from(bufView.subarray(i,i+addition)));
45 | }
46 |
47 | return result;
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/parsers/form-data.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from "https://deno.land/std@0.158.0/node/buffer.ts";
2 | import { asString } from './buffer.ts'
3 |
4 | export async function getFormDataFromRequest(request: Request) {
5 | const boundary = getBoundary(request?.headers.get('Content-Type') || '')
6 | if (boundary) {
7 | return await getParsedFormData(request, boundary)
8 | } else {
9 | return {}
10 | }
11 | }
12 |
13 | function getBoundary(header: string) {
14 | var items = header.split(';');
15 | if (items) {
16 | for (var i = 0; i < items.length; i++) {
17 | var item = new String(items[i]).trim();
18 | if (item.indexOf('boundary') >= 0) {
19 | var k = item.split('=');
20 | return new String(k[1]).trim().replace(/^["']|["']$/g, "");
21 | }
22 | }
23 | }
24 | return '';
25 | }
26 |
27 | const normalizeLf = (str: string) => str.replace(/\r?\n|\r/g, '\r\n')
28 | const escape = (str: string) => normalizeLf(str).replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
29 | const replaceTrailingBoundary = (value: string, boundary: string) => {
30 | const boundaryStr = `--${boundary.trim()}--`
31 | return value.replace(boundaryStr, '')
32 | }
33 |
34 | export interface ParsedFileField {
35 | filename: string
36 | type: string,
37 | data: Buffer,
38 | size: number,
39 | }
40 | export type ParsedFormData = Record
41 |
42 | export function isFileField(fieldTuple: [string, string | ParsedFileField ]): fieldTuple is [string, ParsedFileField] {
43 | return typeof fieldTuple[1] !== 'string';
44 | }
45 | /**
46 | * Get parsed form data
47 | * @param data
48 | * @param boundary
49 | * @param spotText
50 | * @returns
51 | */
52 |
53 | async function getParsedFormData(request: Request, boundary: string, spotText?: string): Promise {
54 | const _boundary = ' ' + `${boundary}`
55 | const result: Record = {};
56 | const prefix = `--${_boundary.trim()}\r\nContent-Disposition: form-data; name="`
57 | const data = asString(Buffer.from(await request?.arrayBuffer()))
58 | const multiParts = data.split(prefix).filter(
59 | part => part.includes('"')
60 | ).map(
61 | part => [
62 | part.substring(0, part.indexOf('"')),
63 | part.slice(part.indexOf('"') + 1, -1)
64 | ]
65 | )
66 | multiParts.forEach(item => {
67 | if (/filename=".+"/g.test(item[1])) {
68 | const fileNameMatch = item[1].match(/filename=".+"/g)
69 | const contentTypeMatch = item[1].match(/Content-Type:\s.+/g)
70 | if(contentTypeMatch && fileNameMatch) {
71 | result[item[0]] = {
72 | filename: fileNameMatch?.[0].slice(10, -1),
73 | type: contentTypeMatch[0].slice(14),
74 | data: spotText? Buffer.from(item[1].slice(item[1].search(/Content-Type:\s.+/g) + contentTypeMatch[0].length + 4, -4), 'binary'):
75 | Buffer.from(item[1].slice(item[1].search(/Content-Type:\s.+/g) + contentTypeMatch[0].length + 4, -4), 'binary'),
76 | };
77 | result[item[0]]['size'] = Buffer.byteLength(result[item[0]].data)
78 | }
79 | } else {
80 | result[item[0]] = normalizeLf(replaceTrailingBoundary(item[1], _boundary)).trim()
81 | }
82 | });
83 | return result
84 | }
85 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/parsers/json.ts:
--------------------------------------------------------------------------------
1 | export const jsonSafeParse = (text: string | Record, reviver: ((this: any, key: string, value: any) => any) | undefined = undefined) => {
2 | if (typeof text !== 'string') return text
3 | const firstChar = text[0]
4 | if (firstChar !== '{' && firstChar !== '[' && firstChar !== '"') return text
5 | try {
6 | return JSON.parse(text, reviver)
7 | } catch (e) {}
8 |
9 | return text
10 | }
11 |
12 | export const jsonSafeStringify = (value: any, replacer: any, space: any) => {
13 | try {
14 | return JSON.stringify(value, replacer, space)
15 | } catch (e) {}
16 | return value
17 | }
18 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/parsers/query-string.ts:
--------------------------------------------------------------------------------
1 | export type ParsedUrlQuery = {
2 | [key: string]: string;
3 | }
4 | /**
5 | *
6 | * @param query
7 | * @returns
8 | */
9 |
10 | export const parseQueryParams = (searchParamsStr = ''): ParsedUrlQuery => Object.fromEntries(new URLSearchParams(searchParamsStr).entries())
11 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/parsers/req-body.ts:
--------------------------------------------------------------------------------
1 | import type { ParsedFormData, ParsedFileField } from './form-data.ts'
2 | import type { ParsedUrlQuery } from './query-string.ts'
3 | import { getFormDataFromRequest } from "./form-data.ts"
4 | import { parseQueryParams } from './query-string.ts'
5 |
6 | export type ParsedBody = string | ParsedUrlQuery | ParsedFormData
7 |
8 | export async function parseBody(request: Request): Promise {
9 | const contentType = request?.headers.get('Content-Type') || ''
10 | if(contentType.includes('application/json')) {
11 | // for `json` type
12 | let body = {}
13 | try {
14 | body = await request!.json()
15 | } catch {
16 | // do nothing
17 | }
18 | return body
19 | } else if (contentType.includes('application/text') || contentType.startsWith('text')) {
20 | // for `application/text` and `text/plain`
21 | return await request!.text()
22 | } else if (contentType.includes('application/x-www-form-urlencoded')) {
23 | const urlEncodedForm = await request.text()
24 | return parseQueryParams(urlEncodedForm)
25 | } else {
26 | return await getFormDataFromRequest(request)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/sanitize.ts:
--------------------------------------------------------------------------------
1 | const sanitizeKeyPrefixLeadingNumber = /^([0-9])/
2 | const sanitizeKeyRemoveDisallowedChar = /[^a-zA-Z0-9]+/g
3 |
4 | export const sanitizeKey = (key: string) => {
5 | return key
6 | .replace(sanitizeKeyPrefixLeadingNumber, '_$1')
7 | .replace(sanitizeKeyRemoveDisallowedChar, '_')
8 | }
9 |
10 | export const sanitzeValue = (value: string) => {
11 | return value
12 | .replace(/&/g, "&")
13 | .replace(//g, ">")
15 | .replace(/"/g, """)
16 | .replace(/'/g, "'");
17 | }
18 |
--------------------------------------------------------------------------------
/deno_dist/core/utils/types.ts:
--------------------------------------------------------------------------------
1 | type ReplaceOptions = {
2 | all?: boolean;
3 | };
4 |
5 | export type Replace<
6 | Input extends string,
7 | Search extends string,
8 | Replacement extends string,
9 | Options extends ReplaceOptions = {},
10 | > = Input extends `${infer Head}${Search}${infer Tail}`
11 | ? Options['all'] extends true
12 | ? `${Head}${Replacement}${Replace}`
13 | : `${Head}${Replacement}${Tail}`
14 | : Input;
15 |
16 | export type DecorateAsObject<
17 | Input extends string,
18 | Search extends string,
19 | Replacement extends string,
20 | > = Input extends `${infer Head}${Search}${infer Tail}`
21 | ? `${Head}${Replace}`
22 | : Input;
23 |
24 | export type Split<
25 | S extends string,
26 | Delimiter extends string,
27 | > = S extends `${infer Head}${Delimiter}${infer Tail}`
28 | ? [Head, ...Split]
29 | : S extends Delimiter
30 | ? []
31 | : [S];
32 |
33 | export type ObjectFromList, S = string> = {
34 | [K in (L extends ReadonlyArray ? U : never)]: S
35 | };
36 |
37 | export type KeysAsList = K extends `${infer Head}-${infer Tail}` ? [ Head, Tail ] : K extends `${infer Head}.${infer Tail}` ? [ Head, Tail ] : [K]
38 |
--------------------------------------------------------------------------------
/deno_dist/index.ts:
--------------------------------------------------------------------------------
1 | export * from './core/server.ts'
2 |
--------------------------------------------------------------------------------
/deno_dist/middlewares/README.md:
--------------------------------------------------------------------------------
1 | ## `Zarf` middlewares
2 |
3 | Soon this folder is gonna be populated with tasty, slick, `Zarf` middlewares
4 |
--------------------------------------------------------------------------------
/deno_dist/middlewares/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zarfjs/zarf/56bd2fafd61729b85a7b054654da1fba2ff62f3d/deno_dist/middlewares/index.ts
--------------------------------------------------------------------------------
/deno_dist/middlewares/mw-body-parser.ts:
--------------------------------------------------------------------------------
1 | import type { MiddlewareFunctionInitializer } from '../core/middleware.ts'
2 | import type { ParsedFileField, ParsedFormData } from '../core/utils/parsers/form-data.ts'
3 | import { parseBody } from '../core/utils/parsers/req-body.ts'
4 | import { isObject } from '../core/utils/is.ts'
5 | import { ZarfUnprocessableEntityError } from '~/core/errors DENOIFY: UNKNOWN NODE BUILTIN'
6 |
7 | type MiddlewareOptions = {
8 | extensions?: Array,
9 | maxSizeBytes?: number,
10 | maxFileSizeBytes?: number
11 | }
12 |
13 | const MW_OPTION_DEFAULTS: MiddlewareOptions = {
14 | extensions: [],
15 | maxSizeBytes: Number.MAX_SAFE_INTEGER,
16 | maxFileSizeBytes: Number.MAX_SAFE_INTEGER
17 | }
18 |
19 | function isFileField(fieldTuple: [string, string | ParsedFileField ]): fieldTuple is [string, ParsedFileField] {
20 | return typeof fieldTuple[1] !== 'string';
21 | }
22 |
23 | function validateFormData(formData: ParsedFormData, options: Required) {
24 | const { maxFileSizeBytes, maxSizeBytes, extensions } = options
25 | const fileFields = Object.entries(formData).filter(isFileField)
26 | let totalBytes = 0;
27 | let errors: Record = {}
28 | for (const [_, file] of fileFields) {
29 | totalBytes += file.size;
30 | if (file.size > maxFileSizeBytes!) {
31 | if(!errors['size']) errors['size'] = {}
32 | errors['size'] = {
33 | [file.filename]: {
34 | size: file.size,
35 | allowed: maxFileSizeBytes,
36 | message: `Unsupported file upload size: ${file.size} bytes, for file: ${file.filename} (maximum: ${maxFileSizeBytes} bytes).`
37 | }
38 | }
39 | }
40 |
41 | if (extensions.length) {
42 | const fileExt = (file.filename || '').split(".").pop()!
43 | if(!extensions.includes(fileExt)) {
44 | if(!errors['ext']) errors['ext'] = {}
45 | errors['ext'] = {
46 | [file.filename]: {
47 | ext: fileExt,
48 | allowed: extensions,
49 | message: `Unsupported file extension: ${fileExt} (allowed: ${extensions}).`
50 | }
51 | }
52 | }
53 | }
54 | }
55 | if (totalBytes > maxSizeBytes!) {
56 | if(!errors['size']) errors['size'] = {}
57 | if(!errors['size']['total']) errors['size']['total'] = {}
58 | errors['size']['total'] = {
59 | size: totalBytes,
60 | allowed: maxSizeBytes,
61 | message: `Unspported total upload size: ${totalBytes} bytes (maximum: ${maxSizeBytes} bytes).`
62 | }
63 | }
64 | return errors
65 | }
66 |
67 |
68 | export const bodyParser: MiddlewareFunctionInitializer = (opts) => {
69 | const options = { ...MW_OPTION_DEFAULTS, ...opts }
70 | return async (ctx, next) => {
71 | if(ctx.method === 'post' || ctx.method === 'put' || ctx.method === 'patch') {
72 | const body = await parseBody(ctx.request!)
73 | if(isObject(body)) {
74 | // @ts-ignore
75 | const errors = validateFormData(body, options)
76 | if (Object.keys(errors).length) {
77 | throw new ZarfUnprocessableEntityError(errors);
78 | }
79 | }
80 | ctx.body = body
81 | }
82 | await next()
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/deno_dist/middlewares/mw-cors.ts:
--------------------------------------------------------------------------------
1 | import { MiddlewareFunctionInitializer } from '../core/middleware.ts'
2 |
3 | const getOrigin = (reqOrigin: string, options: MiddlewareOptions) => {
4 | if(options.origins) {
5 | const { origins, credentials } = options
6 | if (origins.length > 0) {
7 | if (reqOrigin && origins.includes(reqOrigin)) {
8 | return reqOrigin
9 | } else {
10 | return origins[0]
11 | }
12 | } else {
13 | if (reqOrigin && credentials && origins[0] === '*') {
14 | return reqOrigin
15 | }
16 | return origins[0]
17 | }
18 | } else {
19 | return MW_OPTION_DEFAULTS.origins![0]
20 | }
21 | }
22 |
23 | type AllowedMethod = typeof MW_OPTION_METHOD_DEFAULTS[number]
24 | type HeaderVaryContent = "Origin" | "User-Agent" | "Accept-Encoding" | "Accept" | "Accept-Language"
25 | type MiddlewareOptions = {
26 | origins?: Array,
27 | credentials?: boolean,
28 | allowedHeaders?: Array,
29 | exposedHeaders?: Array,
30 | methods?: Array,
31 | vary?: HeaderVaryContent,
32 | maxAge?: number,
33 | preflightContinue?: boolean,
34 | cacheControl?: string
35 | }
36 |
37 | const MW_OPTION_METHOD_DEFAULTS = [ 'GET', 'PUT', 'POST', 'DELETE', 'PATCH', 'OPTIONS' ] as const
38 | const MW_OPTION_DEFAULTS: MiddlewareOptions = {
39 | origins: ['*'],
40 | credentials: false,
41 | allowedHeaders: [],
42 | exposedHeaders: [],
43 | methods: [ ...MW_OPTION_METHOD_DEFAULTS ],
44 | preflightContinue: false
45 | }
46 |
47 | const HEADERS = {
48 | ACAC: 'Access-Control-Allow-Credentials',
49 | ACAH: 'Access-Control-Allow-Headers',
50 | ACAM: 'Access-Control-Allow-Methods',
51 | ACAO: 'Access-Control-Allow-Origin',
52 |
53 | ACEH: 'Access-Control-Expose-Headers',
54 | ACRH: 'Access-Control-Request-Headers',
55 | ACRM: 'Access-Control-Request-Methods'
56 | }
57 |
58 | export const cors: MiddlewareFunctionInitializer = (opts) => {
59 | const options = { ...MW_OPTION_DEFAULTS, ...opts }
60 | return async (ctx, next: Function) => {
61 | const headers = ctx.response?.headers || new Map()
62 |
63 | if(headers.has(HEADERS.ACAC)) {
64 | options.credentials = headers.get(HEADERS.ACAC) === 'true'
65 | }
66 | if(options.credentials) ctx.setHeader(HEADERS.ACAC, String(options.credentials))
67 |
68 | if(options.allowedHeaders && options.allowedHeaders.length && !headers.has(HEADERS.ACAH)) {
69 | ctx.setHeader(HEADERS.ACAH, options.allowedHeaders.join(', '))
70 | }
71 | if(options.exposedHeaders && options.exposedHeaders.length && !headers.has(HEADERS.ACEH)) {
72 | ctx.setHeader(HEADERS.ACEH, options.exposedHeaders.join(', '))
73 | }
74 |
75 | if(options.methods && !headers.has(HEADERS.ACAM)) {
76 | ctx.setHeader(HEADERS.ACAM, options.methods.join(','))
77 | }
78 |
79 | if(options.origins?.length && !headers.has(HEADERS.ACAO)) {
80 | ctx.setHeader(HEADERS.ACAO, getOrigin(ctx.url.origin, options))
81 | }
82 |
83 | if(headers.has(HEADERS.ACAO) && headers.get(HEADERS.ACAO) !== '*') {
84 | options.vary = 'Origin'
85 | }
86 | if(options.vary && !headers.has('Vary')){
87 | ctx.setVary(options.vary)
88 | }
89 |
90 | if(options.maxAge && !headers.has('maxAge')) {
91 | ctx.setHeader('maxAge', String(options.maxAge))
92 | }
93 |
94 | if(ctx.method === 'options') {
95 |
96 | if(options.cacheControl && !headers.has('Cache-Control')) {
97 | ctx.setHeader('Cache-Control', options.cacheControl)
98 | }
99 |
100 | if (options.preflightContinue) {
101 | await next()
102 | } else {
103 | ctx.setHeader('Content-Length', '0')
104 | return ctx.halt(200, '')
105 | }
106 | } else {
107 | await next()
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/deno_dist/middlewares/mw-request-id.ts:
--------------------------------------------------------------------------------
1 | import { MiddlewareFunctionInitializer } from '../core/middleware.ts'
2 |
3 | type MiddlewareOptions = {
4 | header?: string,
5 | keygenFunc?: () => string,
6 | }
7 |
8 | const MW_OPTION_DEFAULTS: MiddlewareOptions = {
9 | header: 'X-Request-ID',
10 | keygenFunc: () => crypto.randomUUID()
11 | }
12 |
13 | const REQ_LOCALS_ID = 'request_id'
14 | export type RequestIdLocals = {[REQ_LOCALS_ID]?: string}
15 |
16 | export const requestId: MiddlewareFunctionInitializer = (opts) => {
17 | const { header, keygenFunc } = { ...MW_OPTION_DEFAULTS, ...opts }
18 | return async (ctx, next) => {
19 | if(header && keygenFunc && !ctx.request?.headers?.has(header)){
20 | const reqId = keygenFunc()
21 | ctx.setHeader(header, reqId)
22 | ctx.locals['request_id'] = reqId
23 | }
24 | await next()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/deno_dist/middlewares/mw-uploads.ts:
--------------------------------------------------------------------------------
1 | import type { MiddlewareFunctionInitializer } from '../core/middleware.ts'
2 | import { isFileField } from '../core/utils/parsers/form-data.ts'
3 |
4 | import * as fs from 'node:fs/promises DENOIFY: UNKNOWN NODE BUILTIN'
5 | import { constants } from 'node:fs DENOIFY: UNKNOWN NODE BUILTIN'
6 | import { join, extname, basename } from 'https://deno.land/std@0.158.0/node/path.ts'
7 |
8 | type Slash = '/'
9 | type MiddlewareOptions = {
10 | path?: '.' | '..' | `${Slash}${string}`
11 | useTempDir?: boolean
12 | useOriginalFileName?: boolean
13 | useLocals?: boolean
14 | }
15 |
16 | const MW_UPLOAD_DIR = 'uploads'
17 | const MW_OPTION_DEFAULTS: MiddlewareOptions = {
18 | useTempDir: false,
19 | useOriginalFileName: false,
20 | useLocals: false
21 | }
22 |
23 | const REQ_LOCALS_ID = 'uploads'
24 | export type RequestIdLocals = {[REQ_LOCALS_ID]?: Record}
25 |
26 | function getRandomizedFileName(filename: string) {
27 | return `${Date.now()}-${crypto.randomUUID()}${extname(filename)}`;
28 | }
29 |
30 | function isValidPath(path: string) {
31 | const _path = path.trim()
32 | return _path === '.' || _path === '..' || basename(_path)
33 | }
34 |
35 | export const uploads: MiddlewareFunctionInitializer = (opts) => {
36 | const options = { ...MW_OPTION_DEFAULTS, ...opts }
37 | return async (ctx, next) => {
38 | // The entire middleware runs only when `body-parser` has run before.
39 | // `body-parser` middleware already makes sure that `ctx.body` is
40 | // populated only when the request is one of the `POST`ables
41 | if(ctx.body) {
42 | const uploadDir = options.path && isValidPath(options.path) ? join(options.path, MW_UPLOAD_DIR) : options.useTempDir && process.env.TMPDIR ? join(process.env.TMPDIR, MW_UPLOAD_DIR) : join(process.cwd(), MW_UPLOAD_DIR)
43 |
44 | try {
45 | await fs.access(uploadDir, constants.R_OK)
46 | } catch(e: any) {
47 | if(e?.code && e.code === 'ENOENT') {
48 | await fs.mkdir(uploadDir)
49 | } else {
50 | throw new Error('Upload middleware cannot proceed')
51 | }
52 | }
53 | const locals: Record = {}
54 | Object.entries(ctx.body).filter(isFileField).forEach(async([name, file]) => {
55 | if(file.filename) {
56 | const filename = options.useOriginalFileName ? file.filename : getRandomizedFileName(file.filename)
57 | const filepath = join(uploadDir, filename)
58 | if(options.useLocals) locals[name] = filepath
59 | await Bun.write(filepath, new Blob([ file.data ], {
60 | type: file.type
61 | }))
62 | }
63 | })
64 | if(options.useLocals) ctx.locals[REQ_LOCALS_ID] = locals
65 | } else {
66 | console.debug(`Please configure body parser module to run before`)
67 | }
68 | await next()
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/deno_dist/mod.ts:
--------------------------------------------------------------------------------
1 | export * from "./index.ts";
--------------------------------------------------------------------------------
/example/app-with-adv-route.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from "../src"
2 |
3 | interface AppLocals {
4 | user: string
5 | }
6 |
7 | const app = new Zarf()
8 |
9 |
10 | app.get("/flights/:from-:to/", (ctx, params) => {
11 | return ctx.json({
12 | params
13 | })
14 | })
15 |
16 | app.get("/folder/:file.:ext", (ctx, params) => {
17 | return ctx.json({
18 | params
19 | })
20 | })
21 |
22 | app.get("/api/users.:ext", (ctx, params) => {
23 | return ctx.json({
24 | params
25 | })
26 | })
27 |
28 | app.get("/shop/product/color::color/size::size/dep::dep", (ctx, params) => {
29 | return ctx.json({
30 | color: params.color,
31 | size: params.size
32 | })
33 | })
34 |
35 | app.get("/", (ctx) => {
36 | return ctx.html(`Welcome to Zarf App - Advanced Route Example Server`)
37 | })
38 |
39 |
40 | app.listen({
41 | port: 3000
42 | }, (server) => {
43 | console.log(`Server started on ${server.port}`)
44 | })
45 |
--------------------------------------------------------------------------------
/example/app-with-middleware.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from "../src"
2 | import { logger, loggerAfter } from './deps/mw-logger'
3 |
4 | interface AppLocals {
5 | user: string
6 | }
7 |
8 | const app = new Zarf()
9 |
10 | app.get("/hello/:name", [ logger() ], (ctx, params) => {
11 | console.log('before:', ctx.locals)
12 | ctx.locals.user = "John"
13 | console.log('after:', ctx.locals)
14 | return ctx.json({
15 | message: `Hello World! ${params.name}`,
16 | });
17 | })
18 |
19 | app.get("/", (ctx) => {
20 | return ctx.html(`Welcome to Zarf App - Middleware Example Server`)
21 | })
22 |
23 | app.use(logger()).use(loggerAfter, 'after')
24 |
25 | app.listen({
26 | port: 3000
27 | }, (server) => {
28 | console.log(`Server started on ${server.port}`)
29 | })
30 |
--------------------------------------------------------------------------------
/example/app-with-mounting.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from "../src"
2 | import { logger, loggerAfter } from './deps/mw-logger'
3 |
4 | /**
5 | * Main App
6 | */
7 | interface AppLocals {
8 | user: string
9 | }
10 |
11 | const app = new Zarf()
12 |
13 | app.get("/", (ctx) => {
14 | return ctx.html(`Welcome to Zarf App - App Mounting Example Server`)
15 | })
16 |
17 | app.get("/hello", (ctx) => {
18 | return ctx.html(`Welcome to Zarf App - App Mounting Example Server`)
19 | })
20 |
21 |
22 | /**
23 | * Sub-App
24 | */
25 |
26 | interface SubAppLocals {
27 | user: string
28 | }
29 |
30 | const subApp = new Zarf({
31 | appName: 'ZarfSubApp'
32 | })
33 |
34 | subApp.get("/goodbye/:name", [ logger() ], (ctx, params) => {
35 | return ctx.json({
36 | message: `Goodbye, ${params.name}`,
37 | });
38 | })
39 |
40 | subApp.use(logger()).use(loggerAfter, 'after')
41 |
42 | /**
43 | * Mount
44 | */
45 |
46 | app.mount("/sub", subApp)
47 |
48 |
49 | /**
50 | * Listen
51 | */
52 | app.listen({
53 | port: 3000
54 | })
55 |
--------------------------------------------------------------------------------
/example/app-with-mw-body-parser.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from '../src'
2 | import { bodyParser } from '../src/middlewares/mw-body-parser'
3 |
4 | const app = new Zarf()
5 |
6 | app.post("/users", [ bodyParser({
7 | extensions: ['jpg'],
8 | maxFileSizeBytes: 2_4000,
9 | maxSizeBytes: 2_6000
10 | }) ], (ctx) => {
11 | // console.log(ctx.body)
12 | return ctx.json({
13 | message: "users created",
14 | });
15 | })
16 |
17 | app.get("/", (ctx) => {
18 | return ctx.json({
19 | message: "Hello World! I'm ready to take all yo data and uploads.",
20 | });
21 | })
22 |
23 | app.listen({
24 | port: 3000
25 | }, (server) => {
26 | console.log(`Server started on ${server.port}`)
27 | })
28 |
--------------------------------------------------------------------------------
/example/app-with-mw-cors.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from '../src'
2 | import { cors } from '../src/middlewares/mw-cors'
3 |
4 | const app = new Zarf()
5 |
6 | app.use(cors(), 'after')
7 |
8 | app.get("/", (ctx) => {
9 | return ctx.json({
10 | message: "Hello World! I'm CORS enabled",
11 | });
12 | })
13 |
14 | app.listen({
15 | port: 3000
16 | }, (server) => {
17 | console.log(`Server started on ${server.port}`)
18 | })
19 |
--------------------------------------------------------------------------------
/example/app-with-mw-request-id.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from '../src'
2 | import { requestId, RequestIdLocals } from '../src/middlewares/mw-request-id'
3 |
4 | const app = new Zarf()
5 |
6 | app.use(requestId())
7 |
8 | app.get("/", (ctx) => {
9 | if(ctx.locals.request_id) {
10 | return ctx.json({
11 | message: `Hello, mate! here's your request id: ${ctx.locals.request_id}`,
12 | });
13 | }
14 | return ctx.json({
15 | message: `Hello, world!`,
16 | });
17 | })
18 |
19 | app.listen({
20 | port: 3000
21 | }, (server) => {
22 | console.log(`Server started on ${server.port}`)
23 | })
24 |
--------------------------------------------------------------------------------
/example/app-with-mw-uploads.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from '../src'
2 | import { bodyParser } from '~/middlewares/mw-body-parser'
3 | import { uploads } from '~/middlewares/mw-uploads'
4 |
5 | const app = new Zarf()
6 |
7 | app.post("/users", [ bodyParser({
8 | extensions: ['jpg', 'jpeg'],
9 | maxFileSizeBytes: 3_4000,
10 | maxSizeBytes: 5_6000
11 | }), uploads({
12 | useOriginalFileName: false,
13 | useLocals: true
14 | }) ], async (ctx) => {
15 | // your upload details here if `useLocals` is true
16 | // console.log(ctx.locals)
17 | return ctx.json({
18 | message: "users created",
19 | });
20 | })
21 |
22 | app.get("/", (ctx) => {
23 | return ctx.json({
24 | message: "Hello World! I'm ready to keep your uploads.",
25 | });
26 | })
27 |
28 | app.listen({
29 | port: 3000
30 | }, (server) => {
31 | console.log(`Server started on ${server.port}`)
32 | })
33 |
--------------------------------------------------------------------------------
/example/app-with-route-grouping.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from "../src"
2 | import { logger, loggerAfter } from './deps/mw-logger'
3 |
4 | interface AppLocals {
5 | user: string
6 | }
7 |
8 | const app = new Zarf()
9 |
10 | const api = app.group("/api", (_, next) => {
11 | console.log("called from API")
12 | return next()
13 | }, async (ctx) => {
14 | console.log("called from API again")
15 | return ctx.html(`You've visited a group route. Please try going to some child route`)
16 | })
17 |
18 | const apiV1 = api.group('/v1', (_, next) => {
19 | console.log("called from API v1")
20 | return next()
21 | })
22 |
23 | apiV1.get('/list', (ctx) => {
24 | return ctx.json({
25 | list: 'list'
26 | })
27 | })
28 | apiV1.get('/user', (ctx) => {
29 | return ctx.json({
30 | user: 'user'
31 | })
32 | })
33 |
34 | const apiV2 = api.group('/v2')
35 | apiV2.get('/list', (ctx) => {
36 | return ctx.json({
37 | list: 'list'
38 | })
39 | })
40 | apiV2.get('/user', (ctx) => {
41 | return ctx.json({
42 | user: 'user'
43 | })
44 | })
45 |
46 | app.get("/", (ctx) => {
47 | return ctx.html(`Welcome to Zarf App - Route Grouping Example Server`)
48 | })
49 |
50 | app.use(logger()).use(loggerAfter, 'after')
51 |
52 | app.listen({
53 | port: 3000
54 | })
55 |
--------------------------------------------------------------------------------
/example/app.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from "../src"
2 | import { JSONValue } from "~/core/types"
3 | import { html } from "~/core/utils/parsers/html"
4 |
5 | interface AppLocals {
6 | user: string
7 | }
8 |
9 | const app = new Zarf()
10 |
11 | app.get("/hello", (ctx) => {
12 | return ctx.json({
13 | hello: "hello"
14 | })
15 | })
16 |
17 | const timeEl = (ts = new Date()) => html`
18 |
19 | `;
20 |
21 | app.get("/hello/:user", async (ctx, params) => {
22 | return ctx.html(html`Hello, ${params.user}! ${[1, 2, 3]} - ${timeEl}`)
23 | })
24 |
25 | app.post("/hello", async(ctx) => {
26 | const { request } = ctx
27 | // `FormData` is not available in `Bun`, if you need this today, you might wanna give `BodyParser` a shot
28 | const body = await request?.json()
29 | // do something with the body
30 | return ctx.json(body!)
31 | })
32 |
33 |
34 | app.get("/text", (ctx) => {
35 | return ctx.text("lorem ipsum", {
36 | status: 404,
37 | statusText: "created"
38 | })
39 | })
40 |
41 | app.get("/user/:name/books/:title", (ctx, params) => {
42 | const { name, title } = params
43 | return ctx.json({
44 | name,
45 | title
46 | })
47 | })
48 |
49 | app.get("/user/:name?", (ctx, params) => {
50 | return ctx.json({
51 |
52 | })
53 | })
54 |
55 | app.get("/admin/*all", (ctx, params) => {
56 |
57 | return ctx.json({
58 | name: params.all
59 | })
60 | })
61 |
62 | app.get("/v1/*brand/shop/*name", (ctx, params) => {
63 | return ctx.json({
64 | params
65 | })
66 | })
67 |
68 | app.get("/send", async (ctx) => {
69 | return ctx.send(Bun.file("./README.md"))
70 | })
71 |
72 | app.get("/", (ctx) => {
73 | return ctx.html(`Welcome to Zarf App server`)
74 | })
75 |
76 | app.listen({
77 | port: 3000,
78 | }, (server) => {
79 | console.log(`Server started on ${server.port}`)
80 | })
81 |
--------------------------------------------------------------------------------
/example/deno/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.unstable": true
4 | }
5 |
--------------------------------------------------------------------------------
/example/deno/app.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from "../../deno_dist/core/server.ts"
2 | import { serve } from "https://deno.land/std@0.158.0/http/server.ts"
3 |
4 | interface AppLocals {
5 | user: string
6 | }
7 |
8 | const app = new Zarf({
9 | appName: 'ZarfRoot',
10 | serverHeader: `Zarf`,
11 | strictRouting: false,
12 | getOnly: false,
13 | })
14 |
15 | app.get("/hello", (ctx) => {
16 | return ctx.json({
17 | hello: "hello"
18 | })
19 | })
20 |
21 | app.post("/hello", async(ctx) => {
22 | const { request } = ctx
23 | const body = await request?.json() // await request.text()
24 | // do something with the body
25 | return ctx.json(body)
26 | })
27 |
28 |
29 | app.get("/text", (ctx) => {
30 | return ctx.text("lorem ipsum", {
31 | status: 404,
32 | statusText: "created"
33 | })
34 | })
35 |
36 | app.get("/user/:name/books/:title", (ctx, params) => {
37 | const { name, title } = params
38 | return ctx.json({
39 | name,
40 | title
41 | })
42 | })
43 |
44 | app.get("/admin/*all", (ctx, params) => {
45 | return ctx.json({
46 | name: params.all
47 | })
48 | })
49 |
50 | app.get("/v1/*brand/shop/*name", (ctx, params) => {
51 | return ctx.json({
52 | params
53 | })
54 | })
55 |
56 | app.get("/", (ctx) => {
57 | return ctx.html(`Welcome to Zarf Deno App server`)
58 | })
59 |
60 | console.log(`Server started on 3000`)
61 | await serve(app.handle, { port: 3000 });
62 |
--------------------------------------------------------------------------------
/example/deps/mw-logger.ts:
--------------------------------------------------------------------------------
1 | import { MiddlewareFunction, MiddlewareFunctionInitializer } from '../../src/core/middleware'
2 |
3 | const MW_LOGGER_DEFAULTS = {
4 | logger: console.error
5 | }
6 |
7 | export const logger: MiddlewareFunctionInitializer = (options) => {
8 | let { logger } = { ...MW_LOGGER_DEFAULTS, ...options }
9 | if (typeof logger !== 'function') logger = console.log
10 | return async (context, next: Function) => {
11 | logger(`Request [${context.method}]: ${context.path}`)
12 | await next()
13 | }
14 | }
15 |
16 | const MW_LOGGER_ERR_DEFAULTS = {
17 | logger: console.error
18 | }
19 |
20 | export const errorLogger: MiddlewareFunctionInitializer = (options) => {
21 | let { logger } = { ...MW_LOGGER_ERR_DEFAULTS, ...options }
22 | if (typeof logger !== 'function') logger = console.error
23 | return async (context, next: Function) => {
24 | logger(context.error)
25 | await next()
26 | }
27 | }
28 |
29 | export const loggerAfter: MiddlewareFunction = async (context, next) => {
30 | console.info(`After response for [${context.method}]: ${context.path}`);
31 | const ms = Date.now() - context.meta.startTime;
32 | context.response?.headers.set("X-Response-Time", `${ms}ms`);
33 | await next()
34 | }
35 |
--------------------------------------------------------------------------------
/example/deps/sub-app.ts:
--------------------------------------------------------------------------------
1 | import { Zarf } from "../../src"
2 |
3 | interface AppLocals {
4 | user: string
5 | }
6 |
7 | export const subApp = new Zarf()
8 |
9 | subApp.get("/goodbye", (ctx) => {
10 | return ctx.json({
11 | hello: "goodbye"
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "restartable": "rs",
3 | "ignore": [".git", "node_modules/", "dist/"],
4 | "exec": "bun example/app.ts",
5 | "ext": "js, json, ts"
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@zarfjs/zarf",
3 | "version": "0.0.1-alpha.23",
4 | "author": "Aftab Alam ",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/zarfjs/zarf.git"
8 | },
9 | "main": "dist/index.js",
10 | "module": "dist/index.mjs",
11 | "devDependencies": {
12 | "@typescript-eslint/eslint-plugin": "^5.33.1",
13 | "@typescript-eslint/parser": "^5.33.1",
14 | "bumpp": "^8.2.1",
15 | "bun-types": "^0.1.8",
16 | "denoify": "^1.1.4",
17 | "eslint": "^8.22.0",
18 | "nodemon": "^2.0.19",
19 | "publint": "^0.1.1",
20 | "rimraf": "^3.0.2",
21 | "tsup": "^6.2.2",
22 | "typescript": "^4.7.4",
23 | "vitest": "^0.23.4"
24 | },
25 | "exports": {
26 | ".": {
27 | "require": "./dist/index.js",
28 | "import": "./dist/index.mjs"
29 | },
30 | "./utils/is": {
31 | "require": "./dist/utils/is/index.js",
32 | "import": "./dist/utils/is/index.mjs"
33 | },
34 | "./utils/mime": {
35 | "require": "./dist/utils/mime/index.js",
36 | "import": "./dist/utils/mime/index.mjs"
37 | },
38 | "./utils/sanitize": {
39 | "require": "./dist/utils/sanitize/index.js",
40 | "import": "./dist/utils/sanitize/index.mjs"
41 | },
42 | "./utils/*": {
43 | "require": "./dist/utils/*.js",
44 | "import": "./dist/utils/*.mjs"
45 | },
46 | "./parsers/body": {
47 | "require": "./dist/parsers/body/index.js",
48 | "import": "./dist/parsers/body/index.mjs"
49 | },
50 | "./parsers/json": {
51 | "require": "./dist/parsers/json/index.js",
52 | "import": "./dist/parsers/json/index.mjs"
53 | },
54 | "./parsers/qs": {
55 | "require": "./dist/parsers/qs/index.js",
56 | "import": "./dist/parsers/qs/index.mjs"
57 | },
58 | "./parsers/*": {
59 | "require": "./dist/parsers/*.js",
60 | "import": "./dist/parsers/*.mjs"
61 | },
62 | "./mw/cors": {
63 | "require": "./dist/mw/cors/index.js",
64 | "import": "./dist/mw/cors/index.mjs"
65 | },
66 | "./mw/request-id": {
67 | "require": "./dist/mw/request-id/index.js",
68 | "import": "./dist/mw/request-id/index.mjs"
69 | },
70 | "./mw/body-parser": {
71 | "require": "./dist/mw/body-parser/index.js",
72 | "import": "./dist/mw/body-parser/index.mjs"
73 | },
74 | "./mw/uploads": {
75 | "require": "./dist/mw/uploads/index.js",
76 | "import": "./dist/mw/uploads/index.mjs"
77 | },
78 | "./mw/*": {
79 | "require": "./dist/mw/*.js",
80 | "import": "./dist/mw/*.mjs"
81 | },
82 | "./package.json": "./package.json"
83 | },
84 | "bugs": "https://github.com/zarfjs/zarf/issues",
85 | "denoify": {
86 | "index": "src/index.ts",
87 | "includes": [
88 | "!src/core/utils/mime.test.ts"
89 | ]
90 | },
91 | "description": "Fast, Bun-powered, and Bun-only(for now) Web API framework with full Typescript support.",
92 | "files": [
93 | "dist",
94 | "utils",
95 | "parsers"
96 | ],
97 | "homepage": "https://github.com/zarfjs/zarf",
98 | "keywords": [
99 | "bun",
100 | "web",
101 | "framework",
102 | "http",
103 | "middleware"
104 | ],
105 | "license": "MIT",
106 | "publishConfig": {
107 | "access": "public"
108 | },
109 | "scripts": {
110 | "dev": "nodemon --config nodemon.json",
111 | "build": "bun run build:tsup --dts-resolve",
112 | "build:tsup": "tsup",
113 | "build:deno": "rimraf deno_dist && denoify",
114 | "release": "bumpp --commit --push --tag && npm publish",
115 | "prepublishOnly": "bun run build",
116 | "test": "bun run test:wip && bun run test:pkg",
117 | "test:wip": "bun wiptest",
118 | "test:unit": "vitest run --reporter=verbose",
119 | "test:pkg": "publint",
120 | "dev:deno": "deno run --unstable --reload --allow-read --allow-env --allow-net example/deno/app.ts"
121 | },
122 | "types": "dist/index.d.ts",
123 | "typesVersions": {
124 | "*": {
125 | "utils/is": [
126 | "./dist/utils/is"
127 | ],
128 | "utils/mime": [
129 | "./dist/utils/mime"
130 | ],
131 | "utils/sanitize": [
132 | "./dist/utils/sanitize"
133 | ],
134 | "utils/*": [
135 | "./dist/utils/*"
136 | ],
137 | "parsers/body": [
138 | "./dist/parsers/body"
139 | ],
140 | "parsers/json": [
141 | "./dist/parsers/json"
142 | ],
143 | "parsers/qs": [
144 | "./dist/parsers/qs"
145 | ],
146 | "parsers/*": [
147 | "./dist/parsers/*"
148 | ],
149 | "mw/cors": [
150 | "./dist/mw/cors"
151 | ],
152 | "mw/request-id": [
153 | "./dist/mw/request-id"
154 | ],
155 | "mw/body-parser": [
156 | "./dist/mw/body-parser"
157 | ],
158 | "mw/uploads": [
159 | "./dist/mw/uploads"
160 | ],
161 | "mw/*": [
162 | "./dist/mw/*"
163 | ]
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/core/constants/codes.ts:
--------------------------------------------------------------------------------
1 | import type { Replace } from '../utils/types'
2 | export const HTTP_STATUS_CODES = {
3 | // Informational
4 | 100: 'Continue',
5 | 101: 'Switching Protocols',
6 | // Successful
7 | 200: 'OK',
8 | 201: 'Created',
9 | 202: 'Accepted',
10 | 203: 'Non-Authoritative Information',
11 | 204: 'No Content',
12 | 205: 'Reset Content',
13 | 206: 'Partial Content',
14 | // Redirection
15 | 301: 'Moved Permanently',
16 | 302: 'Found',
17 | 303: 'See Other',
18 | 304: 'Not Modified',
19 | 307: 'Temporary Redirect',
20 | 308: 'Permanent Redirect',
21 | // Client Error
22 | 400: 'Bad Request',
23 | 401: 'Unauthorized',
24 | 403: 'Forbidden',
25 | 404: 'Not Found',
26 | 405: 'Method Not Allowed',
27 | 406: 'Not Acceptable',
28 | 407: 'Proxy Authentication Required',
29 | 408: 'Request Timeout',
30 | 409: 'Conflict',
31 | 410: 'Gone',
32 | 422: 'Unprocessable Entity',
33 | // Server Error
34 | 500: 'Internal Server Error',
35 | 501: 'Not Implemented',
36 | 502: 'Bad Gateway',
37 | 503: 'Service Unavailable',
38 | 504: 'Gateway Timeout'
39 | } as const
40 |
41 | export type HTTPStatusCode = keyof typeof HTTP_STATUS_CODES
42 | export type HTTPStatusCodeMesssage = typeof HTTP_STATUS_CODES[HTTPStatusCode]
43 | export type HTTPStatusCodeMesssageKey = Replace, '-', '', { all: true }>
44 |
--------------------------------------------------------------------------------
/src/core/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const ROUTE_OPTION_DEFAULT = Symbol('default')
2 |
--------------------------------------------------------------------------------
/src/core/constants/mime/application.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from '.'
2 |
3 | export type AppType = 'application'
4 | export type AppMimeType = `${AppType}/${string}`
5 |
6 | export const MIME_APP: Record = {
7 | 'application/atom+xml': {
8 | cmp: true,
9 | ext: ['atom']
10 | },
11 | 'application/calendar+json': {
12 | cmp: true
13 | },
14 | 'application/calendar+xml': {
15 | cmp: true,
16 | ext: ['xcs']
17 | },
18 | 'application/epub+zip': {
19 | cmp: false,
20 | ext: ['epub']
21 | },
22 | 'application/font-woff': {
23 | cmp: false
24 | },
25 | 'application/geo+json': {
26 | cmp: true,
27 | ext: ['geojson']
28 | },
29 | 'application/inkml+xml': {
30 | cmp: true,
31 | ext: ['ink', 'inkml']
32 | },
33 |
34 | 'application/java-archive': {
35 | cmp: false,
36 | ext: ['jar', 'war', 'ear']
37 | },
38 | 'application/java-serialized-object': {
39 | cmp: false,
40 | ext: ['ser']
41 | },
42 | 'application/java-vm': {
43 | cmp: false,
44 | ext: ['class']
45 | },
46 | 'application/javascript': {
47 | charset: 'UTF-8',
48 | cmp: true,
49 | ext: ['js', 'mjs']
50 | },
51 | 'application/jf2feed+json': {
52 | cmp: true
53 | },
54 | 'application/json': {
55 | charset: 'UTF-8',
56 | cmp: true,
57 | ext: ['json', 'map']
58 | },
59 | 'application/json-patch+json': {
60 | cmp: true
61 | },
62 | 'application/json5': {
63 | ext: ['json5']
64 | },
65 | 'application/jsonml+json': {
66 | cmp: true,
67 | ext: ['jsonml']
68 | },
69 | 'application/ld+json': {
70 | cmp: true,
71 | ext: ['jsonld']
72 | },
73 | 'application/manifest+json': {
74 | charset: 'UTF-8',
75 | cmp: true,
76 | ext: ['webmanifest']
77 | },
78 | 'application/msword': {
79 | cmp: false,
80 | ext: ['doc', 'dot']
81 | },
82 | 'application/node': {
83 | ext: ['cjs']
84 | },
85 | 'application/octet-stream': {
86 | cmp: false,
87 | ext: [
88 | 'bin',
89 | 'dms',
90 | 'lrf',
91 | 'mar',
92 | 'so',
93 | 'dist',
94 | 'distz',
95 | 'pkg',
96 | 'bpk',
97 | 'dump',
98 | 'elc',
99 | 'deploy',
100 | 'exe',
101 | 'dll',
102 | 'deb',
103 | 'dmg',
104 | 'iso',
105 | 'img',
106 | 'msi',
107 | 'msp',
108 | 'msm',
109 | 'buffer'
110 | ]
111 | },
112 | 'application/pdf': {
113 | cmp: false,
114 | ext: ['pdf']
115 | },
116 | 'application/gzip': {
117 | cmp: false,
118 | ext: ['gz']
119 | },
120 | 'application/x-bzip': {
121 | cmp: false,
122 | ext: ['bz']
123 | },
124 | 'application/x-bzip2': {
125 | cmp: false,
126 | ext: ['bz2', 'boz']
127 | },
128 |
129 | 'application/x-mobipocket-ebook': {
130 | ext: ['prc', 'mobi']
131 | },
132 | 'application/x-mpegurl': {
133 | cmp: false
134 | },
135 | 'application/x-ms-application': {
136 | ext: ['application']
137 | },
138 | 'application/x-msdos-program': {
139 | ext: ['exe']
140 | },
141 | 'application/x-msdownload': {
142 | ext: ['exe', 'dll', 'com', 'bat', 'msi']
143 | },
144 | 'application/x-rar-compressed': {
145 | cmp: false,
146 | ext: ['rar']
147 | },
148 | 'application/x-shockwave-flash': {
149 | cmp: false,
150 | ext: ['swf']
151 | },
152 | 'application/x-sql': {
153 | ext: ['sql']
154 | },
155 | 'application/x-web-app-manifest+json': {
156 | cmp: true,
157 | ext: ['webapp']
158 | },
159 | 'application/x-www-form-urlencoded': {
160 | cmp: true
161 | },
162 | 'application/xhtml+xml': {
163 | cmp: true,
164 | ext: ['xhtml', 'xht']
165 | },
166 | 'application/xml': {
167 | cmp: true,
168 | ext: ['xml', 'xsl', 'xsd', 'rng']
169 | },
170 | 'application/zip': {
171 | cmp: false,
172 | ext: ['zip']
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/core/constants/mime/audio.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from "."
2 | export type AudioType = 'audio'
3 | export type AudioMimeType = `${AudioType}/${string}`
4 |
5 | export const MIME_AUDIO: Record = {
6 | "audio/3gpp": {
7 | "cmp": false,
8 | "ext": ["3gpp"]
9 | },
10 | "audio/3gpp2": {
11 | },
12 | "audio/aac": {
13 | },
14 | "audio/ac3": {
15 | },
16 | "audio/midi": {
17 | "ext": ["mid","midi","kar","rmi"]
18 | },
19 | "audio/mp3": {
20 | "cmp": false,
21 | "ext": ["mp3"]
22 | },
23 | "audio/mp4": {
24 | "cmp": false,
25 | "ext": ["m4a","mp4a"]
26 | },
27 | "audio/mpeg": {
28 | "cmp": false,
29 | "ext": ["mpga","mp2","mp2a","mp3","m2a","m3a"]
30 | },
31 | "audio/ogg": {
32 | "cmp": false,
33 | "ext": ["oga","ogg","spx","opus"]
34 | },
35 |
36 | "audio/wav": {
37 | "cmp": false,
38 | "ext": ["wav"]
39 | },
40 | "audio/wave": {
41 | "cmp": false,
42 | "ext": ["wav"]
43 | },
44 | "audio/webm": {
45 |
46 | "cmp": false,
47 | "ext": ["weba"]
48 | },
49 | "audio/x-aac": {
50 |
51 | "cmp": false,
52 | "ext": ["aac"]
53 | },
54 | "audio/x-flac": {
55 | "ext": ["flac"]
56 | },
57 | "audio/x-m4a": {
58 | "ext": ["m4a"]
59 | },
60 | "audio/x-matroska": {
61 | "ext": ["mka"]
62 | },
63 | "audio/x-mpegurl": {
64 | "ext": ["m3u"]
65 | },
66 | "audio/x-ms-wma": {
67 | "ext": ["wma"]
68 | },
69 | "audio/x-wav": {
70 | "ext": ["wav"]
71 | },
72 | "audio/xm": {
73 | "ext": ["xm"]
74 | },
75 | }
76 |
--------------------------------------------------------------------------------
/src/core/constants/mime/font.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from '.'
2 |
3 | export type FontType = 'font'
4 | export type FontMimeType = `${FontType}/${string}`
5 |
6 | export const MIME_FONT: Record = {
7 | 'font/collection': {
8 | ext: ['ttc']
9 | },
10 | 'font/otf': {
11 | cmp: true,
12 | ext: ['otf']
13 | },
14 | 'font/ttf': {
15 | cmp: true,
16 | ext: ['ttf']
17 | },
18 | 'font/woff': {
19 | ext: ['woff']
20 | },
21 | 'font/woff2': {
22 | ext: ['woff2']
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/core/constants/mime/image.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from '.'
2 |
3 | export type ImageType = 'image'
4 | export type ImageMimeType = `${ImageType}/${string}`
5 |
6 | export const MIME_IMAGE: Record = {
7 | 'image/apng': {
8 | cmp: false,
9 | ext: ['apng']
10 | },
11 | 'image/avci': {
12 | ext: ['avci']
13 | },
14 | 'image/avcs': {
15 | ext: ['avcs']
16 | },
17 | 'image/avif': {
18 | cmp: false,
19 | ext: ['avif']
20 | },
21 | 'image/bmp': {
22 | cmp: true,
23 | ext: ['bmp']
24 | },
25 |
26 | 'image/gif': {
27 | cmp: false,
28 | ext: ['gif']
29 | },
30 |
31 | 'image/jp2': {
32 | cmp: false,
33 | ext: ['jp2', 'jpg2']
34 | },
35 | 'image/jpeg': {
36 | cmp: false,
37 | ext: ['jpeg', 'jpg', 'jpe']
38 | },
39 |
40 | 'image/pjpeg': {
41 | cmp: false
42 | },
43 | 'image/png': {
44 | cmp: false,
45 | ext: ['png']
46 | },
47 |
48 | 'image/svg+xml': {
49 | cmp: true,
50 | ext: ['svg', 'svgz']
51 | },
52 | 'image/t38': {
53 | ext: ['t38']
54 | },
55 | 'image/tiff': {
56 | cmp: false,
57 | ext: ['tif', 'tiff']
58 | },
59 | 'image/tiff-fx': {
60 | ext: ['tfx']
61 | },
62 | 'image/vnd.adobe.photoshop': {
63 | cmp: true,
64 | ext: ['psd']
65 | },
66 |
67 | 'image/vnd.microsoft.icon': {
68 | cmp: true,
69 | ext: ['ico']
70 | },
71 |
72 | 'image/vnd.ms-photo': {
73 | ext: ['wdp']
74 | },
75 | 'image/vnd.net-fpx': {
76 | ext: ['npx']
77 | },
78 |
79 | 'image/webp': {
80 | ext: ['webp']
81 | },
82 | 'image/wmf': {
83 | ext: ['wmf']
84 | },
85 |
86 | 'image/x-icon': {
87 | cmp: true,
88 | ext: ['ico']
89 | },
90 |
91 | 'image/x-ms-bmp': {
92 | cmp: true,
93 | ext: ['bmp']
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/core/constants/mime/index.ts:
--------------------------------------------------------------------------------
1 | import { TextMimeType, MIME_TEXT } from './text'
2 | import { FontMimeType, MIME_FONT } from './font'
3 | import { ImageMimeType, MIME_IMAGE } from './image'
4 | import { AudioMimeType, MIME_AUDIO } from './audio'
5 | import { VideoMimeType, MIME_VIDEO } from './video'
6 | import { AppMimeType , MIME_APP } from './application'
7 |
8 | export type MultipartMimeType = `multipart/${string}`
9 | export type MimeType = TextMimeType | FontMimeType | ImageMimeType | AudioMimeType | VideoMimeType | MultipartMimeType | AppMimeType
10 | export type MimeTypeConfig = {
11 | ext?: Array,
12 | cmp?: boolean,
13 | charset?: "UTF-8"
14 | }
15 |
16 | export const MIME_TYPES_CONFIG: Record = {
17 | ...MIME_TEXT,
18 | ...MIME_FONT,
19 | ...MIME_IMAGE,
20 | ...MIME_AUDIO,
21 | ...MIME_VIDEO,
22 | ...MIME_APP,
23 | "multipart/form-data": {
24 | "cmp": false
25 | }
26 | }
27 |
28 | function populateExtensionsMimeTypes() {
29 | Object.entries(MIME_TYPES_CONFIG).forEach(([mimeType, mimeConfig]) => {
30 | if(mimeConfig.ext && mimeConfig.ext.length) {
31 | MIME_TYPE_EXT[mimeType as MimeType] = mimeConfig.ext
32 | mimeConfig.ext.forEach(ext => {
33 | EXT_MIME_TYPES[ext] = mimeType as MimeType
34 | })
35 | }
36 | })
37 | }
38 |
39 | // Runtime consts
40 | export const EXT_MIME_TYPES: Record = Object.create(null);
41 | export const MIME_TYPE_EXT: Record> = Object.create(null);
42 |
43 | populateExtensionsMimeTypes()
44 |
--------------------------------------------------------------------------------
/src/core/constants/mime/text.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from '.'
2 |
3 | export type TextType = 'text'
4 | export type TextMimeType = `${TextType}/${string}`
5 |
6 | export const MIME_TEXT: Record = {
7 | 'text/cache-manifest': {
8 | cmp: true,
9 | ext: ['appcache', 'manifest']
10 | },
11 | 'text/calendar': {
12 | ext: ['ics', 'ifb']
13 | },
14 | 'text/calender': {
15 | cmp: true
16 | },
17 | 'text/cmd': {
18 | cmp: true
19 | },
20 | 'text/coffeescript': {
21 | ext: ['coffee', 'litcoffee']
22 | },
23 | 'text/css': {
24 | charset: 'UTF-8',
25 | cmp: true,
26 | ext: ['css']
27 | },
28 | 'text/csv': {
29 | cmp: true,
30 | ext: ['csv']
31 | },
32 | 'text/html': {
33 | cmp: true,
34 | ext: ['html', 'htm', 'shtml']
35 | },
36 | 'text/jade': {
37 | ext: ['jade']
38 | },
39 | 'text/javascript': {
40 | cmp: true
41 | },
42 | 'text/jsx': {
43 | cmp: true,
44 | ext: ['jsx']
45 | },
46 | 'text/less': {
47 | cmp: true,
48 | ext: ['less']
49 | },
50 | 'text/markdown': {
51 | cmp: true,
52 | ext: ['markdown', 'md']
53 | },
54 | 'text/mdx': {
55 | cmp: true,
56 | ext: ['mdx']
57 | },
58 | 'text/plain': {
59 | cmp: true,
60 | ext: ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini']
61 | },
62 | 'text/richtext': {
63 | cmp: true,
64 | ext: ['rtx']
65 | },
66 | 'text/rtf': {
67 | cmp: true,
68 | ext: ['rtf']
69 | },
70 | 'text/stylus': {
71 | ext: ['stylus', 'styl']
72 | },
73 | 'text/tab-separated-values': {
74 | cmp: true,
75 | ext: ['tsv']
76 | },
77 | 'text/uri-list': {
78 | cmp: true,
79 | ext: ['uri', 'uris', 'urls']
80 | },
81 | 'text/vcard': {
82 | cmp: true,
83 | ext: ['vcard']
84 | },
85 | 'text/vnd.dvb.subtitle': {
86 | ext: ['sub']
87 | },
88 | 'text/vtt': {
89 | charset: 'UTF-8',
90 | cmp: true,
91 | ext: ['vtt']
92 | },
93 | 'text/x-handlebars-template': {
94 | ext: ['hbs']
95 | },
96 | 'text/xml': {
97 | cmp: true,
98 | ext: ['xml']
99 | },
100 | 'text/yaml': {
101 | ext: ['yaml', 'yml']
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/core/constants/mime/video.ts:
--------------------------------------------------------------------------------
1 | import { MimeTypeConfig } from '.'
2 |
3 | export type VideoType = 'video'
4 | export type VideoMimeType = `${VideoType}/${string}`
5 |
6 | export const MIME_VIDEO: Record = {
7 | 'video/h264': {
8 | ext: ['h264']
9 | },
10 | 'video/h265': {},
11 | 'video/mp4': {
12 | cmp: false,
13 | ext: ['mp4', 'mp4v', 'mpg4']
14 | },
15 | 'video/mpeg': {
16 | cmp: false,
17 | ext: ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v']
18 | },
19 | 'video/ogg': {
20 | cmp: false,
21 | ext: ['ogv']
22 | },
23 | 'video/quicktime': {
24 | cmp: false,
25 | ext: ['qt', 'mov']
26 | },
27 | 'video/vnd.mpegurl': {
28 | ext: ['mxu', 'm4u']
29 | },
30 | 'video/webm': {
31 | cmp: false,
32 | ext: ['webm']
33 | },
34 | 'video/x-f4v': {
35 | ext: ['f4v']
36 | },
37 | 'video/x-fli': {
38 | ext: ['fli']
39 | },
40 | 'video/x-flv': {
41 | cmp: false,
42 | ext: ['flv']
43 | },
44 | 'video/x-m4v': {
45 | ext: ['m4v']
46 | },
47 | 'video/x-matroska': {
48 | cmp: false,
49 | ext: ['mkv', 'mk3d', 'mks']
50 | },
51 | 'video/x-msvideo': {
52 | ext: ['avi']
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/core/context.ts:
--------------------------------------------------------------------------------
1 | import type { ContextMeta, ZarfConfig, RouteMethod, HeaderVaryContent, HeaderTypeContent, JSONValue } from './types'
2 | import type { ParsedBody } from './utils/parsers/req-body'
3 | import { json, text, head, send, html } from './response'
4 | import { getContentType } from './utils/mime'
5 | import { MiddlewareFunction } from './middleware'
6 | import { HTTP_STATUS_CODES, HTTPStatusCode } from './constants/codes'
7 | import { ZarfHaltError } from './errors/error'
8 |
9 | // @ts-ignore
10 | const NEEDS_WARMUP = globalThis && globalThis.process && globalThis.process.isBun ? true : false
11 |
12 | /**
13 | * Execution context for handlers and all the middlewares
14 | */
15 | export class AppContext = {}> {
16 | private _response: Response | null
17 | private _config: ZarfConfig = {}
18 | private _relativeConfig: Record = {}
19 | private _error: any
20 | private _code: HTTPStatusCode | undefined;
21 | private _middlewares: Array> = []
22 |
23 | private readonly _request: Request | null
24 | readonly url: URL;
25 | readonly method: RouteMethod
26 | readonly host: string | undefined;
27 | readonly path: string;
28 | readonly query: URLSearchParams | undefined;
29 | readonly headers: Request["headers"]
30 |
31 | private _locals: S = {} as S
32 | readonly meta: ContextMeta = {
33 | startTime: 0
34 | }
35 |
36 | private _isImmediate: boolean = false
37 | public body: ParsedBody | null = null
38 |
39 | constructor(req: Request, config: ZarfConfig) {
40 | this.meta.startTime = Date.now()
41 |
42 | // Config
43 | this._config = config
44 |
45 | // Core Entities - `Request` and `Response`
46 | this._request = req
47 | this._response = new Response('')
48 |
49 | // Convenience Wrappers/Properties around the `Request`
50 | // Request: Verb
51 | this.method = req.method.toLowerCase() as RouteMethod
52 | // Request: URL
53 | this.url = new URL(req.url)
54 | // Request: Host
55 | this.host = this.url.host
56 | // Request: Path
57 | this.path = this._config?.strictRouting || this.url.pathname === '/' ?
58 | this.url.pathname :
59 | this.url.pathname.endsWith('/') ?
60 | this.url.pathname.substring(0, this.url.pathname.length -1) :
61 | this.url.pathname
62 | // Request: Query Params
63 | this.query = new Proxy(new URLSearchParams(req.url), {
64 | get: (params, param) => params.get(param as string),
65 | })
66 | // Request: Headers (Proxy)
67 | this.headers = new Proxy(this._request.headers, {
68 | get: (headers, header) => headers.get(header as string),
69 | })
70 |
71 | // Response: Warm-up!
72 | if(this._config?.serverHeader) {
73 | this._response.headers.set('Server', this._config.serverHeader)
74 | }
75 |
76 | /**
77 | * Currently needed for reading request body using `json`, `text` or `arrayBuffer`
78 | * without taking forever to resolve when any of them are accessed later
79 | *
80 | * But, needed only for `Bun`. If left applied for all the cases, this creates an issue with Node.js, Deno, etc.
81 | */
82 | if(NEEDS_WARMUP) {
83 | this._request.blob();
84 | }
85 | }
86 |
87 | /**
88 | * Get the current request in the raw form
89 | */
90 | get request() {
91 | return this._request
92 | }
93 |
94 | /**
95 | * Get the `Response` if any
96 | */
97 | get response() {
98 | return this._response
99 | }
100 | /**
101 | * Set the `Response`
102 | */
103 | set response(resp) {
104 | this._response = resp
105 | }
106 |
107 | /**
108 | * Get the current status code
109 | */
110 | get status() {
111 | return this._code as HTTPStatusCode
112 | }
113 | /**
114 | * Set the current status code
115 | */
116 | set status(code: HTTPStatusCode) {
117 | this._code = code;
118 | }
119 |
120 | get isImmediate() {
121 | return this._isImmediate
122 | }
123 |
124 | /// HEADER HELPERS ///
125 | setHeader(headerKey: string, headerVal: string) {
126 | return this._response?.headers.set(headerKey, headerVal)
127 | }
128 |
129 | setType(headerVal: HeaderTypeContent) {
130 | const contentType = getContentType(headerVal)
131 | if(contentType) {
132 | this.setHeader('Content-Type', getContentType(headerVal) as string)
133 | }
134 | return
135 | }
136 |
137 | isType(headerVal: HeaderTypeContent) {
138 | return this._request?.headers.get('Content-Type') === getContentType(headerVal)
139 | }
140 |
141 | accepts(headerVal: HeaderTypeContent) {
142 | return this._request?.headers.get('Accepts')?.includes(getContentType(headerVal) || '')
143 | }
144 |
145 | // https://www.smashingmagazine.com/2017/11/understanding-vary-header/
146 | setVary(...headerVals: Array) {
147 | if(headerVals.length) {
148 | const varies = (this._response?.headers.get('Vary') || '').split(',')
149 | this._response?.headers.set('Vary', [...new Set([...varies ,...headerVals])].join(','))
150 | }
151 | }
152 |
153 | /**
154 | * Get Error
155 | */
156 | get error() {
157 | return this._error
158 | }
159 | /**
160 | * Set Error
161 | */
162 | set error(err) {
163 | this._error = err
164 | }
165 |
166 | // Getter/Setter for App-specific details
167 | /**
168 | * Get available App-specific details
169 | */
170 | get locals() {
171 | return this._locals as S
172 | }
173 | /**
174 | * Set App-specific details
175 | */
176 | set locals(value: S) {
177 | this._locals = value
178 | }
179 |
180 | // Context helpers to send the `Response` in all the supported formats
181 | /**
182 | * Send the response as string, json, etc. - One sender to rule `em all!
183 | * @param body
184 | * @returns
185 | */
186 | async send(body: JSONValue | Blob | BufferSource , args: ResponseInit = {}): Promise {
187 | if(this._request?.method === 'HEAD') return this.head()
188 | return await send(body, {...this.getResponseInit(), ...args})
189 | }
190 |
191 | /**
192 | * Send the provided values as `json`
193 | * @param body
194 | * @returns
195 | */
196 | json(body: JSONValue, args: ResponseInit = {}): Response {
197 | return json(body, {...this.getResponseInit(), ...args})
198 | }
199 |
200 | /**
201 | * Send the provided value as `text`
202 | * @param _text
203 | * @returns
204 | */
205 | text(_text: string, args: ResponseInit = {}): Response {
206 | return text(_text, {...this.getResponseInit(), ...args})
207 | }
208 |
209 | /**
210 | * Send the provided value as `html`
211 | * @param _text
212 | * @returns
213 | */
214 | html(text: string, args: ResponseInit = {}): Response {
215 | return html(text, {...this.getResponseInit(), ...args})
216 | }
217 |
218 | /**
219 | * Just return with `head` details
220 | * @returns
221 | */
222 | head(args: ResponseInit = {}): Response {
223 | return head({...this.getResponseInit(), ...args})
224 | }
225 |
226 | /**
227 | * Halt flow, and immediately return with provided HTTP status code
228 | *
229 | * @param {number} statusCode - a valid HTTP status code
230 | * @param {JSONValue} body - to send in response. Could be `json`, `string`, etc. or nothing at all
231 | * @returns
232 | *
233 | *
234 | * @example HTTP Status
235 | * app.get("/authorized", (ctx) => {
236 | * // do something to check user's authenticity
237 | * ctx.halt(401)
238 | * // this line, and lines next to this will never be reached
239 | * return ctx.text("Authorized")
240 | * })
241 | *
242 | * @example HTTP Status and Body
243 | * app.get("/authorized", (ctx) => {
244 | * // do something to check user's authenticity
245 | * ctx.halt(401, 'You shall not pass')
246 | * // this line, and lines next to this will never be reached
247 | * return ctx.text("Authorized")
248 | * })
249 | *
250 | */
251 | halt(statusCode: HTTPStatusCode, body?: JSONValue) {
252 | throw new ZarfHaltError(
253 | statusCode,
254 | body || HTTP_STATUS_CODES[statusCode],
255 | {
256 | ...this.getResponseInit(),
257 | status: statusCode
258 | }
259 | )
260 | }
261 |
262 | /**
263 | * Redirect to the given URL
264 | *
265 | * @param url
266 | * @param statusCode
267 | * @returns
268 | */
269 | redirect(url: string, statusCode: HTTPStatusCode = 302): Response {
270 | this._isImmediate = true
271 | let loc = url
272 | if(loc ==='back') loc = this._request?.headers.get('Referrer') || '/'
273 | return Response.redirect(encodeURI(loc), statusCode)
274 | }
275 |
276 | /**
277 | * Use the settings from available `Response` details,
278 | * if there's one (as an outcome of handler processing and middleware execution)
279 | * @returns
280 | */
281 | private getResponseInit(): ResponseInit {
282 | if(this._response) {
283 | const { status, statusText, headers } = this._response
284 |
285 | const _headers: Record = {}
286 | headers.forEach((val, key) => {
287 | _headers[key] = val
288 | })
289 |
290 | return {
291 | headers: _headers,
292 | status: this._code || status,
293 | statusText
294 | }
295 | }
296 | else {
297 | return {}
298 | }
299 | }
300 |
301 | // MIDDLEWARE METHODS (MIDDLEWARE-ONLY USE)
302 |
303 | /**
304 | * Add a middleware that's invoked post the `Request` processing.
305 | *
306 | * IT'S INTENDED TO BE USED ONLY BY MIDDLEWARE DEVELOPERS. PLEASE DON'T
307 | * THIS IF YOU'RE A FRAMEWORK USER/CONSUMER
308 | *
309 | * @param func {MiddlewareFunction}
310 | */
311 | after(func: MiddlewareFunction) {
312 | this._middlewares.push(func)
313 | }
314 |
315 | /**
316 | * Returns a list of all the POST middlewares.
317 | *
318 | * IT'S INTENDED TO BE USED ONLY BY MIDDLEWARE DEVELOPERS. PLEASE DON'T
319 | * THIS IF YOU'RE A FRAMEWORK USER/CONSUMER
320 | *
321 | * @param func {MiddlewareFunction}
322 | */
323 | get afterMiddlewares() {
324 | return this._middlewares
325 | }
326 |
327 | // MOUNT METHODS (PRIVATE USE)
328 |
329 | public useAppConfigOnPath(prefix: string, config: ZarfConfig) {
330 | this._relativeConfig[prefix] = config
331 | }
332 |
333 | private getAppPrefix() {
334 | const path = this.path.substring(1)
335 | return '/' + path.substring(0, path.indexOf('/'))
336 | }
337 |
338 | // MOUNT METHODS (PUBLIC USE)
339 |
340 | /**
341 | * Get the current app's configuration, as provided while instantiating
342 | * the `Zarf` app
343 | * @returns {ZarfConfig}
344 | *
345 | * @example Access inside a route handler
346 | * app.get("/a-path-inside-main-or-mounted-app", (ctx) => {
347 | * // get the config
348 | * ctx.config
349 | * // go ahead...
350 | * })
351 | */
352 | public get config(): ZarfConfig {
353 | const appPrefix = this.getAppPrefix()
354 | return (this._relativeConfig[appPrefix] ? this._relativeConfig[appPrefix] : this._config) as ZarfConfig
355 | }
356 |
357 | /**
358 | * Get the relative path for the current app. Unlike `path` it isn't the
359 | * full relative path from the base URL of the site, but the actual relative
360 | * path you'd probably expect w.r.t a mounted app
361 | *
362 | * @returns {string} the relative path if it's a mounted app, or the original path if it's the main app
363 | *
364 | * @example Access inside a handler
365 | * app.get("/a-path-inside-main-or-mounted-app", (ctx) => {
366 | * // get the relativeUrl
367 | * ctx.relativeUrl
368 | * // go ahead...
369 | * })
370 | */
371 | public get relativePath() {
372 | const appPrefix = this.getAppPrefix()
373 | return this._relativeConfig[appPrefix] ? this.path.replace(appPrefix, '') : this.path
374 | }
375 | }
376 |
--------------------------------------------------------------------------------
/src/core/errors/config.ts:
--------------------------------------------------------------------------------
1 | import { badRequest, internalServerError, methodNotAllowed, notFound, unprocessableEntity } from "../response/created";
2 | import { RespCreatorRequestInit } from "../response/creator";
3 | import { HTTPStatusCodeMesssageKey } from '../constants/codes'
4 |
5 | export const serverErrorFns: Partial Response>> = {
6 | 'InternalServerError': internalServerError,
7 | 'NotFound': notFound,
8 | 'BadRequest': badRequest,
9 | 'UnprocessableEntity': unprocessableEntity,
10 | 'MethodNotAllowed': methodNotAllowed
11 | }
12 |
--------------------------------------------------------------------------------
/src/core/errors/error.ts:
--------------------------------------------------------------------------------
1 | import type { Errorlike } from "bun";
2 | import type { JSONValue } from '../types'
3 | import { pick } from '../utils/choose'
4 | import { HTTPStatusCode, HTTPStatusCodeMesssageKey } from '../constants/codes'
5 | import { serverErrorFns } from './config'
6 |
7 | type ErrorData = { [key: string]: any };
8 |
9 | export class ZarfCustomError extends Error implements Errorlike {
10 | constructor(
11 | readonly message: string,
12 | readonly code: HTTPStatusCodeMesssageKey = 'InternalServerError',
13 | readonly data: ErrorData = {},
14 | ) {
15 | super();
16 | }
17 | }
18 |
19 | export class ZarfHaltError extends Error {
20 | constructor(
21 | readonly code: HTTPStatusCode,
22 | readonly body: JSONValue,
23 | readonly init: ResponseInit = {},
24 | ) {
25 | super();
26 | }
27 | }
28 |
29 | export class ZarfNotFoundError extends ZarfCustomError {
30 | constructor(originalUrl: string) {
31 | super(`Route '${originalUrl}' does not exist.`, 'NotFound');
32 | this.name = this.constructor.name;
33 | }
34 | }
35 |
36 | export class ZarfMethodNotAllowedError extends ZarfCustomError {
37 | constructor() {
38 | super(`Requested method is not allowed for the server.`, 'MethodNotAllowed');
39 | this.name = this.constructor.name;
40 | }
41 | }
42 |
43 | export class ZarfBadRequestError extends ZarfCustomError {
44 | constructor(errorData: ErrorData) {
45 | super('There were validation errors.', 'BadRequest', errorData);
46 | this.name = this.constructor.name;
47 | }
48 | }
49 |
50 | export class ZarfUnprocessableEntityError extends ZarfCustomError {
51 | constructor(errorData: ErrorData) {
52 | super('Unprocessable Entity', 'UnprocessableEntity', errorData);
53 | this.name = this.constructor.name;
54 | }
55 | }
56 |
57 | export const sendError = (error: Errorlike) => {
58 | const isErrorSafeForClient = error instanceof ZarfCustomError;
59 | const send = error.code ? serverErrorFns[error.code as HTTPStatusCodeMesssageKey] : serverErrorFns['InternalServerError']
60 | const { message, data } = isErrorSafeForClient
61 | ? pick(error, ['message', 'data'])
62 | : {
63 | message: 'Something went wrong!',
64 | data: {},
65 | };
66 | return send?.(JSON.stringify({ message, errors: data }, null, 0))
67 | }
68 |
--------------------------------------------------------------------------------
/src/core/errors/handler.ts:
--------------------------------------------------------------------------------
1 | import type { Errorlike } from 'bun'
2 | import { AppContext } from '../context';
3 | import { sendError } from './error'
4 | /**
5 | * Special handlers
6 | * /
7 | /*
8 | * Default error handler
9 | * @param ctx
10 | * @param err
11 | * @returns
12 | */
13 | export function defaultErrorHandler = {}>(ctx: AppContext, err: Errorlike): Response {
14 | return sendError(err)!
15 | }
16 | /**
17 | * Not Found Error handler
18 | * @param ctx
19 | * @returns
20 | */
21 | export function notFoundHandler = {}>(ctx: AppContext): Response | Promise{
22 | return new Response(`No matching ${ctx.method.toUpperCase()} routes discovered for the path: ${ctx.path}`, {
23 | status: 404,
24 | });
25 | }
26 |
27 | /**
28 | * Not found verb error handler
29 | * @param ctx
30 | * @returns
31 | */
32 | export function notFoundVerbHandler = {}>(ctx: AppContext): Response | Promise {
33 | return new Response(`No implementations found for the verb: ${ctx.method.toUpperCase()}`, {
34 | status: 404,
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/core/errors/index.ts:
--------------------------------------------------------------------------------
1 | export * from './error'
2 | export * from './handler'
3 |
--------------------------------------------------------------------------------
/src/core/middleware.ts:
--------------------------------------------------------------------------------
1 | import type { AppContext } from "./context";
2 |
3 | // `Middleware` configs
4 | export type MiddlewareType = 'before' | 'after' | 'error'
5 | export const NoOpMiddleware: MiddlewareFunction = (_, next) => next();
6 | export type MiddlewareMeta = {
7 | isFirst: boolean,
8 | isLast: boolean
9 | }
10 |
11 | // `Middleware` return types
12 | export type MiddlewareFuncResp = void | Response;
13 | export type MiddlewareNextFunc = () => Promise;
14 |
15 | // `Middleware` function types
16 | export type MiddlewareFunction = {}> = (
17 | context: AppContext,
18 | next: MiddlewareNextFunc,
19 | meta?: MiddlewareMeta
20 | ) => Promise;
21 | export type MiddlewareFunctionInitializer<
22 | T extends Record = {},
23 | S extends Record = {}> = (options?: T) => MiddlewareFunction
24 |
25 | /**
26 | * Execute a sequence of middlewares
27 | * @param context
28 | * @param middlewares
29 | * @returns
30 | */
31 | export async function exec = {}>(context: AppContext, middlewares: Array>) {
32 | let prevIndexAt: number = -1;
33 |
34 | async function runner(indexAt: number): Promise {
35 | if (indexAt <= prevIndexAt) {
36 | throw new Error(`next() called multiple times by middleware #${indexAt}`)
37 | }
38 |
39 | prevIndexAt = indexAt;
40 | const middleware = middlewares[indexAt];
41 | if (middleware) {
42 | const resp = await middleware(context, () => {
43 | return runner(indexAt + 1);
44 | }, {
45 | isFirst: indexAt === 0,
46 | isLast: indexAt === middlewares.length - 1,
47 | });
48 | if (resp) return resp;
49 | }
50 | }
51 |
52 | return runner(0)
53 | }
54 |
--------------------------------------------------------------------------------
/src/core/response.ts:
--------------------------------------------------------------------------------
1 | import { getContentType } from './utils/mime'
2 | import { HTTPStatusCode, HTTP_STATUS_CODES } from './constants/codes'
3 |
4 | /**
5 | * Response helper: Sends the provided data as `json` to the client
6 | *
7 | * @param data `any` or provided type which could be JSON, string, etc.
8 | * @param args
9 | * @returns
10 | */
11 | export function json