Repository: kitojs/kito Branch: main Commit: 33b3c40ef365 Files: 179 Total size: 325.7 KB Directory structure: gitextract_l4urthhv/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── feature_request.yml │ └── workflows/ │ ├── ci.yml │ ├── doc-deploy.yml │ └── release.yml ├── .gitignore ├── Cargo.toml ├── bench/ │ ├── cases/ │ │ └── basic/ │ │ ├── elysia.ts │ │ ├── express.ts │ │ ├── fastify.ts │ │ ├── hapi.ts │ │ ├── hono.ts │ │ ├── kito.ts │ │ ├── koa.ts │ │ ├── restify.ts │ │ └── tinyhttp.ts │ ├── config.ts │ ├── package.json │ ├── readme.md │ ├── results/ │ │ ├── comparison-bun.json │ │ ├── comparison-node.json │ │ └── data/ │ │ ├── bun/ │ │ │ ├── elysia.json │ │ │ ├── express.json │ │ │ ├── fastify.json │ │ │ ├── hapi.json │ │ │ ├── hono.json │ │ │ ├── kito.json │ │ │ ├── koa.json │ │ │ └── tinyhttp.json │ │ └── node/ │ │ ├── elysia.json │ │ ├── express.json │ │ ├── fastify.json │ │ ├── hapi.json │ │ ├── hono.json │ │ ├── kito.json │ │ ├── koa.json │ │ ├── restify.json │ │ └── tinyhttp.json │ ├── runBench.ts │ ├── tsconfig.json │ └── utils/ │ ├── chart.ts │ ├── frameworkRunner.ts │ ├── http.ts │ └── wait.ts ├── biome.json ├── cli/ │ ├── .gitignore │ ├── Cargo.toml │ ├── install.js │ ├── package.json │ └── src/ │ ├── commands.rs │ ├── main.rs │ └── utils.rs ├── cliff.toml ├── examples/ │ ├── fluent/ │ │ ├── basic.ts │ │ ├── clustering/ │ │ │ ├── index.ts │ │ │ └── server.ts │ │ ├── extend.ts │ │ ├── file.ts │ │ ├── middlewares/ │ │ │ ├── index.ts │ │ │ └── samples/ │ │ │ ├── auth.ts │ │ │ └── logger.ts │ │ ├── params.ts │ │ ├── route.ts │ │ ├── router/ │ │ │ ├── cats.ts │ │ │ └── index.ts │ │ ├── schemas/ │ │ │ ├── builder.ts │ │ │ └── json.ts │ │ ├── streaming/ │ │ │ ├── sse.ts │ │ │ └── stream.ts │ │ └── unixSocket.ts │ ├── instance/ │ │ ├── basic.ts │ │ ├── clustering/ │ │ │ ├── index.ts │ │ │ └── server.ts │ │ ├── extend.ts │ │ ├── file.ts │ │ ├── middlewares/ │ │ │ ├── index.ts │ │ │ └── samples/ │ │ │ ├── auth.ts │ │ │ └── logger.ts │ │ ├── params.ts │ │ ├── route.ts │ │ ├── router/ │ │ │ ├── cats.ts │ │ │ └── index.ts │ │ ├── schemas/ │ │ │ ├── builder.ts │ │ │ └── json.ts │ │ ├── streaming/ │ │ │ ├── sse.ts │ │ │ └── stream.ts │ │ └── unixSocket.ts │ ├── package.json │ ├── runExample.ts │ └── tsconfig.json ├── lefthook.yml ├── license ├── package.json ├── packages/ │ ├── core/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── package.json │ │ ├── src/ │ │ │ ├── http/ │ │ │ │ ├── cookies.rs │ │ │ │ ├── cookies_tests.rs │ │ │ │ ├── files.rs │ │ │ │ ├── mime.rs │ │ │ │ ├── mime_tests.rs │ │ │ │ ├── request.rs │ │ │ │ └── response.rs │ │ │ ├── http.rs │ │ │ ├── lib.rs │ │ │ ├── server/ │ │ │ │ ├── context.rs │ │ │ │ ├── core.rs │ │ │ │ ├── handler.rs │ │ │ │ ├── router.rs │ │ │ │ └── routes.rs │ │ │ ├── server.rs │ │ │ ├── validation/ │ │ │ │ ├── parser.rs │ │ │ │ ├── parser_tests.rs │ │ │ │ ├── tests.rs │ │ │ │ ├── types.rs │ │ │ │ └── validators.rs │ │ │ └── validation.rs │ │ └── tsconfig.json │ ├── kitojs/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── readme.md │ │ ├── src/ │ │ │ ├── helpers/ │ │ │ │ ├── middleware.ts │ │ │ │ └── schema.ts │ │ │ ├── index.ts │ │ │ ├── schemas/ │ │ │ │ ├── builders.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jsonSchema.ts │ │ │ │ └── primitives/ │ │ │ │ ├── array.ts │ │ │ │ ├── boolean.ts │ │ │ │ ├── literal.ts │ │ │ │ ├── number.ts │ │ │ │ ├── object.ts │ │ │ │ ├── string.ts │ │ │ │ └── union.ts │ │ │ ├── server/ │ │ │ │ ├── analyzer.ts │ │ │ │ ├── request.ts │ │ │ │ ├── response.ts │ │ │ │ ├── router.ts │ │ │ │ └── server.ts │ │ │ └── types.ts │ │ ├── tests/ │ │ │ ├── analyzer.test.ts │ │ │ ├── middleware.test.ts │ │ │ ├── route-chain.test.ts │ │ │ ├── router.test.ts │ │ │ ├── schema-helper.test.ts │ │ │ ├── server.test.ts │ │ │ └── types.test.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── typedoc.json │ │ └── vitest.config.ts │ ├── readme.md │ └── types/ │ ├── .gitignore │ ├── package.json │ ├── src/ │ │ ├── context.d.ts │ │ ├── handlers.d.ts │ │ ├── http/ │ │ │ ├── request.d.ts │ │ │ └── response.d.ts │ │ ├── index.d.ts │ │ ├── router.d.ts │ │ ├── routes.d.ts │ │ ├── schema/ │ │ │ ├── array.d.ts │ │ │ ├── base.d.ts │ │ │ ├── boolean.d.ts │ │ │ ├── jsonSchema.d.ts │ │ │ ├── literal.d.ts │ │ │ ├── number.d.ts │ │ │ ├── object.d.ts │ │ │ ├── string.d.ts │ │ │ └── union.d.ts │ │ └── server.d.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-workspace.yaml ├── readme.md ├── rust-toolchain.toml ├── rustfmt.toml └── tsconfig.base.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug Report description: Report a problem or unexpected behavior in the project title: "[Bug]: " labels: ["bug"] body: - type: markdown attributes: value: | Thanks for taking the time to report a bug! - type: input id: version attributes: label: Version description: What version are you using? placeholder: e.g. 1.0.0-alpha.4 validations: required: true - type: textarea id: description attributes: label: Description of the issue description: Explain what happened and what you expected to happen placeholder: "Describe the bug here" validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce description: Provide steps so we can reproduce the issue placeholder: | 1. Run ... 2. Call ... 3. Observe ... validations: required: true - type: textarea id: logs attributes: label: Logs or error messages description: Paste any relevant logs, stack traces, or console output render: shell - type: textarea id: environment attributes: label: Environment description: Describe your environment placeholder: | OS: Runtime (Node/Bun/Deno): CPU: Additional details: - type: checkboxes id: checklist attributes: label: Checklist options: - label: I have searched existing issues - label: I am using the latest version ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature Request description: Suggest a new idea or improvement title: "[Feature]: " labels: ["enhancement"] body: - type: textarea id: summary attributes: label: Summary description: Briefly describe the feature you'd like to see validations: required: true - type: textarea id: motivation attributes: label: Motivation description: Why is this feature important? What problem does it solve? validations: required: true - type: textarea id: proposal attributes: label: Proposed Solution description: Describe how this feature could be implemented or how you'd expect it to behave - type: checkboxes id: checklist attributes: label: Checklist options: - label: I've searched for related issues - label: I've read the documentation ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [main, dev] pull_request: branches: [main, dev] jobs: lint-and-test: name: Lint & Test runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 8 - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache uses: actions/cache@v4 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install -r - name: Build run: pnpm build - name: Test TypeScript run: pnpm test working-directory: packages/kitojs rust-test: name: Rust Tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ packages/core/target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo- - name: Run Rust tests run: cargo test --verbose working-directory: packages/core - name: Check Rust formatting run: cargo fmt -- --check working-directory: packages/core - name: Run Clippy run: cargo clippy -- -D warnings working-directory: packages/core ================================================ FILE: .github/workflows/doc-deploy.yml ================================================ name: Deploy Docs on: push: branches: [main] paths: - 'packages/kitojs/src/**' - 'packages/types/src/**' workflow_dispatch: jobs: deploy-docs: name: Generate and Deploy API Docs runs-on: ubuntu-latest steps: - name: Checkout kito repo uses: actions/checkout@v4 with: path: kito - name: Checkout web repo uses: actions/checkout@v4 with: repository: kitojs/web token: ${{ secrets.WEB_REPO_TOKEN }} path: web - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 8 - name: Install kito dependencies run: pnpm install -r --ignore-scripts working-directory: kito - name: Build kito packages run: pnpm build working-directory: kito - name: Install TypeDoc in kitojs package run: pnpm add -D typedoc working-directory: kito/packages/kitojs - name: Generate TypeDoc working-directory: kito run: | cd packages/kitojs pnpm exec typedoc - name: Copy docs to web repo run: | # Remove old API docs if they exist rm -rf web/public/api # Copy new generated docs mkdir -p web/public/api cp -r kito/packages/kitojs/docs/* web/public/api/ # Verify files were copied ls -la web/public/api/ - name: Commit and push to web repo working-directory: web run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add public/api if git diff --staged --quiet; then echo "No changes to commit" else git commit -m "docs: update API documentation" git push fi ================================================ FILE: .github/workflows/release.yml ================================================ name: Release permissions: contents: write packages: write on: push: tags: - 'v*' workflow_dispatch: jobs: build-napi: name: Build NAPI - ${{ matrix.settings.target }} strategy: fail-fast: false matrix: settings: - host: macos-latest target: x86_64-apple-darwin build: pnpm core:build --target x86_64-apple-darwin - host: macos-latest target: aarch64-apple-darwin build: pnpm core:build --target aarch64-apple-darwin - host: windows-latest target: x86_64-pc-windows-msvc build: pnpm core:build --target x86_64-pc-windows-msvc - host: windows-latest target: i686-pc-windows-msvc build: pnpm core:build --target i686-pc-windows-msvc - host: windows-latest target: aarch64-pc-windows-msvc build: pnpm core:build --target aarch64-pc-windows-msvc - host: ubuntu-latest target: x86_64-unknown-linux-gnu build: pnpm core:build --target x86_64-unknown-linux-gnu --use-napi-cross - host: ubuntu-latest target: x86_64-unknown-linux-musl build: pnpm core:build --target x86_64-unknown-linux-musl -x - host: ubuntu-latest target: aarch64-unknown-linux-gnu build: pnpm core:build --target aarch64-unknown-linux-gnu --use-napi-cross - host: ubuntu-latest target: aarch64-unknown-linux-musl build: pnpm core:build --target aarch64-unknown-linux-musl -x - host: ubuntu-latest target: armv7-unknown-linux-gnueabihf build: pnpm core:build --target armv7-unknown-linux-gnueabihf --use-napi-cross runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: '20' - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 8 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: toolchain: stable targets: ${{ matrix.settings.target }} components: clippy, rustfmt - name: Install build dependencies (Linux only) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y build-essential musl-tools clang pkg-config libssl-dev - name: Setup zig (required for musl / cargo-zigbuild) if: ${{ contains(matrix.settings.target, 'musl') }} uses: mlugg/setup-zig@v2 with: version: '0.15.2' - name: Install cargo-zigbuild (musl only) if: ${{ contains(matrix.settings.target, 'musl') }} uses: taiki-e/install-action@v2 with: tool: cargo-zigbuild env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install dependencies run: pnpm install -r - name: Build NAPI binary run: ${{ matrix.settings.build }} working-directory: . shell: bash - name: List built files run: ls -la packages/core/dist/ shell: bash - name: Upload artifact uses: actions/upload-artifact@v5 with: name: bindings-${{ matrix.settings.target }} path: packages/core/dist/*.node if-no-files-found: error build-freebsd: runs-on: ubuntu-latest name: Build NAPI - FreeBSD steps: - uses: actions/checkout@v5 - name: Build on FreeBSD id: build uses: cross-platform-actions/action@v0.30.0 env: DEBUG: napi:* RUSTUP_IO_THREADS: 1 with: operating_system: freebsd version: '14.3' memory: 8G cpu_count: 3 environment_variables: 'DEBUG RUSTUP_IO_THREADS' shell: bash run: | sudo pkg install -y -f curl node libnghttp2 npm sudo npm install -g corepack curl https://sh.rustup.rs -sSf --output rustup.sh sh rustup.sh -y --profile minimal --default-toolchain stable sudo corepack enable source "$HOME/.cargo/env" echo "~~~~ rustc --version ~~~~" rustc --version echo "~~~~ node -v ~~~~" node -v echo "~~~~ pnpm --version ~~~~" pnpm --version pwd ls -lah whoami env freebsd-version pnpm install -r --filter '!bench' --filter '!examples' pnpm core:build rm -rf node_modules rm -rf target - name: Upload artifact uses: actions/upload-artifact@v5 with: name: bindings-x86_64-unknown-freebsd path: packages/core/dist/*.node if-no-files-found: error publish-npm: name: Publish to NPM runs-on: ubuntu-latest needs: - build-napi - build-freebsd steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' registry-url: 'https://registry.npmjs.org' - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 8 - name: Install dependencies run: pnpm install -r - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts - name: List all downloaded artifacts run: | echo "-- Downloaded artifacts structure --" ls -laR artifacts/ - name: Create dist directory for core run: mkdir -p packages/core/dist - name: Move all .node binaries to core dist run: | echo "-- Moving .node files --" for artifact_dir in artifacts/bindings-*; do if [ -d "$artifact_dir" ]; then target=$(basename "$artifact_dir" | sed 's/^bindings-//') echo "Processing target: $target" node_file=$(find "$artifact_dir" -name "*.node" -type f) if [ -n "$node_file" ]; then case "$target" in x86_64-apple-darwin) dest_name="core.darwin-x64.node" ;; aarch64-apple-darwin) dest_name="core.darwin-arm64.node" ;; x86_64-pc-windows-msvc) dest_name="core.win32-x64-msvc.node" ;; i686-pc-windows-msvc) dest_name="core.win32-ia32-msvc.node" ;; aarch64-pc-windows-msvc) dest_name="core.win32-arm64-msvc.node" ;; x86_64-unknown-linux-gnu) dest_name="core.linux-x64-gnu.node" ;; x86_64-unknown-linux-musl) dest_name="core.linux-x64-musl.node" ;; aarch64-unknown-linux-gnu) dest_name="core.linux-arm64-gnu.node" ;; aarch64-unknown-linux-musl) dest_name="core.linux-arm64-musl.node" ;; armv7-unknown-linux-gnueabihf) dest_name="core.linux-arm-gnueabihf.node" ;; x86_64-unknown-freebsd) dest_name="core.freebsd-x64.node" ;; *) echo "Warning: Unknown target $target, using generic name" dest_name="core.$target.node" ;; esac echo " Copying: $node_file -> packages/core/dist/$dest_name" cp -v "$node_file" "packages/core/dist/$dest_name" else echo " Warning: No .node file found in $artifact_dir" fi fi done echo "-- Final dist contents --" ls -la packages/core/dist/ - name: Build NAPI index files run: | cd packages/core pnpm napi build --platform --release --esm --const-enum --output-dir dist shell: bash - name: Verify core package structure run: | echo "-- Core package dist/ contents --" ls -la packages/core/dist/ echo "-- Checking for required files --" test -f packages/core/dist/index.js && echo "✓ index.js found" || echo "✗ index.js missing" test -f packages/core/dist/index.d.ts && echo "✓ index.d.ts found" || echo "✗ index.d.ts missing" echo "-- .node files --" find packages/core/dist/ -name "*.node" -exec basename {} \; - name: Build TypeScript packages run: pnpm ts:build - name: Publish @kitojs/kito-core run: pnpm publish --access public --no-git-checks working-directory: packages/core env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish @kitojs/types run: pnpm publish --access public --no-git-checks working-directory: packages/types env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish kitojs run: pnpm publish --access public --no-git-checks working-directory: packages/kitojs env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} create-release: name: Create GitHub Release runs-on: ubuntu-latest needs: publish-npm steps: - name: Checkout code uses: actions/checkout@v4 - name: Extract version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - name: Create Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: Release ${{ github.ref_name }} body: | ## 🐺 Kito ${{ steps.version.outputs.VERSION }} ### Installation ```bash npm install kitojs@${{ steps.version.outputs.VERSION }} # or pnpm add kitojs@${{ steps.version.outputs.VERSION }} ``` ### What's Changed See the changelog for details. ### Documentation Visit kito.pages.dev for full documentation. draft: false prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # typescript node_modules # rust /target ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "3" members = ["packages/core", "cli"] [profile.release] lto = "fat" codegen-units = 1 panic = "abort" opt-level = 3 strip = true ================================================ FILE: bench/cases/basic/elysia.ts ================================================ import { Elysia } from "elysia"; import { node } from "@elysiajs/node"; declare const Bun: any; export function start(port: number): { stop: () => void } { const isBun = typeof Bun !== "undefined"; const app = isBun ? new Elysia() : new Elysia({ adapter: node() }); app.get("/", () => "hello world!"); const server = app.listen(port); return { stop: async () => server.stop(), }; } ================================================ FILE: bench/cases/basic/express.ts ================================================ import express from "express"; export function start(port: number): { stop: () => void } { const app = express(); app.get("/", (req, res) => { res.send("hello world!"); }); const appListen = app.listen(port); return { stop: async () => appListen.close(), }; } ================================================ FILE: bench/cases/basic/fastify.ts ================================================ import Fastify from "fastify"; export function start(port: number): { stop: () => void } { const app = Fastify({ logger: false, }); app.get("/", (request, reply) => { reply.send("hello world!"); }); app.listen({ port }); return { stop: async () => await app.close(), }; } ================================================ FILE: bench/cases/basic/hapi.ts ================================================ import Hapi from "@hapi/hapi"; export function start(port: number): { stop: () => void } { const app = Hapi.server({ port }); app.route({ method: "GET", path: "/", handler: (request, h) => { return "hello world!"; }, }); (async () => await app.start())(); return { stop: async () => await app.stop(), }; } ================================================ FILE: bench/cases/basic/hono.ts ================================================ import { serve } from "@hono/node-server"; import { Hono } from "hono"; declare const Bun: any; export function start(port: number): { stop: () => void } { const app = new Hono(); app.get("/", (c) => c.text("hello world!")); if (typeof Bun !== "undefined") { const server = Bun.serve({ fetch: app.fetch, port, }); return { stop: () => server.stop(), }; } const server = serve({ fetch: app.fetch, port, }); return { stop: async () => server.close(), }; } ================================================ FILE: bench/cases/basic/kito.ts ================================================ import { server } from "kitojs"; export function start(port: number): { stop: () => void } { const app = server(); app.get("/", (ctx) => { ctx.res.send("hello world!"); }); app.listen(port); return { stop: async () => app.close(), }; } ================================================ FILE: bench/cases/basic/koa.ts ================================================ import Koa from "koa"; import Router from "@koa/router"; export function start(port: number): { stop: () => void } { const app = new Koa(); const router = new Router(); router.get("/", (ctx) => { ctx.body = "hello world!"; }); app.use(router.routes()); app.use(router.allowedMethods()); const appListen = app.listen(port); return { stop: async () => appListen.close(), }; } ================================================ FILE: bench/cases/basic/restify.ts ================================================ import restify from "restify"; export function start(port: number): { stop: () => void } { const app = restify.createServer(); app.get("/", (req, res, next) => { res.send("hello world!"); return next(); }); app.listen(port); return { stop: async () => app.close(), }; } ================================================ FILE: bench/cases/basic/tinyhttp.ts ================================================ import { App } from "@tinyhttp/app"; export function start(port: number): { stop: () => void } { const app = new App(); app.get("/", (req, res) => { res.send("hello world!"); }); const appListen = app.listen(port); return { stop: async () => appListen.close(), }; } ================================================ FILE: bench/config.ts ================================================ type FrameworkConfig = string; type FrameworkRuntime = "bun" | "node"; export default { frameworks: [ "kito", "elysia", "express", "fastify", "hono", "restify", "tinyhttp", "koa", "hapi", ] satisfies FrameworkConfig[], hostname: "localhost", connections: 100, pipelining: 10, duration: 30, workers: undefined, chart: { enabled: true, output: "results/charts/result.png", }, }; export type { FrameworkConfig, FrameworkRuntime }; ================================================ FILE: bench/package.json ================================================ { "name": "@kitojs/bench", "description": "Kito's benchmarks.", "private": true, "type": "module", "scripts": { "bench:run": "bun run runBench.ts", "bench:node": "bun run runBench.ts --exclude-runtime=bun", "bench:bun": "bun run runBench.ts --exclude-runtime=node" }, "dependencies": { "@elysiajs/node": "^1.4.2", "@hapi/hapi": "^21.4.3", "@hono/node-server": "^1.19.6", "@koa/router": "^14.0.0", "@tinyhttp/app": "^3.0.1", "chart.js": "^4.5.1", "elysia": "^1.4.16", "express": "^5.1.0", "fastify": "^5.6.1", "hono": "^4.10.4", "kitojs": "workspace:*", "koa": "^3.1.1", "restify": "^11.1.0", "skia-canvas": "^3.0.8" }, "devDependencies": { "@types/autocannon": "^7.12.7", "@types/express": "^5.0.5", "@types/koa": "^3.0.1", "@types/koa__router": "^12.0.5", "@types/restify": "^8.5.12", "tsx": "^4.20.6", "typescript": "^5.9.2" } } ================================================ FILE: bench/readme.md ================================================ # Kito - `benchmarks` Performance benchmarks comparing Kito against popular Node.js web frameworks. --- ## 📊 Latest Results ![Benchmark Results](results/charts/result-node.png) > **Note:** Benchmarks are automatically updated on each release. --- ## 🎯 Benchmarked Frameworks - **Kito** - High-performance TypeScript framework written in Rust - **Fastify** - Fast and low overhead web framework - **Hono** - Fast web framework for the edge - **Express** - Fast, unopinionated web framework - **Koa** - Expressive middleware framework - **Restify** - REST API framework - **TinyHTTP** - Lightweight Express-like framework - **Hapi** - Rich framework for building applications --- ## 🔧 Benchmark Configuration ```typescript { connections: 100, pipelining: 10, duration: 30, // seconds workers: undefined // auto } ``` All frameworks are tested with a simple "Hello World" endpoint to measure raw performance. --- ## 🚀 Running Benchmarks ### Prerequisites ```bash pnpm install ``` ### Run All Benchmarks ```bash pnpm bench:run basic ``` This will: 1. Start each framework on sequential ports (3000, 3001, ...) 2. Run [wrk](https://github.com/wg/wrk) load tests 3. Generate comparison charts 4. Save detailed results to `results/data/` ### Mixing Node and Bun runtimes Some framework adapters only work on Node.js while others can run under Bun. You can pin the runtime per framework in `config.ts`: ```ts frameworks: [ { name: "kito", runtime: "bun" }, { name: "elysia", runtime: "bun" }, { name: "restify", runtime: "node" }, "koa", // uses whichever runtime you launch the runner with ]; ``` - Default (string) entries execute inside the same runtime that starts `runBench.ts`. - `runtime: "bun"` executes via `pnpm dlx bun run ...`, so make sure `pnpm` is available (it will download Bun on demand). - `runtime: "node"` executes via `pnpm dlx tsx ...`, so no local CLI install is required beyond `pnpm`. --- ## 📁 Project Structure ``` bench/ ├── cases/ │ └── basic/ # Basic "Hello World" benchmarks │ ├── kito.ts │ ├── fastify.ts │ ├── express.ts │ └── ... ├── results/ │ ├── charts/ # Generated benchmark charts │ └── data/ # Raw JSON results ├── utils/ │ ├── http.ts # Wrk runner │ └── chart.ts # Chart generation ├── config.ts # Benchmark configuration └── runBench.ts # Main benchmark runner ``` --- ## 📈 Metrics Measured - **Requests/sec** - Average requests per second - **Latency (ms)** - Average response time in milliseconds - **Throughput (bytes/sec)** - Average data throughput --- ## 🎨 Adding New Benchmarks 1. Create a new folder in `cases/` (e.g., `cases/routing/`) 2. Add framework implementations following the pattern: ```typescript export function start(port: number): { stop: () => void } { const app = createServer(); app.get("/", handler); app.listen(port); return { stop: async () => app.close(), }; } ``` 3. Run your benchmark: ```bash pnpm bench:run routing ``` --- ## 📊 Understanding Results ### Requests/sec (Higher is better) Number of requests the server can handle per second. ### Latency (Lower is better) Time taken to receive a response. Includes network and processing time. ### Throughput (Higher is better) Amount of data transferred per second. --- ## ⚙️ Configuration Edit `config.ts` to customize: ```typescript export default { frameworks: ["kito", "fastify", "hono", ...], connections: 100, // Concurrent connections pipelining: 10, // Requests per connection duration: 30, // Test duration in seconds chart: { enabled: true, output: "results/charts/result.png" } }; ``` --- ## 🔬 Methodology - All tests run on the same machine with identical conditions - Each framework uses its recommended setup and defaults - Tests measure only the framework overhead, not business logic - Results are averaged over the test duration --- ## 📝 Notes - Benchmarks are indicative and may vary based on hardware - Real-world performance depends on application complexity - These tests measure raw throughput, not production scenarios - All frameworks are excellent choices for different use cases --- ## 🤝 Contributing Found an issue with the benchmarks? Want to add a new framework? 1. Fork the repository 2. Add your changes 3. Run the benchmarks locally 4. Submit a pull request --- ## 📄 License Licensed under the [MIT License](../license). --- [![blazingly fast](https://blazingly.fast/api/badge.svg?repo=kitojs%2Fkito)](https://blazingly.fast) ================================================ FILE: bench/results/comparison-bun.json ================================================ { "winner": "kito", "runtime": "bun", "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "ranking": [ { "rank": 1, "framework": "kito", "requests": 283106.67, "latency": 0.39031, "throughput": 32275169.28, "difference": "0%" }, { "rank": 2, "framework": "elysia", "requests": 149351.06, "latency": 0.6690900000000001, "throughput": 19115540.48, "difference": "-47.25%" }, { "rank": 3, "framework": "hono", "requests": 104550.49, "latency": 0.96, "throughput": 13379829.76, "difference": "-63.07%" }, { "rank": 4, "framework": "fastify", "requests": 58216.12, "latency": 1.72, "throughput": 7507804.16, "difference": "-79.44%" }, { "rank": 5, "framework": "tinyhttp", "requests": 40612.94, "latency": 2.46, "throughput": 7843348.48, "difference": "-85.65%" }, { "rank": 6, "framework": "express", "requests": 36992.17, "latency": 2.7, "throughput": 7098859.52, "difference": "-86.93%" }, { "rank": 7, "framework": "koa", "requests": 32931.2, "latency": 3.03, "throughput": 4246732.8, "difference": "-88.37%" }, { "rank": 8, "framework": "hapi", "requests": 25239.19, "latency": 3.96, "throughput": 4414504.96, "difference": "-91.08%" } ] } ================================================ FILE: bench/results/comparison-node.json ================================================ { "winner": "kito", "runtime": "node", "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "ranking": [ { "rank": 1, "framework": "kito", "requests": 282020.9, "latency": 0.38923, "throughput": 32149340.16, "difference": "0%" }, { "rank": 2, "framework": "fastify", "requests": 42597.14, "latency": 2.7, "throughput": 7539261.44, "difference": "-84.90%" }, { "rank": 3, "framework": "hono", "requests": 37869.16, "latency": 3.2, "throughput": 6668943.36, "difference": "-86.57%" }, { "rank": 4, "framework": "koa", "requests": 28399.52, "latency": 4.14, "throughput": 5001707.52, "difference": "-89.93%" }, { "rank": 5, "framework": "restify", "requests": 26687.06, "latency": 4.31, "throughput": 4959764.48, "difference": "-90.54%" }, { "rank": 6, "framework": "hapi", "requests": 26628.01, "latency": 4.48, "throughput": 5913968.64, "difference": "-90.56%" }, { "rank": 7, "framework": "tinyhttp", "requests": 24126.25, "latency": 4.89, "throughput": 5788139.52, "difference": "-91.45%" }, { "rank": 8, "framework": "elysia", "requests": 15821.79, "latency": 7.81, "throughput": 2548039.68, "difference": "-94.39%" }, { "rank": 9, "framework": "express", "requests": 9914.66, "latency": 12.59, "throughput": 2369781.76, "difference": "-96.48%" } ] } ================================================ FILE: bench/results/data/bun/elysia.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "bun", "framework": "elysia", "results": { "requests": { "average": 149351.06 }, "latency": { "average": 0.6690900000000001 }, "throughput": { "average": 19115540.48 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/bun/express.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "bun", "framework": "express", "results": { "requests": { "average": 36992.17 }, "latency": { "average": 2.7 }, "throughput": { "average": 7098859.52 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/bun/fastify.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "bun", "framework": "fastify", "results": { "requests": { "average": 58216.12 }, "latency": { "average": 1.72 }, "throughput": { "average": 7507804.16 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/bun/hapi.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "bun", "framework": "hapi", "results": { "requests": { "average": 25239.19 }, "latency": { "average": 3.96 }, "throughput": { "average": 4414504.96 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/bun/hono.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "bun", "framework": "hono", "results": { "requests": { "average": 104550.49 }, "latency": { "average": 0.96 }, "throughput": { "average": 13379829.76 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/bun/kito.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "bun", "framework": "kito", "results": { "requests": { "average": 283106.67 }, "latency": { "average": 0.39031 }, "throughput": { "average": 32275169.28 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/bun/koa.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "bun", "framework": "koa", "results": { "requests": { "average": 32931.2 }, "latency": { "average": 3.03 }, "throughput": { "average": 4246732.8 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/bun/tinyhttp.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "bun", "framework": "tinyhttp", "results": { "requests": { "average": 40612.94 }, "latency": { "average": 2.46 }, "throughput": { "average": 7843348.48 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/node/elysia.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "node", "framework": "elysia", "results": { "requests": { "average": 15821.79 }, "latency": { "average": 7.81 }, "throughput": { "average": 2548039.68 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/node/express.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "node", "framework": "express", "results": { "requests": { "average": 9914.66 }, "latency": { "average": 12.59 }, "throughput": { "average": 2369781.76 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/node/fastify.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "node", "framework": "fastify", "results": { "requests": { "average": 42597.14 }, "latency": { "average": 2.7 }, "throughput": { "average": 7539261.44 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/node/hapi.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "node", "framework": "hapi", "results": { "requests": { "average": 26628.01 }, "latency": { "average": 4.48 }, "throughput": { "average": 5913968.64 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/node/hono.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "node", "framework": "hono", "results": { "requests": { "average": 37869.16 }, "latency": { "average": 3.2 }, "throughput": { "average": 6668943.36 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/node/kito.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "node", "framework": "kito", "results": { "requests": { "average": 282020.9 }, "latency": { "average": 0.38923 }, "throughput": { "average": 32149340.16 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/node/koa.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "node", "framework": "koa", "results": { "requests": { "average": 28399.52 }, "latency": { "average": 4.14 }, "throughput": { "average": 5001707.52 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/node/restify.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "node", "framework": "restify", "results": { "requests": { "average": 26687.06 }, "latency": { "average": 4.31 }, "throughput": { "average": 4959764.48 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/results/data/node/tinyhttp.json ================================================ { "machine": { "cpu": "Intel(R) Core(TM) i5-8600K CPU @ 3.60GHz", "memory": "31 GB", "platform": "linux 6.12.48-1-MANJARO" }, "runtime": "node", "framework": "tinyhttp", "results": { "requests": { "average": 24126.25 }, "latency": { "average": 4.89 }, "throughput": { "average": 5788139.52 } }, "previousResults": null, "comparison": null } ================================================ FILE: bench/runBench.ts ================================================ import { runBenchmark } from "./utils/http.ts"; import config, { type FrameworkRuntime } from "./config.ts"; const { hostname, frameworks, chart } = config; import { generateChart } from "./utils/chart.ts"; import { waitForServerReady } from "./utils/wait.ts"; import fs from "node:fs"; import { spawn, type ChildProcess } from "node:child_process"; import { fileURLToPath } from "node:url"; import path from "node:path"; import os from "node:os"; type BenchmarkResult = { framework: string; result: { requests: { average: number }; latency: { average: number }; throughput: { average: number }; }; }; type RunningBenchmark = { stop: () => Promise | void; }; type NormalizedFramework = { name: string; runtime: FrameworkRuntime; }; const CURRENT_RUNTIME: FrameworkRuntime = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined" ? "bun" : "node"; const PROJECT_ROOT = path.dirname(fileURLToPath(import.meta.url)); const RUNNER_ENTRY = path.join(PROJECT_ROOT, "utils", "frameworkRunner.ts"); const PNPM_BIN = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; async function launchFramework( benchName: string, framework: NormalizedFramework, port: number, ): Promise { if (framework.runtime === CURRENT_RUNTIME) { const mod = await import(`./cases/${benchName}/${framework.name}.ts`); const bench = mod.default || mod; const instance: RunningBenchmark = bench.start(port); return { stop: async () => { if (typeof instance?.stop === "function") { await instance.stop(); } }, }; } const child = spawnFrameworkProcess( framework.runtime, benchName, framework.name, port, ); return { stop: async () => await stopChildProcess(child), }; } function spawnFrameworkProcess( targetRuntime: FrameworkRuntime, benchName: string, frameworkName: string, port: number, ): ChildProcess { if (targetRuntime === "node") { return spawn( PNPM_BIN, ["dlx", "tsx", RUNNER_ENTRY, benchName, frameworkName, String(port)], { stdio: "inherit", cwd: PROJECT_ROOT, }, ); } return spawn( PNPM_BIN, ["dlx", "bun", "run", RUNNER_ENTRY, benchName, frameworkName, String(port)], { stdio: "inherit", cwd: PROJECT_ROOT, }, ); } async function stopChildProcess(child: ChildProcess): Promise { if (!child.killed) { child.kill("SIGTERM"); } await new Promise((resolve, reject) => { child.once("error", reject); child.once("exit", (code, signal) => { if (code === 0 || signal === "SIGTERM") { resolve(); } else { reject( new Error(`Framework runner exited with code ${code ?? "null"}`), ); } }); }); } function getMachineSpecs() { const cpu = os.cpus()[0].model; const memory = `${Math.round(os.totalmem() / 1024 / 1024 / 1024)} GB`; const platform = `${os.platform()} ${os.release()}`; return { cpu, memory, platform }; } async function main() { const benchName = process.argv[2]; if (!benchName) { console.error( "You must pass the name of the benchmark, e.g.: pnpm bench:run basic", ); process.exit(1); } const args = process.argv.slice(3); const excludeRuntimes = args .find((arg) => arg.startsWith("--exclude-runtime=")) ?.split("=")[1] ?.split(",") || []; const excludeFrameworks = args .find((arg) => arg.startsWith("--exclude-framework=")) ?.split("=")[1] ?.split(",") || []; const runtimes: FrameworkRuntime[] = ["node", "bun"].filter( (r) => !excludeRuntimes.includes(r), ) as FrameworkRuntime[]; const machine = getMachineSpecs(); for (const runtime of runtimes) { console.log(`\n${"-".repeat(40)}`); console.log(`Running benchmarks on ${runtime.toUpperCase()} runtime`); console.log(`${"-".repeat(40)}\n`); const results: BenchmarkResult[] = []; let port = 3000; for (const frameworkName of frameworks) { if (excludeFrameworks.includes(frameworkName)) { console.log(`Skipping ${frameworkName} (excluded via flag)`); continue; } if (runtime === "bun" && frameworkName === "restify") { console.log( `Skipping ${frameworkName} (not compatible with Bun runtime)`, ); continue; } const framework: NormalizedFramework = { name: frameworkName, runtime: runtime, }; const benchInstance = await launchFramework(benchName, framework, port); await waitForServerReady(port); console.log( `Running benchmark for ${framework.name} (runtime: ${framework.runtime})...`, ); const URL = `http://${hostname}:${port}`; const result = await runBenchmark(URL); results.push({ framework: framework.name, result }); port++; await benchInstance.stop(); } console.table( results.map(({ framework, result }) => ({ Framework: framework, "Requests/sec": result.requests.average, "Latency (ms)": result.latency.average, "Throughput (bytes/sec)": result.throughput.average, })), ); if (chart?.enabled) { const output = chart.output?.replace("result.png", `result-${runtime}.png`) || `results/charts/result-${runtime}.png`; await generateChart( { frameworks: results.map((r) => r.framework), requests: results.map((r) => r.result.requests.average), latency: results.map((r) => r.result.latency.average), throughput: results.map((r) => r.result.throughput.average), }, output, runtime, ); } for (const result of results) { const OUTPUT_PATH = `results/data/${runtime}`; const FILE_PATH = `${OUTPUT_PATH}/${result.framework}.json`; let previousResults = null; let comparison = null; if (fs.existsSync(FILE_PATH)) { try { const content = JSON.parse(fs.readFileSync(FILE_PATH, "utf-8")); if (content.results) { previousResults = content.results; const calcDiff = (current: number, previous: number) => { const diff = ((current - previous) / previous) * 100; return `${diff > 0 ? "+" : ""}${diff.toFixed(2)}%`; }; comparison = { requests: calcDiff( result.result.requests.average, previousResults.requests.average, ), latency: calcDiff( result.result.latency.average, previousResults.latency.average, ), throughput: calcDiff( result.result.throughput.average, previousResults.throughput.average, ), }; } } catch (_) {} } const outputData = { machine, runtime, framework: result.framework, results: result.result, previousResults, comparison, }; const data = JSON.stringify(outputData, null, "\t"); if (!fs.existsSync(OUTPUT_PATH)) { fs.mkdirSync(OUTPUT_PATH, { recursive: true }); } fs.writeFileSync(FILE_PATH, data); } const sortedResults = [...results].sort( (a, b) => b.result.requests.average - a.result.requests.average, ); const winner = sortedResults[0]; const ranking = sortedResults.map((item, index) => { const diff = ((item.result.requests.average - winner.result.requests.average) / winner.result.requests.average) * 100; return { rank: index + 1, framework: item.framework, requests: item.result.requests.average, latency: item.result.latency.average, throughput: item.result.throughput.average, difference: index === 0 ? "0%" : `${diff.toFixed(2)}%`, }; }); const leaderboard = { winner: winner.framework, runtime, machine, ranking, }; fs.writeFileSync( `results/comparison-${runtime}.json`, JSON.stringify(leaderboard, null, "\t"), ); } console.log(`\n${"-".repeat(40)}`); console.log("✅ All benchmarks completed!"); console.log(`${"-".repeat(40)}\n`); process.exit(0); } (async () => await main())(); ================================================ FILE: bench/tsconfig.json ================================================ { "extends": "../tsconfig.base.json", "compilerOptions": { "noEmit": true, "allowImportingTsExtensions": true }, "references": [{ "path": "../packages/kitojs" }] } ================================================ FILE: bench/utils/chart.ts ================================================ import { BarController, BarElement, CategoryScale, Chart, LinearScale, Title, Tooltip, Legend, } from "chart.js"; import { Canvas } from "skia-canvas"; import fs from "node:fs/promises"; Chart.register([ BarController, BarElement, CategoryScale, LinearScale, Title, Tooltip, Legend, ]); type ChartData = { frameworks: string[]; requests: number[]; latency: number[]; throughput: number[]; }; export async function generateChart( data: ChartData, outputPath: string, runtime?: string, ) { try { const canvas = new Canvas(1000, 600); const colors = [ "#3b82f6", // blue "#10b981", // green "#f59e0b", // amber "#8b5cf6", // purple "#ec4899", // pink "#06b6d4", // cyan ]; const chart = new Chart(canvas as any, { type: "bar", data: { labels: data.frameworks.map( (f) => f.charAt(0).toUpperCase() + f.slice(1), ), datasets: [ { label: "Requests/sec", data: data.requests, backgroundColor: "#3b82f6", borderRadius: 8, borderSkipped: false, }, { label: "Latency (ms)", data: data.latency, backgroundColor: "#10b981", borderRadius: 8, borderSkipped: false, yAxisID: "y1", }, ], }, options: { responsive: false, layout: { padding: { top: 20, bottom: 20, left: 20, right: 20, }, }, plugins: { legend: { display: true, position: "top", align: "end", labels: { color: "#1f2937", font: { size: 14, weight: 600, family: "'Inter', 'Segoe UI', 'Helvetica Neue', sans-serif", }, padding: 15, usePointStyle: true, pointStyle: "rectRounded", }, }, title: { display: true, text: runtime ? `Benchmark Results - ${runtime.toUpperCase()}` : "Benchmark Results", color: "#1f2937", font: { size: 28, weight: "bold", family: "'Inter', 'Segoe UI', 'Helvetica Neue', sans-serif", }, padding: { top: 10, bottom: 30, }, }, tooltip: { backgroundColor: "rgba(0, 0, 0, 0.8)", titleFont: { size: 14, weight: "bold", family: "'Inter', 'Segoe UI', 'Helvetica Neue', sans-serif", }, bodyFont: { size: 13, family: "'Inter', 'Segoe UI', 'Helvetica Neue', sans-serif", }, padding: 12, cornerRadius: 8, displayColors: false, callbacks: { label: (context) => { const label = context.dataset.label || ""; const value = context.parsed.y; if (label.includes("Latency")) { return `${label}: ${value?.toFixed(2)} ms`; } return `${label}: ${value?.toLocaleString()} req/s`; }, }, }, }, scales: { x: { grid: { display: false, }, ticks: { color: "#4b5563", font: { size: 16, weight: 600, family: "'Inter', 'Segoe UI', 'Helvetica Neue', sans-serif", }, padding: 10, }, border: { display: false, }, }, y: { beginAtZero: true, position: "left", grid: { color: "#e5e7eb", lineWidth: 1, }, ticks: { color: "#3b82f6", font: { size: 13, weight: 600, family: "'Inter', 'Segoe UI', 'Helvetica Neue', sans-serif", }, padding: 10, callback: (value) => { return value.toLocaleString(); }, }, border: { display: false, }, title: { display: true, text: "Requests/sec", color: "#3b82f6", font: { size: 13, weight: "bold", family: "'Inter', 'Segoe UI', 'Helvetica Neue', sans-serif", }, }, }, y1: { beginAtZero: true, position: "right", grid: { display: false, }, ticks: { color: "#10b981", font: { size: 13, weight: 600, family: "'Inter', 'Segoe UI', 'Helvetica Neue', sans-serif", }, padding: 10, callback: (value) => { let valueNumber = value as number; return valueNumber.toFixed(1); }, }, border: { display: false, }, title: { display: true, text: "Latency (ms)", color: "#10b981", font: { size: 13, weight: "bold", family: "'Inter', 'Segoe UI', 'Helvetica Neue', sans-serif", }, }, }, }, }, }); const dir = outputPath.substring(0, outputPath.lastIndexOf("/")); if (dir) { await fs.mkdir(dir, { recursive: true }); } const pngBuffer = await canvas.toBuffer("png", { matte: "white", density: 2, }); await fs.writeFile(outputPath, pngBuffer); console.log(`✅ Chart saved to ${outputPath}`); chart.destroy(); } catch (err) { console.error("Error generating chart:", err); } } ================================================ FILE: bench/utils/frameworkRunner.ts ================================================ const [, , benchName, frameworkName, portArg] = process.argv; if (!benchName || !frameworkName || !portArg) { console.error( "Missing arguments. Usage: frameworkRunner ", ); process.exit(1); } const port = Number(portArg); if (!Number.isFinite(port)) { console.error(`Invalid port provided to frameworkRunner: ${portArg}`); process.exit(1); } const mod = await import(`../cases/${benchName}/${frameworkName}.ts`); const bench = mod.default || mod; const instance = bench.start(port); async function shutdown(code = 0) { try { if (typeof instance?.stop === "function") { await instance.stop(); } } catch (error) { console.error("Failed to stop framework cleanly", error); } finally { process.exit(code); } } process.on("SIGTERM", () => void shutdown()); process.on("SIGINT", () => void shutdown()); process.on("uncaughtException", async (error) => { console.error(error); await shutdown(1); }); export {}; ================================================ FILE: bench/utils/http.ts ================================================ import { exec } from "node:child_process"; import config from "../config.ts"; const { duration, connections } = config; export type WrkResult = { requests: { average: number }; latency: { average: number }; throughput: { average: number }; }; export const runBenchmark = (url: string) => { return new Promise((resolve, reject) => { const cmd = `wrk -t${connections} -c${connections} -d${duration}s -H "Connection: keep-alive" ${url}`; exec(cmd, (err, stdout) => { if (err) { reject(err); return; } const output = stdout.toString(); const reqMatch = output.match(/Requests\/sec:\s*([\d.]+)/); const requests = reqMatch ? parseFloat(reqMatch[1]) : 0; const latMatch = output.match(/Latency\s+([\d.]+)(ms|s|us)/); let latency = 0; if (latMatch) { const v = parseFloat(latMatch[1]); const unit = latMatch[2]; if (unit === "s") latency = v * 1000; else if (unit === "us") latency = v / 1000; else latency = v; } const tpMatch = output.match(/Transfer\/sec:\s*([\d.]+)(KB|MB|B)/); let throughput = 0; if (tpMatch) { const v = parseFloat(tpMatch[1]); const unit = tpMatch[2]; if (unit === "MB") throughput = v * 1024 * 1024; else if (unit === "KB") throughput = v * 1024; else throughput = v; } resolve({ requests: { average: requests }, latency: { average: latency }, throughput: { average: throughput }, }); }); }); }; ================================================ FILE: bench/utils/wait.ts ================================================ import net from "node:net"; export function waitForServerReady(port: number, retries = 50): Promise { return new Promise((resolve, reject) => { (function check(retries: number) { const socket = net.createConnection(port, "localhost"); socket.on("connect", () => { socket.end(); resolve(); }); socket.on("error", () => { if (retries <= 0) return reject(new Error("Server not ready")); setTimeout(() => check(--retries), 50); }); })(retries); }); } ================================================ FILE: biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/2.2.3/schema.json", "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, "files": { "ignoreUnknown": false, "includes": ["**", "packages/**", "examples/**", "bench/**"] }, "formatter": { "enabled": true, "indentStyle": "space" }, "linter": { "enabled": true, "rules": { "recommended": true } }, "javascript": { "formatter": { "quoteStyle": "double" } }, "assist": { "enabled": true, "actions": { "source": { "organizeImports": "on" } } } } ================================================ FILE: cli/.gitignore ================================================ bin/ dist/ ================================================ FILE: cli/Cargo.toml ================================================ [package] name = "kito" # cli version = "0.1.0" edition = "2024" authors = ["Nehuén "] description = "🐺 Kito's official CLI." license = "MIT" repository = "https://github.com/kitojs/kito" readme = "readme.md" include = ["src/*", "Cargo.toml", "readme.md"] [[bin]] name = "kito" path = "src/main.rs" [profile.release] opt-level = "z" lto = true codegen-units = 1 panic = "abort" strip = true [dependencies] async-trait = "0.1.89" clap = { version = "4.5.47", features = ["derive"] } paste = "1.0.15" tokio = { version = "1.47.1", features = ["full"] } ================================================ FILE: cli/install.js ================================================ ================================================ FILE: cli/package.json ================================================ { "name": "@kitojs/cli", "version": "1.0.0", "description": "🍄 Kito's official CLI.", "bin": { "kito": "bin/kito" }, "scripts": { "postinstall": "node install.js" }, "keywords": [], "homepage": "https://github.com/kitojs/kito", "repository": { "type": "git", "url": "git+https://github.com/kitojs/kito.git" }, "bugs": { "url": "https://github.com/kitojs/kito/issues" }, "license": "MIT", "author": "Nehuén ", "main": "bin/kito", "os": [ "darwin", "linux", "win32" ], "cpu": [ "x64", "arm64" ], "files": [ "bin/", "install.js", "readme.md" ] } ================================================ FILE: cli/src/commands.rs ================================================ use crate::register_commands; use async_trait::async_trait; #[async_trait] pub(crate) trait Command { async fn run(&self) -> Result<(), ()>; } register_commands!(); ================================================ FILE: cli/src/main.rs ================================================ mod commands; mod utils; use async_trait::async_trait; use clap::{Parser, Subcommand}; use crate::commands::Command; #[derive(Parser)] #[command( name = "kito", version = env!("CARGO_PKG_VERSION"), about = env!("CARGO_PKG_DESCRIPTION") )] #[command(arg_required_else_help = true)] struct Cli { #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Commands {} #[async_trait] impl Command for Commands { async fn run(&self) -> Result<(), ()> { // match self {} Ok(()) } } #[tokio::main] async fn main() { let cli = Cli::parse(); if let Some(cmd) = cli.command { let cmd_run = cmd.run().await; if cmd_run.is_err() { std::process::exit(1); } } } ================================================ FILE: cli/src/utils.rs ================================================ #[macro_export] macro_rules! register_commands { ($($name:ident),*) => { $( pub(crate) mod $name; paste::paste! { pub(crate) use $name::[<$name:camel Command>]; } )* } } ================================================ FILE: cliff.toml ================================================ [changelog] body = """ {% if version %}\ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} {% else %}\ ## [unreleased] {% endif %}\ {% for group, commits in commits | group_by(attribute="group") %} ### {{ group | striptags | trim | upper_first }} {% for commit in commits %} - {% if commit.scope %}*({{ commit.scope }})* {% endif %}{% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }} {% endfor %} {% endfor %} """ trim = true render_always = true postprocessors = [ { pattern = '', replace = "https://github.com/kitojs/kito" }, ] [git] conventional_commits = true filter_unconventional = true require_conventional = false split_commits = false commit_preprocessors = [ { pattern = '\\((\\w+\\s)?#([0-9]+)\\)', replace = "([#${2}](/issues/${2}))" }, { pattern = '.*', replace_command = 'typos --write-changes -' }, ] protect_breaking_commits = false commit_parsers = [ { message = "^feat", group = "🚀 Features" }, { message = "^fix", group = "🐛 Bug Fixes" }, { message = "^perf", group = "⚡ Performance" }, { message = "^refactor", group = "🚜 Refactor" }, { message = "^style", group = "🎨 Styling" }, { message = "^test", group = "🧪 Testing" }, { message = "^doc", group = "📚 Documentation" }, { message = "^chore\\(release\\): prepare for", skip = true }, { message = "^chore\\(deps.*\\)", skip = true }, { message = "^chore\\(pr\\)", skip = true }, { message = "^chore\\(pull\\)", skip = true }, { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, { body = ".*security", group = "🛡️ Security" }, { message = "^revert", group = "◀️ Revert" }, { message = ".*", group = "💼 Other" }, ] filter_commits = false link_parsers = [] use_branch_tags = false topo_order = false topo_order_commits = true sort_commits = "oldest" recurse_submodules = false ================================================ FILE: examples/fluent/basic.ts ================================================ import { server } from "kitojs"; server() .get("/", ({ res }) => res.send("hello world!")) .listen(3000); ================================================ FILE: examples/fluent/clustering/index.ts ================================================ import cluster from "node:cluster"; import { availableParallelism } from "node:os"; if (cluster.isPrimary) { const numCPUs = availableParallelism(); console.log(`Primary ${process.pid} is running`); console.log(`Starting ${numCPUs} workers...`); for (let i = 0; i < numCPUs; i++) cluster.fork(); } else { import("./server"); } ================================================ FILE: examples/fluent/clustering/server.ts ================================================ import { server } from "kitojs"; server({ reusePort: true, // this is required for clustering }) .get("/", ({ res }) => res.send("hello world!")) .listen(3000, () => { console.log(`Worker ${process.pid} listening on port 3000`); }); ================================================ FILE: examples/fluent/extend.ts ================================================ import { server } from "kitojs"; interface Extends { user: { id: string; name: string }; } server() .extend((ctx) => { ctx.user = { id: "1", name: "Neo" }; }) .get("/", ({ res, user }) => { res.json({ user }); }) .listen(3000); ================================================ FILE: examples/fluent/file.ts ================================================ import { server } from "kitojs"; server() .get("/download", ({ res }) => { res.sendFile("public/banner.png", { root: "./", maxAge: 3600, lastModified: true, cacheControl: true, immutable: false, }); }) .get("/export", ({ res }) => { res.download("public/banner.png", "kito-banner.png"); }) .listen(3000); ================================================ FILE: examples/fluent/middlewares/index.ts ================================================ import { server } from "kitojs"; import { auth } from "./samples/auth"; import { logger } from "./samples/logger"; server() .use(logger) .get("/", auth, ({ res }) => res.send("hello world!")) .listen(3000); ================================================ FILE: examples/fluent/middlewares/samples/auth.ts ================================================ import { middleware } from "kitojs"; export const auth = middleware((ctx, next) => { const { req, res } = ctx; const header = req.headers.authorization; if (header && header === "Bearer secret_token") { return next(); } res.status(401).send("unauthorized"); }); ================================================ FILE: examples/fluent/middlewares/samples/logger.ts ================================================ import { middleware } from "kitojs"; export const logger = middleware((ctx, next) => { const { method, url } = ctx.req; const date = new Date().toISOString(); console.log(`[${date}] ${method} ${url}`); return next(); }); ================================================ FILE: examples/fluent/params.ts ================================================ import { server } from "kitojs"; server() .get("/:id", ({ req, res }) => res.send(req.params.id)) .get("/multi/:id/:name", ({ req, res }) => { const { id, name } = req.params; res.json({ id, name }); }) .listen(3000); ================================================ FILE: examples/fluent/route.ts ================================================ import { server } from "kitojs"; server() .route("/") .get(({ res }) => { res.send("hello world!"); }) .post(({ req, res }) => { res.json({ body: req.body }); }) .end() .get("/bye", ({ res }) => { res.send("bye!"); }) .listen(3000); ================================================ FILE: examples/fluent/router/cats.ts ================================================ import { router } from "kitojs"; export default router() .get("/", ({ res }) => { res.send("hello cats!"); }) .post("/", ({ res }) => { res.json({ msg: "cat created!" }); }); ================================================ FILE: examples/fluent/router/index.ts ================================================ import { server } from "kitojs"; import cats from "./cats"; server() .mount("/cats", cats) .get("/", ({ res }) => res.send("hello world!")) .listen(3000); ================================================ FILE: examples/fluent/schemas/builder.ts ================================================ import { schema, server, t } from "kitojs"; const userSchema = schema({ params: t.object({ id: t.str().uuid() }), query: t.object({ limit: t.num().max(100) }), body: t.object({ name: t.str().min(1) }), }); server() .post( "/users:id", ({ req, res }) => { const { id } = req.params; const { limit } = req.query; const { name } = req.body; res.json({ id, limit, name }); }, userSchema, ) .listen(3000); ================================================ FILE: examples/fluent/schemas/json.ts ================================================ import { schema, server } from "kitojs"; const userSchema = schema.json({ params: { type: "object", properties: { id: { type: "string", format: "uuid" }, }, required: ["id"], }, query: { type: "object", properties: { limit: { type: "number", maximum: 100 }, }, required: ["limit"], }, body: { type: "object", properties: { name: { type: "string", minLength: 1 }, }, required: ["name"], }, }); server() .post( "/users/:id", ({ req, res }) => { // types are automatically inferred from userSchema const { id } = req.params; const { limit } = req.query; const { name } = req.body; res.json({ id, limit, name }); }, userSchema, ) .listen(3000); ================================================ FILE: examples/fluent/streaming/sse.ts ================================================ import { server } from "kitojs"; server() .get("/", ({ res }) => { const sse = res.sse(); sse.send("hello world!"); sse.send( { msg: "this is a message", }, "message", ); sse.close(); }) .listen(3000); ================================================ FILE: examples/fluent/streaming/stream.ts ================================================ import { server } from "kitojs"; server() .get("/", ({ res }) => { const stream = res.stream(); stream.write("Chunk 1\n"); setTimeout(() => { stream.write("Chunk 2\n"); }, 1000); setTimeout(() => { stream.end("Final chunk\n"); }, 2000); }) .listen(3000); ================================================ FILE: examples/fluent/unixSocket.ts ================================================ import { server } from "kitojs"; server() .get("/", ({ res }) => { res.send("Hello from Unix socket!"); }) .listen({ unixSocket: "./app.sock" }); ================================================ FILE: examples/instance/basic.ts ================================================ import { server } from "kitojs"; const app = server(); app.get("/", ({ res }) => { res.send("hello world!"); }); app.listen(3000); ================================================ FILE: examples/instance/clustering/index.ts ================================================ import cluster from "node:cluster"; import { availableParallelism } from "node:os"; if (cluster.isPrimary) { const numCPUs = availableParallelism(); console.log(`Primary ${process.pid} is running`); console.log(`Starting ${numCPUs} workers...`); for (let i = 0; i < numCPUs; i++) cluster.fork(); } else { import("./server"); } ================================================ FILE: examples/instance/clustering/server.ts ================================================ import { server } from "kitojs"; const app = server({ reusePort: true, // this is required for clustering }); app.get("/", ({ res }) => res.send("hello world!")); app.listen(3000, () => { console.log(`Worker ${process.pid} listening on port 3000`); }); ================================================ FILE: examples/instance/extend.ts ================================================ import { server } from "kitojs"; interface Extends { user: { id: string; name: string }; } const app = server().extend((ctx) => { ctx.user = { id: "1", name: "Neo" }; }); app.get("/", ({ res, user }) => { res.json({ user }); }); app.listen(3000); ================================================ FILE: examples/instance/file.ts ================================================ import { server } from "kitojs"; const app = server(); app.get("/download", ({ res }) => { res.sendFile("public/banner.png", { root: "./", maxAge: 3600, lastModified: true, cacheControl: true, immutable: false, etag: true, }); }); app.get("/export", ({ res }) => { res.download("public/banner.png", "kito-banner.png"); }); app.listen(3000); ================================================ FILE: examples/instance/middlewares/index.ts ================================================ import { server } from "kitojs"; import { auth } from "./samples/auth"; import { logger } from "./samples/logger"; const app = server(); app.use(logger); app.get("/", auth, ({ res }) => { res.send("hello world!"); }); app.listen(3000); ================================================ FILE: examples/instance/middlewares/samples/auth.ts ================================================ import { middleware } from "kitojs"; export const auth = middleware((ctx, next) => { const { req, res } = ctx; const header = req.headers.authorization; if (header && header === "Bearer secret_token") { return next(); } res.status(401).send("unauthorized"); }); ================================================ FILE: examples/instance/middlewares/samples/logger.ts ================================================ import { middleware } from "kitojs"; export const logger = middleware((ctx, next) => { const { method, url } = ctx.req; const date = new Date().toISOString(); console.log(`[${date}] ${method} ${url}`); return next(); }); ================================================ FILE: examples/instance/params.ts ================================================ import { server } from "kitojs"; const app = server(); app.get("/:id", ({ req, res }) => { res.send(req.params.id); }); app.get("/multi/:id/:name", ({ req, res }) => { const { id, name } = req.params; res.json({ id, name }); }); app.listen(3000); ================================================ FILE: examples/instance/route.ts ================================================ import { server } from "kitojs"; const app = server(); const routes = app.route("/"); routes.get(({ res }) => { res.send("hello world!"); }); routes.post(({ req, res }) => { res.json({ body: req.body }); }); app.get("/bye", ({ res }) => { res.send("bye!"); }); app.listen(3000); ================================================ FILE: examples/instance/router/cats.ts ================================================ import { router } from "kitojs"; const cats = router(); cats.get("/", ({ res }) => { res.send("hello cats!"); }); cats.post("/", ({ res }) => { res.json({ msg: "cat created!" }); }); export default cats; ================================================ FILE: examples/instance/router/index.ts ================================================ import { server } from "kitojs"; import cats from "./cats"; const app = server(); app.mount("/cats", cats); app.get("/", ({ res }) => { res.send("hello world!"); }); app.listen(3000); ================================================ FILE: examples/instance/schemas/builder.ts ================================================ import { schema, server, t } from "kitojs"; const app = server(); const userSchema = schema({ params: t.object({ id: t.str().uuid() }), query: t.object({ limit: t.num().max(100) }), body: t.object({ name: t.str().min(1) }), }); app.post( "/users/:id", ({ req, res }) => { // types are automatically inferred from userSchema const { id } = req.params; const { limit } = req.query; const { name } = req.body; res.json({ id, limit, name }); }, userSchema, ); app.listen(3000); ================================================ FILE: examples/instance/schemas/json.ts ================================================ import { schema, server } from "kitojs"; const app = server(); const userSchema = schema.json({ params: { type: "object", properties: { id: { type: "string", format: "uuid" }, }, required: ["id"], }, query: { type: "object", properties: { limit: { type: "number", maximum: 100 }, }, required: ["limit"], }, body: { type: "object", properties: { name: { type: "string", minLength: 1 }, }, required: ["name"], }, }); app.post( "/users/:id", ({ req, res }) => { // types are automatically inferred from userSchema const { id } = req.params; const { limit } = req.query; const { name } = req.body; res.json({ id, limit, name }); }, userSchema, ); app.listen(3000); ================================================ FILE: examples/instance/streaming/sse.ts ================================================ import { server } from "kitojs"; const app = server(); app.get("/", ({ res }) => { const sse = res.sse(); sse.send("hello world!"); sse.send( { msg: "this is a message", }, "message", ); sse.close(); }); app.listen(3000); ================================================ FILE: examples/instance/streaming/stream.ts ================================================ import { server } from "kitojs"; const app = server(); app.get("/", ({ res }) => { const stream = res.stream(); stream.write("Chunk 1\n"); setTimeout(() => { stream.write("Chunk 2\n"); }, 1000); setTimeout(() => { stream.end("Final chunk\n"); }, 2000); }); app.listen(3000); ================================================ FILE: examples/instance/unixSocket.ts ================================================ import { server } from "kitojs"; const app = server(); app.get("/", ({ res }) => { res.send("Hello from Unix socket!"); }); app.listen({ unixSocket: "./app.sock" }); ================================================ FILE: examples/package.json ================================================ { "name": "@kitojs/examples", "description": "Kito's usage examples.", "private": true, "type": "module", "scripts": { "ex:run": "tsx runExample.ts" }, "dependencies": { "kitojs": "workspace:*" }, "devDependencies": { "tsx": "^4.20.6", "typescript": "^5.9.2" } } ================================================ FILE: examples/runExample.ts ================================================ #!/usr/bin/env tsx import { existsSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const args = process.argv.slice(2); let style: string | undefined; let example: string | undefined; for (let i = 0; i < args.length; i++) { if (args[i] === "--style" && i + 1 < args.length) { style = args[i + 1]; i++; } else if (args[i] === "--example" && i + 1 < args.length) { example = args[i + 1]; i++; } } if (!style || !example) { console.error( "Usage: pnpm ex:run --style --example ", ); console.error("\nExamples:"); console.error(" pnpm ex:run --style fluent --example basic"); console.error(" pnpm ex:run --style fluent --example router"); console.error(" pnpm ex:run --style fluent --example schemas/builder"); process.exit(1); } if (style !== "fluent" && style !== "instance") { console.error(`Invalid style: ${style}. Must be 'fluent' or 'instance'.`); process.exit(1); } let filePath: string; const dirWithIndex = resolve(__dirname, style, example, "index.ts"); if (existsSync(dirWithIndex)) { filePath = dirWithIndex; } else { const directFile = resolve(__dirname, style, `${example}.ts`); if (existsSync(directFile)) { filePath = directFile; } else { console.error(`Example not found: ${style}/${example}`); console.error(`Tried: ${dirWithIndex} and ${directFile}`); process.exit(1); } } console.log(`Running example: ${style}/${example}`); import(filePath); ================================================ FILE: examples/tsconfig.json ================================================ { "extends": "../tsconfig.base.json", "compilerOptions": {}, "include": ["instance/**/*", "fluent/**/*"], "references": [ { "path": "../packages/kitojs" } ] } ================================================ FILE: lefthook.yml ================================================ skip_output: - meta - summary pre-commit: parallel: true commands: # Rust commands fmt-check: tags: rust formatter run: cargo fmt --all -- --check fail_text: "Code is not formatted. Run 'cargo fmt' to fix." clippy: tags: rust linter run: cargo clippy --all-targets --all-features -- -D warnings fail_text: "Linting failed. Fix clippy warnings." check: tags: rust compile run: cargo check --all-targets --all-features fail_text: "Code doesn't compile. Fix compilation errors." # TypeScript commands biome-check: tags: js formatter linter glob: "*.{js,ts,tsx,json,jsonc}" run: pnpm biome check --write {staged_files} stage_fixed: true fail_text: "JS/TS formatting/linting failed. Fix biome issues." # ts-typecheck: # tags: typescript checker # glob: "*.{ts,tsx}" # run: pnpm tsc --noEmit # fail_text: "TypeScript type checking failed. Fix type errors." pre-push: parallel: false commands: # Rust tests test: tags: rust test run: cargo test --all-features fail_text: "Rust tests failed. Fix failing tests before pushing." # Security audits audit: tags: rust security run: cargo audit fail_text: "Security vulnerabilities found. Run 'cargo audit fix' or update dependencies." skip: - merge - rebase npm-audit: tags: npm security run: pnpm audit --audit-level moderate fail_text: "NPM security vulnerabilities found. Run 'pnpm audit --fix'." skip: - merge - rebase prepare-commit-msg: commands: conventional-commits-prepare: tags: commit-msg run: | # This hook runs before the commit message is created # We can use it to prepare or validate the template echo "📝 Preparing commit message validation..." echo "Remember to use Conventional Commits format: [optional scope]: " commit-msg: commands: conventional-commits: tags: commit-msg run: | #!/bin/sh # Try to extract commit message commit_msg="" # 1. Argument (standard for commit-msg hook) if [ -n "$1" ] && [ -f "$1" ]; then commit_msg="$(head -n 1 "$1" | tr -d '\r\n')" fi # 2. Lefthook-provided variable if [ -z "$commit_msg" ] && [ -n "$LEFTHOOK_COMMIT_MSG" ]; then commit_msg="$(echo "$LEFTHOOK_COMMIT_MSG" | head -n 1 | tr -d '\r\n')" fi # 3. Git fallback if [ -z "$commit_msg" ] && [ -f ".git/COMMIT_EDITMSG" ]; then commit_msg="$(head -n 1 .git/COMMIT_EDITMSG | tr -d '\r\n')" fi # Skip empty or comment-only messages if [ -z "$commit_msg" ] || echo "$commit_msg" | grep -qE '^#'; then echo "⚠️ Empty commit message or comment, skipping validation." exit 0 fi echo "🔍 Validating commit message: \"$commit_msg\"" # Check Conventional Commit pattern if ! echo "$commit_msg" | grep -qE '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([^)]+\))?: .{1,50}$'; then echo "❌ Invalid commit message format!" echo "Format: [optional scope]: " echo "Types: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test" echo "Example: feat(auth): add login functionality" echo "Your message: \"$commit_msg\"" exit 1 fi # Length check if [ "${#commit_msg}" -gt 72 ]; then echo "❌ Commit message too long! Keep it under 72 characters." echo "Length: ${#commit_msg}" echo "Your message: \"$commit_msg\"" exit 1 fi # Imperative mood check description="$(echo "$commit_msg" | sed -E 's/^[a-z]+(\([^)]+\))?: //')" if echo "$description" | grep -qE '^(added|fixed|updated|changed|removed|created)'; then echo "⚠️ Use imperative mood (add, fix, update), not past tense" echo "Example: 'feat: add auth' not 'feat: added auth'" echo "Your message: \"$commit_msg\"" exit 1 fi echo "✅ Commit message is valid!" fail_text: "Commit message doesn't follow Conventional Commits specification." post-checkout: commands: cargo-update: tags: rust dependencies files: git diff --name-only HEAD@{1} HEAD glob: "{Cargo.toml,Cargo.lock}" run: | echo "🔄 Cargo files changed, updating dependencies..." cargo check skip: - merge - rebase npm-install: tags: npm dependencies files: git diff --name-only HEAD@{1} HEAD glob: "{package.json,pnpm-lock.yaml,pnpm-workspace.yaml}" run: | echo "🔄 NPM files changed, updating dependencies..." pnpm install skip: - merge - rebase post-merge: commands: cargo-check: tags: rust post-merge run: | echo "🔄 Running cargo check after merge..." cargo check echo "✅ Rust dependencies updated successfully" npm-check: tags: npm post-merge run: | echo "🔄 Running npm check after merge..." pnpm install --frozen-lockfile echo "✅ NPM dependencies updated successfully" colors: true no_tty: false source_dir: ".lefthook" source_dir_local: ".lefthook-local" output: - execution - execution_out - execution_info - skips ================================================ FILE: license ================================================ MIT License Copyright (c) 2024 kitojs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: package.json ================================================ { "name": "kito", "version": "1.0.0", "description": "The Kito's workspace.", "scripts": { "fmt": "biome format", "fmt:fix": "biome format --write", "lint": "biome lint", "lint:fix": "biome lint --write", "check": "biome check", "check:fix": "biome check --write", "build": "pnpm core:build --platform && pnpm ts:build", "core:build": "pnpm --filter @kitojs/kito-core build", "ts:build": "pnpm --filter @kitojs/types build && pnpm --filter kitojs build", "dev": "pnpm -r dev", "test": "pnpm -r test", "ex:run": "pnpm --filter examples ex:run", "bench:run": "pnpm --filter bench bench:run" }, "author": "Nehuén ", "license": "MIT", "devDependencies": { "@biomejs/biome": "2.2.3", "@types/node": "^24.5.2", "typescript": "^5.9.2" } } ================================================ FILE: packages/core/.gitignore ================================================ dist/ ================================================ FILE: packages/core/Cargo.toml ================================================ [package] name = "core" version = "0.1.0" edition = "2024" [lib] name = "core" crate-type = ["cdylib"] [dependencies] napi = { version = "3.2.4", default-features = false, features = ["async"] } napi-derive = "3.2.4" hyper = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] } http-body-util = "0.1" hyper-util = { version = "0.1", features = ["full"] } once_cell = "1.21.3" dashmap = "6.1.0" urlencoding = "2.1.3" httpdate = "1.0.3" parking_lot = "0.12.5" serde = { version = "1.0.228", features = ["derive"] } regex = "1.12.2" serde_json = "1.0.145" matchit = "0.9.0" ahash = "0.8.12" base64 = "0.22.1" socket2 = "0.6.1" futures-util = "0.3.31" [dev-dependencies] tokio-test = "0.4.4" [build-dependencies] napi-build = "2.2.3" ================================================ FILE: packages/core/build.rs ================================================ fn main() { napi_build::setup(); } ================================================ FILE: packages/core/package.json ================================================ { "name": "@kitojs/kito-core", "version": "1.0.0-alpha.8", "description": "Internal logic (core) of the Kito framework written in Rust.", "scripts": { "build": "napi build --release --esm --const-enum --output-dir dist", "test": "cargo test" }, "dependencies": {}, "devDependencies": { "@napi-rs/cli": "^3.1.5" }, "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }, "files": [ "dist" ], "napi": { "binaryName": "core", "packageName": "@kitojs/kito-core", "targets": [ "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", "x86_64-unknown-linux-musl", "aarch64-unknown-linux-gnu", "i686-pc-windows-msvc", "armv7-unknown-linux-gnueabihf", "aarch64-linux-android", "x86_64-unknown-freebsd", "aarch64-unknown-linux-musl", "aarch64-pc-windows-msvc", "armv7-linux-androideabi" ] }, "engines": { "node": ">= 10" } } ================================================ FILE: packages/core/src/http/cookies.rs ================================================ #[derive(Clone, Default)] #[napi(object)] pub struct CookieOptionsCore { pub domain: Option, pub http_only: Option, pub max_age: Option, pub path: Option, pub secure: Option, pub signed: Option, pub same_site: Option, } pub fn serialize_cookie(name: &str, value: &str, options: &CookieOptionsCore) -> String { let mut cookie = format!("{name}={value}"); if let Some(max_age) = options.max_age { cookie.push_str(&format!("; Max-Age={max_age}")); } if let Some(ref path) = options.path { cookie.push_str(&format!("; Path={path}")); } else { cookie.push_str("; Path=/"); } if let Some(ref domain) = options.domain { cookie.push_str(&format!("; Domain={domain}")); } if options.http_only.unwrap_or(false) { cookie.push_str("; HttpOnly"); } if options.secure.unwrap_or(false) { cookie.push_str("; Secure"); } if let Some(ref same_site) = options.same_site { cookie.push_str(&format!("; SameSite={same_site}")); } cookie } ================================================ FILE: packages/core/src/http/cookies_tests.rs ================================================ #[cfg(test)] mod tests { use super::super::cookies::*; #[test] fn test_basic_cookie() { let options = CookieOptionsCore { domain: None, http_only: None, max_age: None, path: None, secure: None, signed: None, same_site: None, }; let result = serialize_cookie("session", "abc123", &options); assert!(result.contains("session=abc123")); assert!(result.contains("Path=/")); } #[test] fn test_cookie_with_max_age() { let options = CookieOptionsCore { domain: None, http_only: None, max_age: Some(3600), path: None, secure: None, signed: None, same_site: None, }; let result = serialize_cookie("token", "xyz", &options); assert!(result.contains("Max-Age=3600")); } #[test] fn test_cookie_with_custom_path() { let options = CookieOptionsCore { domain: None, http_only: None, max_age: None, path: Some("/admin".to_string()), secure: None, signed: None, same_site: None, }; let result = serialize_cookie("admin", "secret", &options); assert!(result.contains("Path=/admin")); } #[test] fn test_cookie_with_domain() { let options = CookieOptionsCore { domain: Some("example.com".to_string()), http_only: None, max_age: None, path: None, secure: None, signed: None, same_site: None, }; let result = serialize_cookie("user", "john", &options); assert!(result.contains("Domain=example.com")); } #[test] fn test_cookie_http_only() { let options = CookieOptionsCore { domain: None, http_only: Some(true), max_age: None, path: None, secure: None, signed: None, same_site: None, }; let result = serialize_cookie("secure", "data", &options); assert!(result.contains("HttpOnly")); } #[test] fn test_cookie_secure() { let options = CookieOptionsCore { domain: None, http_only: None, max_age: None, path: None, secure: Some(true), signed: None, same_site: None, }; let result = serialize_cookie("token", "value", &options); assert!(result.contains("Secure")); } #[test] fn test_cookie_same_site() { let options = CookieOptionsCore { domain: None, http_only: None, max_age: None, path: None, secure: None, signed: None, same_site: Some("Strict".to_string()), }; let result = serialize_cookie("csrf", "token", &options); assert!(result.contains("SameSite=Strict")); } #[test] fn test_cookie_all_options() { let options = CookieOptionsCore { domain: Some("example.com".to_string()), http_only: Some(true), max_age: Some(7200), path: Some("/api".to_string()), secure: Some(true), signed: None, same_site: Some("Lax".to_string()), }; let result = serialize_cookie("full", "options", &options); assert!(result.contains("full=options")); assert!(result.contains("Max-Age=7200")); assert!(result.contains("Path=/api")); assert!(result.contains("Domain=example.com")); assert!(result.contains("HttpOnly")); assert!(result.contains("Secure")); assert!(result.contains("SameSite=Lax")); } } ================================================ FILE: packages/core/src/http/files.rs ================================================ use hyper::body::Bytes; use std::{collections::HashMap, time::UNIX_EPOCH}; use tokio::{fs::File, io::AsyncReadExt}; use crate::http::mime::get_mime_type; use crate::http::response::SendFileOptionsCore; pub async fn read_file_for_response( path: &str, options: Option, ) -> Result<(Bytes, HashMap), std::io::Error> { let full_path = if let Some(ref opts) = options { if let Some(ref root) = opts.root { format!("{root}/{path}") } else { path.to_string() } } else { path.to_string() }; let mut file = File::open(&full_path).await?; let metadata = file.metadata().await?; let mut contents = Vec::new(); file.read_to_end(&mut contents).await?; let mut headers = HashMap::new(); headers.insert("Content-Type".to_string(), get_mime_type(path).to_string()); headers.insert("Content-Length".to_string(), metadata.len().to_string()); if let Some(opts) = options { if let Some(ref custom_headers) = opts.headers { headers.extend(custom_headers.clone()); } if opts.last_modified.unwrap_or(true) && let Ok(modified) = metadata.modified() && let Ok(duration) = modified.duration_since(UNIX_EPOCH) { headers.insert( "Last-Modified".to_string(), httpdate::fmt_http_date(UNIX_EPOCH + duration), ); } if opts.cache_control.unwrap_or(true) && let Some(max_age) = opts.max_age { let cache_value = if opts.immutable.unwrap_or(false) { format!("public, max-age={max_age}, immutable") } else { format!("public, max-age={max_age}") }; headers.insert("Cache-Control".to_string(), cache_value); } if opts.accept_ranges.unwrap_or(true) { headers.insert("Accept-Ranges".to_string(), "bytes".to_string()); } } Ok((Bytes::from(contents), headers)) } ================================================ FILE: packages/core/src/http/mime.rs ================================================ use std::path::Path; pub fn get_mime_type(path: &str) -> &'static str { let extension = Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or(""); match extension.to_lowercase().as_str() { "html" | "htm" => "text/html", "css" => "text/css", "js" | "mjs" => "application/javascript", "json" => "application/json", "xml" => "application/xml", "txt" => "text/plain", "png" => "image/png", "jpg" | "jpeg" => "image/jpeg", "gif" => "image/gif", "svg" => "image/svg+xml", "webp" => "image/webp", "ico" => "image/x-icon", "pdf" => "application/pdf", "zip" => "application/zip", "wasm" => "application/wasm", "mp4" => "video/mp4", "webm" => "video/webm", "mp3" => "audio/mpeg", "wav" => "audio/wav", "woff" | "woff2" => "font/woff2", "ttf" => "font/ttf", "otf" => "font/otf", _ => "application/octet-stream", } } ================================================ FILE: packages/core/src/http/mime_tests.rs ================================================ #[cfg(test)] mod tests { use super::super::mime::get_mime_type; #[test] fn test_html_mime_types() { assert_eq!(get_mime_type("index.html"), "text/html"); assert_eq!(get_mime_type("page.htm"), "text/html"); } #[test] fn test_css_mime_type() { assert_eq!(get_mime_type("styles.css"), "text/css"); } #[test] fn test_javascript_mime_types() { assert_eq!(get_mime_type("app.js"), "application/javascript"); assert_eq!(get_mime_type("module.mjs"), "application/javascript"); } #[test] fn test_json_mime_type() { assert_eq!(get_mime_type("data.json"), "application/json"); } #[test] fn test_image_mime_types() { assert_eq!(get_mime_type("photo.png"), "image/png"); assert_eq!(get_mime_type("photo.jpg"), "image/jpeg"); assert_eq!(get_mime_type("photo.jpeg"), "image/jpeg"); assert_eq!(get_mime_type("icon.gif"), "image/gif"); assert_eq!(get_mime_type("logo.svg"), "image/svg+xml"); assert_eq!(get_mime_type("image.webp"), "image/webp"); } #[test] fn test_font_mime_types() { assert_eq!(get_mime_type("font.woff"), "font/woff2"); assert_eq!(get_mime_type("font.woff2"), "font/woff2"); assert_eq!(get_mime_type("font.ttf"), "font/ttf"); assert_eq!(get_mime_type("font.otf"), "font/otf"); } #[test] fn test_video_mime_types() { assert_eq!(get_mime_type("video.mp4"), "video/mp4"); assert_eq!(get_mime_type("video.webm"), "video/webm"); } #[test] fn test_audio_mime_types() { assert_eq!(get_mime_type("song.mp3"), "audio/mpeg"); assert_eq!(get_mime_type("sound.wav"), "audio/wav"); } #[test] fn test_default_mime_type() { assert_eq!(get_mime_type("file.unknown"), "application/octet-stream"); assert_eq!(get_mime_type("no_extension"), "application/octet-stream"); } #[test] fn test_case_insensitive() { assert_eq!(get_mime_type("FILE.HTML"), "text/html"); assert_eq!(get_mime_type("IMAGE.PNG"), "image/png"); assert_eq!(get_mime_type("Script.JS"), "application/javascript"); } #[test] fn test_path_with_directories() { assert_eq!(get_mime_type("/path/to/file.html"), "text/html"); assert_eq!(get_mime_type("../assets/image.png"), "image/png"); } } ================================================ FILE: packages/core/src/http/request.rs ================================================ use http_body_util::BodyExt; use hyper::{ Request, body::{Bytes, Incoming}, }; use napi::bindgen_prelude::{Buffer, External}; use std::{collections::HashMap, net::SocketAddr, sync::Arc}; #[derive(Clone)] pub struct RequestCore { pub method: String, pub url: String, pub pathname: String, pub search: Option, pub protocol: String, pub hostname: String, pub original_url: String, pub secure: bool, pub xhr: bool, pub ip: String, pub ips: Vec, pub body: Bytes, pub headers_raw: HashMap, pub params: HashMap, pub query_raw: HashMap>, pub cookies_raw: HashMap, } impl RequestCore { pub async fn new( req: Request, remote_addr: Option, trust_proxy: bool, ) -> Result> { let method = req.method().as_str().to_string(); let uri = req.uri(); let url = uri.to_string(); let pathname = uri.path().to_string(); let search = uri.query().map(|q| format!("?{q}")); let original_url = url.clone(); let mut headers_raw = HashMap::with_capacity(req.headers().len()); for (name, value) in req.headers() { if let Ok(v) = value.to_str() { headers_raw.insert(name.as_str().to_string(), v.to_string()); } } let mut query_raw: HashMap> = HashMap::new(); if let Some(q) = uri.query() { for pair in q.split('&') { if let Some((key, value)) = pair.split_once('=') { let decoded_key = urlencoding::decode(key).unwrap_or_default().into_owned(); let decoded_value = urlencoding::decode(value).unwrap_or_default().into_owned(); query_raw.entry(decoded_key).or_default().push(decoded_value); } } } let scheme = req.uri().scheme_str().unwrap_or("http").to_string(); let body = req.into_body().collect().await?.to_bytes(); let protocol = if trust_proxy { headers_raw .get("x-forwarded-proto") .map(|s| s.to_string()) .unwrap_or_else(|| "http".to_string()) } else { scheme }; let secure = protocol.eq_ignore_ascii_case("https"); let hostname = headers_raw.get("host").cloned().unwrap_or_else(|| "localhost".to_string()); let cookies_raw = headers_raw.get("cookie").map_or(HashMap::new(), |cookie_str| { cookie_str .split(';') .filter_map(|c| { let parts: Vec<&str> = c.trim().splitn(2, '=').collect(); if parts.len() == 2 { Some((parts[0].to_string(), parts[1].to_string())) } else { None } }) .collect() }); let ips: Vec = if trust_proxy { headers_raw .get("x-forwarded-for") .map(|s| s.split(',').map(|ip| ip.trim().to_string()).collect()) .unwrap_or_default() } else { vec![] }; let ip = ips .first() .cloned() .or_else(|| remote_addr.map(|a| a.ip().to_string())) .unwrap_or_default(); let xhr = headers_raw.get("x-requested-with").map(|v| v == "XMLHttpRequest").unwrap_or(false); Ok(Self { method, url, pathname, search, protocol, hostname, original_url, secure, xhr, ip, ips, body, headers_raw, params: HashMap::new(), query_raw, cookies_raw, }) } } #[napi] pub fn get_body_buffer(core: &External>) -> Buffer { Buffer::from(core.body.as_ref()) } #[napi] pub fn get_header(core: &External>, name: String) -> Option { core.headers_raw.get(&name.to_lowercase()).cloned() } #[napi] pub fn get_all_headers(core: &External>) -> HashMap { core.headers_raw.clone() } #[napi] pub fn get_query_param(core: &External>, name: String) -> Option> { core.query_raw.get(&name).cloned() } #[napi] pub fn get_all_query(core: &External>) -> HashMap> { core.query_raw.clone() } #[napi] pub fn get_param(core: &External>, name: String) -> Option { core.params.get(&name).cloned() } #[napi] pub fn get_all_params(core: &External>) -> HashMap { core.params.clone() } #[napi] pub fn get_cookie(core: &External>, name: String) -> Option { core.cookies_raw.get(&name).cloned() } #[napi] pub fn get_all_cookies(core: &External>) -> HashMap { core.cookies_raw.clone() } #[napi] pub fn get_method(core: &External>) -> String { core.method.clone() } #[napi] pub fn get_url(core: &External>) -> String { core.url.clone() } #[napi] pub fn get_pathname(core: &External>) -> String { core.pathname.clone() } #[napi] pub fn get_search(core: &External>) -> Option { core.search.clone() } #[napi] pub fn get_protocol(core: &External>) -> String { core.protocol.clone() } #[napi] pub fn get_hostname(core: &External>) -> String { core.hostname.clone() } #[napi] pub fn get_ip(core: &External>) -> String { core.ip.clone() } #[napi] pub fn get_ips(core: &External>) -> Vec { core.ips.clone() } #[napi] pub fn get_secure(core: &External>) -> bool { core.secure } #[napi] pub fn get_xhr(core: &External>) -> bool { core.xhr } ================================================ FILE: packages/core/src/http/response.rs ================================================ use http_body_util::{Full, StreamBody, combinators::BoxBody}; use hyper::body::{Bytes, Frame}; use napi::bindgen_prelude::{Buffer, External}; use parking_lot::Mutex; use serde_json::from_slice; use std::{collections::HashMap, sync::Arc}; use tokio::sync::mpsc::UnboundedSender; pub struct ResponseChannel { pub tx: Mutex>>, } impl ResponseChannel { pub fn new(tx: UnboundedSender) -> Self { Self { tx: Mutex::new(Some(tx)) } } } pub enum ResponseMessage { Complete { status: u16, headers: Vec<(String, String)>, body: Bytes }, StreamStart { status: u16, headers: Vec<(String, String)> }, StreamChunk { data: Bytes }, StreamEnd, } pub enum ResponseBody { Full(Full), Stream(StreamBody>), } pub type BoxedBody = BoxBody>; /// buffer: [status_code(2)] [headers_len(4)] [headers_json] [body] #[napi] pub fn send_response(channel: &External>, buffer: Buffer) -> napi::Result<()> { let data = buffer.as_ref(); if data.len() < 6 { return Err(napi::Error::from_reason("Invalid response buffer")); } let status_code = u16::from_le_bytes([data[0], data[1]]); let headers_len = u32::from_le_bytes([data[2], data[3], data[4], data[5]]) as usize; if data.len() < 6 + headers_len { return Err(napi::Error::from_reason("Invalid headers length")); } let headers_json = &data[6..6 + headers_len]; let headers: Vec<(String, String)> = from_slice(headers_json) .map_err(|e| napi::Error::from_reason(format!("Invalid headers JSON: {e}")))?; let body_start = 6 + headers_len; let body = if body_start < data.len() { Bytes::copy_from_slice(&data[body_start..]) } else { Bytes::new() }; let mut tx_guard = channel.tx.lock(); if let Some(tx) = tx_guard.as_ref() { let _ = tx.send(ResponseMessage::Complete { status: status_code, headers, body }); *tx_guard = None; Ok(()) } else { Err(napi::Error::from_reason("Response already sent")) } } /// buffer: [status_code(2)] [headers_len(4)] [headers_json] #[napi] pub fn start_stream(channel: &External>, buffer: Buffer) -> napi::Result<()> { let data = buffer.as_ref(); if data.len() < 6 { return Err(napi::Error::from_reason("Invalid stream start buffer")); } let status_code = u16::from_le_bytes([data[0], data[1]]); let headers_len = u32::from_le_bytes([data[2], data[3], data[4], data[5]]) as usize; if data.len() < 6 + headers_len { return Err(napi::Error::from_reason("Invalid headers length")); } let headers_json = &data[6..6 + headers_len]; let headers: Vec<(String, String)> = from_slice(headers_json) .map_err(|e| napi::Error::from_reason(format!("Invalid headers JSON: {e}")))?; let tx_guard = channel.tx.lock(); if let Some(tx) = tx_guard.as_ref() { let _ = tx.send(ResponseMessage::StreamStart { status: status_code, headers }); Ok(()) } else { Err(napi::Error::from_reason("Response already sent")) } } #[napi] pub fn send_chunk(channel: &External>, data: Buffer) -> napi::Result<()> { let tx_guard = channel.tx.lock(); if let Some(tx) = tx_guard.as_ref() { let bytes = Bytes::copy_from_slice(data.as_ref()); let _ = tx.send(ResponseMessage::StreamChunk { data: bytes }); Ok(()) } else { Err(napi::Error::from_reason("Stream not started")) } } #[napi] pub fn end_stream(channel: &External>) -> napi::Result<()> { let mut tx_guard = channel.tx.lock(); if let Some(tx) = tx_guard.take() { let _ = tx.send(ResponseMessage::StreamEnd); Ok(()) } else { Err(napi::Error::from_reason("Stream already ended")) } } #[derive(Clone)] #[napi(object)] pub struct SendFileOptionsCore { pub max_age: Option, pub root: Option, pub last_modified: Option, pub headers: Option>, pub dotfiles: Option, pub accept_ranges: Option, pub cache_control: Option, pub immutable: Option, } ================================================ FILE: packages/core/src/http.rs ================================================ pub mod cookies; pub mod cookies_tests; pub mod files; pub mod mime; pub mod mime_tests; pub mod request; pub mod response; ================================================ FILE: packages/core/src/lib.rs ================================================ #[macro_use] extern crate napi_derive; pub mod http; pub mod server; pub mod validation; ================================================ FILE: packages/core/src/server/context.rs ================================================ use crate::http::{request::RequestCore, response::ResponseChannel}; use napi::{ Env, bindgen_prelude::{External, Object, ToNapiValue}, sys, }; use std::sync::Arc; pub struct ContextObject { pub req: External>, pub res: External>, } impl ToNapiValue for ContextObject { unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> napi::Result { let mut obj = Object::new(&Env::from_raw(env))?; obj.set("req", val.req)?; obj.set("res", val.res)?; unsafe { Object::to_napi_value(env, obj) } } } ================================================ FILE: packages/core/src/server/core.rs ================================================ use hyper::server::conn::http1; use hyper_util::rt::TokioIo; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; use std::net::SocketAddr; use tokio::{net::TcpListener, sync::watch}; #[cfg(unix)] use std::fs; #[cfg(unix)] use std::path::Path; #[cfg(unix)] use tokio::net::UnixListener; use crate::server::{handler::handle_request, routes::insert_route}; use super::routes::Route; #[derive(Clone)] #[napi(object)] pub struct ServerOptionsCore { pub port: Option, pub host: Option, pub unix_socket: Option, pub trust_proxy: Option, pub max_request_size: Option, pub timeout: Option, pub reuse_port: Option, } #[napi] pub struct ServerCore { config: ServerOptionsCore, shutdown_tx: Option>, } #[napi] impl ServerCore { #[napi(constructor)] pub fn new(config: ServerOptionsCore) -> Self { ServerCore { config, shutdown_tx: None } } #[napi] pub fn get_config(&self) -> ServerOptionsCore { self.config.clone() } #[napi] pub fn set_config(&mut self, config: ServerOptionsCore) { self.config = config; } #[napi] pub fn add_route(&mut self, route: Route) -> napi::Result<()> { insert_route(route) } /// Start the HTTP server on TCP or Unix socket and execute the `ready` callback if provided. /// /// # Safety /// /// This function is unsafe because it exposes logic that will be called from JavaScript via N-API, and depends on: /// - The `ServerCore` object remains alive while async tasks are running. /// - The `ThreadsafeFunction` passed (if it exists) is valid and has not been released. /// - `start` is not called twice simultaneously on the same instance. /// /// The caller must guarantee these conditions. #[napi(ts_args_type = "ready: (() => void) | undefined")] pub async unsafe fn start(&mut self, ready: Option>) { let (shutdown_tx, mut shutdown_rx) = watch::channel::<()>(()); self.shutdown_tx = Some(shutdown_tx); #[cfg(unix)] if let Some(ref socket_path) = self.config.unix_socket { self.start_unix_socket(socket_path, ready, &mut shutdown_rx).await; return; } self.start_tcp(ready, &mut shutdown_rx).await; } async fn start_tcp( &self, ready: Option>, shutdown_rx: &mut watch::Receiver<()>, ) { let port = self.config.port.unwrap_or(3000); let host = self.config.host.as_deref().unwrap_or("0.0.0.0"); let addr: SocketAddr = format!("{host}:{port}").parse().expect("Invalid address"); let listener = create_reusable_listener(addr).await.expect("Failed to create listener"); if let Some(ready_cb) = ready { ready_cb.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking); } loop { tokio::select! { Ok((tcp, remote_addr)) = listener.accept() => { let io = TokioIo::new(tcp); let config = self.config.clone(); tokio::spawn(async move { if let Err(err) = http1::Builder::new() .serve_connection(io, hyper::service::service_fn(move |req| { handle_request(req, config.clone(), Some(remote_addr)) })) .await { eprintln!("Error serving connection: {err:?}"); } }); }, _ = shutdown_rx.changed() => break, } } } #[cfg(unix)] async fn start_unix_socket( &self, socket_path: &str, ready: Option>, shutdown_rx: &mut watch::Receiver<()>, ) { let path = Path::new(socket_path); if path.exists() { fs::remove_file(path).unwrap_or_else(|e| { eprintln!("Warning: Could not remove existing socket file: {e}"); }); } let listener = UnixListener::bind(path).expect("Failed to bind Unix socket"); if let Some(ready_cb) = ready { ready_cb.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking); } loop { tokio::select! { Ok((stream, _)) = listener.accept() => { let io = TokioIo::new(stream); let config = self.config.clone(); tokio::spawn(async move { if let Err(err) = http1::Builder::new() .serve_connection(io, hyper::service::service_fn(move |req| { handle_request(req, config.clone(), None) })) .await { eprintln!("Error serving connection: {err:?}"); } }); }, _ = shutdown_rx.changed() => break, } } let _ = fs::remove_file(path); } #[napi] pub fn close(&self) { if let Some(tx) = &self.shutdown_tx { let _ = tx.send(()); } } } async fn create_reusable_listener(addr: SocketAddr) -> std::io::Result { use socket2::{Domain, Protocol, Socket, Type}; use std::net::TcpListener as StdListener; let domain = if addr.is_ipv4() { Domain::IPV4 } else { Domain::IPV6 }; let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?; socket.set_reuse_address(true)?; #[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))] socket.set_reuse_port(true)?; socket.set_nonblocking(true)?; socket.bind(&addr.into())?; socket.listen(1024)?; let std_listener: StdListener = socket.into(); TcpListener::from_std(std_listener) } ================================================ FILE: packages/core/src/server/handler.rs ================================================ use http_body_util::{BodyExt, Full, StreamBody}; use hyper::{ Request, Response, body::{Bytes, Frame, Incoming}, header::{HeaderName, HeaderValue}, }; use napi::{bindgen_prelude::External, threadsafe_function::ThreadsafeFunctionCallMode}; use serde_json::json; use futures_util::stream; use std::{convert::Infallible, net::SocketAddr, sync::Arc}; use tokio::sync::mpsc; use crate::{ http::{ request::RequestCore, response::{BoxedBody, ResponseChannel, ResponseMessage}, }, server::{ context::ContextObject, core::ServerOptionsCore, routes::{ROUTER, ResponseStrategy}, }, validation::parser::*, }; pub async fn handle_request( req: Request, config: ServerOptionsCore, remote_addr: Option, ) -> Result, std::convert::Infallible> { let method = req.method().to_string(); let pathname = req.uri().path().to_string(); let matched = match ROUTER.find(&method, &pathname) { Some(m) => m, None => { return Ok(Response::builder() .status(404) .body( Full::new(Bytes::from_static(b"Not Found")) .map_err(|never| match never {}) .boxed(), ) .unwrap()); } }; let route = matched.route; if let ResponseStrategy::FullStatic(ref response) = route.strategy { let (parts, body) = response.clone().into_parts(); let body_bytes = body.collect().await.unwrap().to_bytes(); return Ok(Response::from_parts( parts, Full::new(body_bytes).map_err(|never| match never {}).boxed(), )); } if let ResponseStrategy::ParamTemplate { ref template, ref params, ref headers } = route.strategy { let mut rendered = template.clone(); for param_name in params { if let Some(value) = matched.params.get(param_name) { let placeholder = format!("{{{{params.{param_name}}}}}"); rendered = rendered.replace(&placeholder, value); } } let mut response = Response::builder().status(200); for (name, value) in headers { response = response.header(name.as_str(), value.as_str()); } return Ok(response .body(Full::new(Bytes::from(rendered)).map_err(|never| match never {}).boxed()) .unwrap()); } let mut req_core = match RequestCore::new(req, remote_addr, config.trust_proxy.unwrap_or(false)).await { Ok(core) => core, Err(e) => { eprintln!("Error creating request: {e}"); return Ok(Response::builder() .status(400) .body( Full::new(Bytes::from_static(b"Bad Request")) .map_err(|never| match never {}) .boxed(), ) .unwrap()); } }; req_core.params = matched.params.into_iter().collect(); if let Some(schema) = &route.schema { if let Some(params_schema) = &schema.params && let Err(e) = parse_params(&req_core.params, params_schema) { let error_msg = format!("Validation error in {}: {}", e.field, e.message); return Ok(Response::builder() .status(400) .header("Content-Type", "application/json") .body( Full::new(Bytes::from( json!({ "error": "Validation Error", "message": error_msg }) .to_string(), )) .map_err(|never| match never {}) .boxed(), ) .unwrap()); } if let Some(query_schema) = &schema.query && let Err(e) = parse_query(&req_core.query_raw, query_schema) { let error_msg = format!("Validation error in {}: {}", e.field, e.message); return Ok(Response::builder() .status(400) .header("Content-Type", "application/json") .body( Full::new(Bytes::from( json!({ "error": "Validation Error", "message": error_msg }) .to_string(), )) .map_err(|never| match never {}) .boxed(), ) .unwrap()); } if let Some(body_schema) = &schema.body { let has_body_method = matches!(method.as_str(), "POST" | "PUT" | "PATCH" | "DELETE"); if has_body_method { if req_core.body.is_empty() { let error_msg = "Request body is required".to_string(); return Ok(Response::builder() .status(400) .header("Content-Type", "application/json") .body( Full::new(Bytes::from( json!({ "error": "Validation Error", "message": error_msg }) .to_string(), )) .map_err(|never| match never {}) .boxed(), ) .unwrap()); } if let Err(e) = parse_body(req_core.body.as_ref(), body_schema) { let error_msg = format!("Validation error in {}: {}", e.field, e.message); return Ok(Response::builder() .status(400) .header("Content-Type", "application/json") .body( Full::new(Bytes::from( json!({ "error": "Validation Error", "message": error_msg }) .to_string(), )) .map_err(|never| match never {}) .boxed(), ) .unwrap()); } } } if let Some(headers_schema) = &schema.headers && let Err(e) = parse_headers(&req_core.headers_raw, headers_schema) { let error_msg = format!("Validation error in {}: {}", e.field, e.message); return Ok(Response::builder() .status(400) .header("Content-Type", "application/json") .body( Full::new(Bytes::from( json!({ "error": "Validation Error", "message": error_msg }) .to_string(), )) .map_err(|never| match never {}) .boxed(), ) .unwrap()); } } let (response_tx, mut response_rx) = mpsc::unbounded_channel(); let res_builder = Arc::new(ResponseChannel::new(response_tx)); let ctx_obj = ContextObject { req: External::new(Arc::new(req_core)), res: External::new(res_builder.clone()), }; if let ResponseStrategy::Dynamic(handler) = route.strategy.clone() { let _ = handler.call(ctx_obj, ThreadsafeFunctionCallMode::NonBlocking); } if let Some(first_msg) = response_rx.recv().await { match first_msg { ResponseMessage::Complete { status, headers, body } => { let mut response = Response::builder().status(status); for (name, value) in headers { if let (Ok(header_name), Ok(header_value)) = (name.parse::(), value.parse::()) { response = response.header(header_name, header_value); } } return Ok(response .body(Full::new(body).map_err(|never| match never {}).boxed()) .unwrap()); } ResponseMessage::StreamStart { status, headers } => { let mut response = Response::builder().status(status); for (name, value) in headers { if let (Ok(header_name), Ok(header_value)) = (name.parse::(), value.parse::()) { response = response.header(header_name, header_value); } } let stream = stream::unfold(response_rx, |mut rx| async move { match rx.recv().await { Some(ResponseMessage::StreamChunk { data }) => { Some((Ok(Frame::data(data)), rx)) } Some(ResponseMessage::StreamEnd) | None => None, _ => None, } }); let body = StreamBody::new(stream); return Ok(response .body(body.map_err(|never: Infallible| match never {}).boxed()) .unwrap()); } _ => {} } } Ok(Response::builder() .status(200) .body(Full::new(Bytes::new()).map_err(|never| match never {}).boxed()) .unwrap()) } ================================================ FILE: packages/core/src/server/router.rs ================================================ use ahash::AHashMap; use matchit::{Match, Router as MatchitRouter}; use parking_lot::RwLock; use std::sync::Arc; use super::routes::CompiledRoute; pub struct TrieRouter { inner: MatchitRouter>, } impl Default for TrieRouter { fn default() -> Self { Self::new() } } impl TrieRouter { pub fn new() -> Self { Self { inner: MatchitRouter::new() } } pub fn insert(&mut self, path: &str, route: Arc) -> Result<(), String> { self.inner.insert(path, route).map_err(|e| format!("Failed to insert route: {e}")) } pub fn find<'a>(&'a self, path: &'a str) -> Option>> { self.inner.at(path).ok() } } pub struct HttpRouter { static_routes: AHashMap, Arc>, dynamic_routes: TrieRouter, } impl Default for HttpRouter { fn default() -> Self { Self::new() } } impl HttpRouter { pub fn new() -> Self { Self { static_routes: AHashMap::new(), dynamic_routes: TrieRouter::new() } } pub fn insert(&mut self, route: CompiledRoute) -> Result<(), String> { let route_arc = Arc::new(route); if Self::is_static_route(&route_arc.path) { self.static_routes.insert(route_arc.path.clone(), route_arc.clone()); } else { self.dynamic_routes.insert(&route_arc.path, route_arc.clone())?; } Ok(()) } pub fn find<'a>(&'a self, path: &'a str) -> Option { if let Some(route) = self.static_routes.get(path) { return Some(RouteMatch { route: route.clone(), params: AHashMap::new() }); } if let Some(matched) = self.dynamic_routes.find(path) { let mut params = AHashMap::new(); for (key, value) in matched.params.iter() { params.insert(key.to_string(), value.to_string()); } return Some(RouteMatch { route: matched.value.clone(), params }); } None } fn is_static_route(path: &str) -> bool { !path.contains(':') && !path.contains('*') && !path.contains('{') } } pub struct RouteMatch { pub route: Arc, pub params: AHashMap, } pub struct GlobalRouter { routers: RwLock, HttpRouter>>, } impl Default for GlobalRouter { fn default() -> Self { Self::new() } } impl GlobalRouter { pub fn new() -> Self { Self { routers: RwLock::new(AHashMap::new()) } } pub fn insert(&self, method: &str, route: CompiledRoute) -> Result<(), String> { let mut routers = self.routers.write(); let router = routers.entry(method.to_string().into_boxed_str()).or_default(); router.insert(route) } pub fn find(&self, method: &str, path: &str) -> Option { let routers = self.routers.read(); if let Some(router) = routers.get(method) { return router.find(path); } None } pub fn route_count(&self) -> usize { let routers = self.routers.read(); routers.values().map(|r| r.static_routes.len()).sum() } } ================================================ FILE: packages/core/src/server/routes.rs ================================================ use ahash::HashMap; use std::sync::Arc; use http_body_util::Full; use hyper::Response; use hyper::body::Bytes; use napi::Error; use napi::{bindgen_prelude::Function, threadsafe_function::ThreadsafeFunction}; use base64::Engine; use base64::engine::general_purpose; use once_cell::sync::Lazy; use serde_json::{Value, from_str, from_value}; use crate::server::context::ContextObject; use crate::server::router::GlobalRouter; use crate::validation::types::SchemaType; pub type RouteHandler = ThreadsafeFunction; pub struct CompiledRoute { pub method: Box, pub path: Box, pub segments: Box<[Box]>, pub strategy: ResponseStrategy, pub schema: Option, } #[derive(Clone)] pub struct RouteSchema { pub params: Option, pub query: Option, pub body: Option, pub headers: Option, } pub static ROUTER: Lazy = Lazy::new(GlobalRouter::new); #[napi(object)] pub struct Route { pub path: String, #[napi(ts_type = "'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'TRACE'")] pub method: String, #[napi(ts_type = "RouteHandler")] pub handler: Function<'static, ContextObject, ()>, pub schema: Option, pub static_response: Option, } #[derive(Clone)] pub enum ResponseStrategy { Dynamic(Arc), FullStatic(Response>), ParamTemplate { template: String, params: Vec, headers: HashMap }, } fn convert_path_to_matchit_format(path: &str) -> String { if !path.contains(':') { return path.to_string(); } let parts: Vec<&str> = path.split('/').collect(); let converted_parts: Vec = parts .iter() .map(|part| { if let Some(stripped) = part.strip_prefix(':') { format!("{{{stripped}}}") } else { part.to_string() } }) .collect(); converted_parts.join("/") } pub fn insert_route(route: Route) -> napi::Result<()> { let method_key: Box = route.method.clone().into_boxed_str(); let converted_path = convert_path_to_matchit_format(&route.path); let segments: Vec> = converted_path .split('/') .filter(|s| !s.is_empty()) .map(|s| s.to_string().into_boxed_str()) .collect(); let strategy = if let Some(static_json) = route.static_response { let static_info: Value = from_str(&static_json) .map_err(|e| Error::from_reason(format!("Invalid static response: {e}")))?; match static_info["type"].as_str() { Some("full_static") => { let status = static_info["status"].as_u64().unwrap_or(200) as u16; let body_base64 = static_info["body"] .as_str() .ok_or_else(|| Error::from_reason("Missing body field in static response"))?; let body_bytes = general_purpose::STANDARD .decode(body_base64) .map_err(|e| Error::from_reason(format!("Invalid base64 body: {e}")))?; let mut response = Response::builder().status(status); if let Some(headers) = static_info["headers"].as_object() { for (name, value) in headers { if let Some(v) = value.as_str() { response = response.header(name.as_str(), v); } } } let response = response .body(Full::new(Bytes::from(body_bytes))) .map_err(|e| Error::from_reason(format!("Failed to build response: {e}")))?; ResponseStrategy::FullStatic(response) } Some("param_template") => { let template = static_info["template"] .as_str() .ok_or_else(|| Error::from_reason("Missing template field"))? .to_string(); let params: Vec = static_info["params"] .as_array() .ok_or_else(|| Error::from_reason("Missing params field"))? .iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect(); let headers: HashMap = static_info["headers"] .as_object() .ok_or_else(|| Error::from_reason("Missing headers field"))? .iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect(); ResponseStrategy::ParamTemplate { template, params, headers } } _ => { let tsfn = route.handler.build_threadsafe_function().build()?; ResponseStrategy::Dynamic(Arc::new(tsfn)) } } } else { let tsfn = route.handler.build_threadsafe_function().build()?; ResponseStrategy::Dynamic(Arc::new(tsfn)) }; let schema = if let Some(schema_json) = route.schema { let parsed: Value = from_str(&schema_json) .map_err(|e| Error::from_reason(format!("Invalid schema JSON: {e}")))?; Some(RouteSchema { params: parsed.get("params").and_then(|v| from_value(v.clone()).ok()), query: parsed.get("query").and_then(|v| from_value(v.clone()).ok()), body: parsed.get("body").and_then(|v| from_value(v.clone()).ok()), headers: parsed.get("headers").and_then(|v| from_value(v.clone()).ok()), }) } else { None }; let compiled = CompiledRoute { method: method_key.clone(), path: converted_path.into_boxed_str(), segments: segments.into_boxed_slice(), strategy, schema, }; ROUTER.insert(&route.method, compiled).map_err(Error::from_reason)?; Ok(()) } ================================================ FILE: packages/core/src/server.rs ================================================ pub mod context; pub mod core; pub mod handler; pub mod router; pub mod routes; pub use core::ServerCore; pub use router::{GlobalRouter, HttpRouter}; pub use routes::{CompiledRoute, ROUTER, Route}; ================================================ FILE: packages/core/src/validation/parser.rs ================================================ use serde_json::{Map, Value, from_slice}; use super::types::*; use crate::validation::validate_value; use std::collections::HashMap; pub fn parse_params( params: &HashMap, schema: &SchemaType, ) -> Result { let mut obj = Map::new(); for (key, value) in params { obj.insert(key.clone(), Value::String(value.clone())); } validate_value(&Value::Object(obj), schema, "params") } pub fn parse_query( query: &HashMap>, schema: &SchemaType, ) -> Result { let mut obj = Map::new(); for (key, values) in query { if values.len() == 1 { obj.insert(key.clone(), Value::String(values[0].clone())); } else if values.is_empty() { obj.insert(key.clone(), Value::Null); } else { let arr: Vec = values.iter().map(|v| Value::String(v.clone())).collect(); obj.insert(key.clone(), Value::Array(arr)); } } validate_value(&Value::Object(obj), schema, "query") } pub fn parse_body(body: &[u8], schema: &SchemaType) -> Result { if body.is_empty() { if let SchemaType::Object { optional: true, default, .. } = schema { return Ok(default.clone().unwrap_or(Value::Null)); } return Err(ValidationError::new("body", "Request body is required")); } let body_value: Value = from_slice(body).map_err(|_| ValidationError::new("body", "Invalid JSON"))?; validate_value(&body_value, schema, "body") } pub fn parse_headers( headers: &HashMap, schema: &SchemaType, ) -> Result { let mut obj = Map::new(); for (key, value) in headers { obj.insert(key.clone(), Value::String(value.clone())); } validate_value(&Value::Object(obj), schema, "headers") } ================================================ FILE: packages/core/src/validation/parser_tests.rs ================================================ #[cfg(test)] mod tests { use super::super::parser::*; use super::super::types::*; use serde_json::json; use std::collections::HashMap; #[test] fn test_parse_params_basic() { let mut params = HashMap::new(); params.insert("id".to_string(), "123".to_string()); params.insert("name".to_string(), "john".to_string()); let mut shape = HashMap::new(); shape.insert( "id".to_string(), SchemaType::String { optional: false, default: None, constraints: vec![] }, ); shape.insert( "name".to_string(), SchemaType::String { optional: false, default: None, constraints: vec![] }, ); let schema = SchemaType::Object { optional: false, default: None, shape }; let result = parse_params(¶ms, &schema); assert!(result.is_ok()); } #[test] fn test_parse_query_single_value() { let mut query = HashMap::new(); query.insert("page".to_string(), vec!["1".to_string()]); let mut shape = HashMap::new(); shape.insert( "page".to_string(), SchemaType::Number { optional: false, default: None, constraints: vec![] }, ); let schema = SchemaType::Object { optional: false, default: None, shape }; let result = parse_query(&query, &schema); assert!(result.is_ok()); } #[test] fn test_parse_query_multiple_values() { let mut query = HashMap::new(); query.insert("tags".to_string(), vec!["rust".to_string(), "web".to_string()]); let mut shape = HashMap::new(); shape.insert( "tags".to_string(), SchemaType::Array { optional: false, default: None, constraints: vec![], item: Box::new(SchemaType::String { optional: false, default: None, constraints: vec![], }), }, ); let schema = SchemaType::Object { optional: false, default: None, shape }; let result = parse_query(&query, &schema); assert!(result.is_ok()); } #[test] fn test_parse_body_valid_json() { let body = br#"{"name":"Alice","age":25}"#; let mut shape = HashMap::new(); shape.insert( "name".to_string(), SchemaType::String { optional: false, default: None, constraints: vec![] }, ); shape.insert( "age".to_string(), SchemaType::Number { optional: false, default: None, constraints: vec![] }, ); let schema = SchemaType::Object { optional: false, default: None, shape }; let result = parse_body(body, &schema); assert!(result.is_ok()); } #[test] fn test_parse_body_invalid_json() { let body = b"not valid json"; let schema = SchemaType::Object { optional: false, default: None, shape: HashMap::new() }; let result = parse_body(body, &schema); assert!(result.is_err()); } #[test] fn test_parse_body_empty_with_optional() { let body = b""; let schema = SchemaType::Object { optional: true, default: Some(json!({})), shape: HashMap::new() }; let result = parse_body(body, &schema); assert!(result.is_ok()); } #[test] fn test_parse_body_empty_required() { let body = b""; let schema = SchemaType::Object { optional: false, default: None, shape: HashMap::new() }; let result = parse_body(body, &schema); assert!(result.is_err()); } #[test] fn test_parse_headers() { let mut headers = HashMap::new(); headers.insert("authorization".to_string(), "Bearer token123".to_string()); headers.insert("content-type".to_string(), "application/json".to_string()); let mut shape = HashMap::new(); shape.insert( "authorization".to_string(), SchemaType::String { optional: false, default: None, constraints: vec![] }, ); let schema = SchemaType::Object { optional: false, default: None, shape }; let result = parse_headers(&headers, &schema); assert!(result.is_ok()); } #[test] fn test_parse_params_with_validation() { let mut params = HashMap::new(); params.insert("id".to_string(), "550e8400-e29b-41d4-a716-446655440000".to_string()); let mut shape = HashMap::new(); shape.insert( "id".to_string(), SchemaType::String { optional: false, default: None, constraints: vec![StringConstraint::Uuid], }, ); let schema = SchemaType::Object { optional: false, default: None, shape }; let result = parse_params(¶ms, &schema); assert!(result.is_ok()); } #[test] fn test_parse_params_validation_fails() { let mut params = HashMap::new(); params.insert("id".to_string(), "not-a-uuid".to_string()); let mut shape = HashMap::new(); shape.insert( "id".to_string(), SchemaType::String { optional: false, default: None, constraints: vec![StringConstraint::Uuid], }, ); let schema = SchemaType::Object { optional: false, default: None, shape }; let result = parse_params(¶ms, &schema); assert!(result.is_err()); } } ================================================ FILE: packages/core/src/validation/tests.rs ================================================ #[cfg(test)] mod tests { use super::super::{types::*, validators::*}; use serde_json::{Value, json}; use std::collections::HashMap; #[test] fn test_string_min_length() { let constraints = vec![StringConstraint::Min { value: 5 }]; assert!(validate_string("hello", &constraints).is_ok()); assert!(validate_string("hi", &constraints).is_err()); } #[test] fn test_string_max_length() { let constraints = vec![StringConstraint::Max { value: 5 }]; assert!(validate_string("hello", &constraints).is_ok()); assert!(validate_string("hello world", &constraints).is_err()); } #[test] fn test_string_email() { let constraints = vec![StringConstraint::Email]; assert!(validate_string("user@example.com", &constraints).is_ok()); assert!(validate_string("invalid-email", &constraints).is_err()); assert!(validate_string("no@domain", &constraints).is_err()); } #[test] fn test_string_uuid() { let constraints = vec![StringConstraint::Uuid]; assert!(validate_string("550e8400-e29b-41d4-a716-446655440000", &constraints).is_ok()); assert!(validate_string("not-a-uuid", &constraints).is_err()); } #[test] fn test_number_min() { let constraints = vec![NumberConstraint::Min { value: 10.0 }]; assert!(validate_number(15.0, &constraints).is_ok()); assert!(validate_number(5.0, &constraints).is_err()); } #[test] fn test_number_max() { let constraints = vec![NumberConstraint::Max { value: 100.0 }]; assert!(validate_number(50.0, &constraints).is_ok()); assert!(validate_number(150.0, &constraints).is_err()); } #[test] fn test_number_int() { let constraints = vec![NumberConstraint::Int]; assert!(validate_number(42.0, &constraints).is_ok()); assert!(validate_number(42.5, &constraints).is_err()); } #[test] fn test_number_positive() { let constraints = vec![NumberConstraint::Positive]; assert!(validate_number(10.0, &constraints).is_ok()); assert!(validate_number(-5.0, &constraints).is_err()); assert!(validate_number(0.0, &constraints).is_err()); } #[test] fn test_validate_string_value() { let schema = SchemaType::String { optional: false, default: None, constraints: vec![StringConstraint::Min { value: 3 }], }; assert!(validate_value(&json!("hello"), &schema, "field").is_ok()); assert!(validate_value(&json!("hi"), &schema, "field").is_err()); assert!(validate_value(&Value::Null, &schema, "field").is_err()); } #[test] fn test_validate_optional_with_default() { let schema = SchemaType::String { optional: true, default: Some("default".to_string()), constraints: vec![], }; let result = validate_value(&Value::Null, &schema, "field").unwrap(); assert_eq!(result, json!("default")); } #[test] fn test_validate_number_from_string() { let schema = SchemaType::Number { optional: false, default: None, constraints: vec![] }; assert!(validate_value(&json!("42"), &schema, "field").is_ok()); assert!(validate_value(&json!("not-a-number"), &schema, "field").is_err()); } #[test] fn test_validate_boolean_from_string() { let schema = SchemaType::Boolean { optional: false, default: None }; assert_eq!(validate_value(&json!("true"), &schema, "field").unwrap(), json!(true)); assert_eq!(validate_value(&json!("false"), &schema, "field").unwrap(), json!(false)); assert_eq!(validate_value(&json!("1"), &schema, "field").unwrap(), json!(true)); assert_eq!(validate_value(&json!("0"), &schema, "field").unwrap(), json!(false)); } #[test] fn test_validate_array() { let item_schema = SchemaType::Number { optional: false, default: None, constraints: vec![] }; let schema = SchemaType::Array { optional: false, default: None, constraints: vec![ArrayConstraint::Min { value: 2 }], item: Box::new(item_schema), }; assert!(validate_value(&json!([1, 2, 3]), &schema, "field").is_ok()); assert!(validate_value(&json!([1]), &schema, "field").is_err()); } #[test] fn test_validate_object() { let mut shape = HashMap::new(); shape.insert( "name".to_string(), SchemaType::String { optional: false, default: None, constraints: vec![] }, ); shape.insert( "age".to_string(), SchemaType::Number { optional: false, default: None, constraints: vec![NumberConstraint::Positive], }, ); let schema = SchemaType::Object { optional: false, default: None, shape }; let valid_value = json!({ "name": "Alice", "age": 25 }); let invalid_value = json!({ "name": "Bob", "age": -5 }); assert!(validate_value(&valid_value, &schema, "field").is_ok()); assert!(validate_value(&invalid_value, &schema, "field").is_err()); } #[test] fn test_validate_literal() { let schema = SchemaType::Literal { optional: false, default: None, value: json!("admin") }; assert!(validate_value(&json!("admin"), &schema, "field").is_ok()); assert!(validate_value(&json!("user"), &schema, "field").is_err()); } #[test] fn test_validate_union() { let schemas = vec![ SchemaType::String { optional: false, default: None, constraints: vec![] }, SchemaType::Number { optional: false, default: None, constraints: vec![] }, ]; let schema = SchemaType::Union { optional: false, default: None, schemas }; assert!(validate_value(&json!("hello"), &schema, "field").is_ok()); assert!(validate_value(&json!(42), &schema, "field").is_ok()); assert!(validate_value(&json!(true), &schema, "field").is_err()); } } ================================================ FILE: packages/core/src/validation/types.rs ================================================ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum SchemaType { String { optional: bool, default: Option, constraints: Vec, }, Number { optional: bool, default: Option, constraints: Vec, }, Boolean { optional: bool, default: Option, }, Array { optional: bool, default: Option, constraints: Vec, item: Box, }, Object { optional: bool, default: Option, shape: HashMap, }, Literal { optional: bool, default: Option, value: Value, }, Union { optional: bool, default: Option, schemas: Vec, }, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum StringConstraint { Min { value: usize }, Max { value: usize }, Length { value: usize }, Email, Url, Uuid, Regex { value: String }, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum NumberConstraint { Min { value: f64 }, Max { value: f64 }, Int, Positive, Negative, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum ArrayConstraint { Min { value: usize }, Max { value: usize }, Length { value: usize }, } #[derive(Debug, Clone)] pub struct ValidationError { pub field: String, pub message: String, } impl ValidationError { pub fn new(field: impl Into, message: impl Into) -> Self { Self { field: field.into(), message: message.into() } } } ================================================ FILE: packages/core/src/validation/validators.rs ================================================ use super::types::*; use regex::Regex; use serde_json::{Map, Number, Value}; pub fn validate_string( value: &str, constraints: &[StringConstraint], ) -> Result<(), ValidationError> { let uuid_regex = Regex::new( r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", ) .unwrap(); let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap(); for constraint in constraints { match constraint { StringConstraint::Min { value: min } => { if value.len() < *min { return Err(ValidationError::new( "", format!("String must be at least {min} characters long"), )); } } StringConstraint::Max { value: max } => { if value.len() > *max { return Err(ValidationError::new( "", format!("String must be at most {max} characters long"), )); } } StringConstraint::Length { value: length } => { if value.len() != *length { return Err(ValidationError::new( "", format!("String must be exactly {length} characters long"), )); } } StringConstraint::Email => { if !email_regex.is_match(value) { return Err(ValidationError::new("", "Invalid email format")); } } StringConstraint::Url => { if !value.starts_with("http://") && !value.starts_with("https://") { return Err(ValidationError::new("", "Invalid URL format")); } } StringConstraint::Uuid => { if !uuid_regex.is_match(value) { return Err(ValidationError::new("", "Invalid UUID format")); } } StringConstraint::Regex { value: pattern } => { let regex = Regex::new(pattern) .map_err(|_| ValidationError::new("", "Invalid regex pattern"))?; if !regex.is_match(value) { return Err(ValidationError::new("", "String does not match pattern")); } } } } Ok(()) } pub fn validate_number( value: f64, constraints: &[NumberConstraint], ) -> Result<(), ValidationError> { for constraint in constraints { match constraint { NumberConstraint::Min { value: min } => { if value < *min { return Err(ValidationError::new("", format!("Number must be at least {min}"))); } } NumberConstraint::Max { value: max } => { if value > *max { return Err(ValidationError::new("", format!("Number must be at most {max}"))); } } NumberConstraint::Int => { if value.fract() != 0.0 { return Err(ValidationError::new("", "Number must be an integer")); } } NumberConstraint::Positive => { if value <= 0.0 { return Err(ValidationError::new("", "Number must be positive")); } } NumberConstraint::Negative => { if value >= 0.0 { return Err(ValidationError::new("", "Number must be negative")); } } } } Ok(()) } pub fn validate_value( value: &Value, schema: &SchemaType, field_path: &str, ) -> Result { match schema { SchemaType::String { optional, default, constraints } => { if value.is_null() { if *optional { return Ok(default .as_ref() .map(|d| Value::String(d.clone())) .unwrap_or(Value::Null)); } return Err(ValidationError::new(field_path, "Field is required")); } let str_value = value .as_str() .ok_or_else(|| ValidationError::new(field_path, "Expected string"))?; validate_string(str_value, constraints) .map_err(|e| ValidationError::new(field_path, e.message))?; Ok(value.clone()) } SchemaType::Number { optional, default, constraints } => { if value.is_null() { if *optional { return Ok(default .as_ref() .map(|d| Value::Number(Number::from_f64(*d).unwrap())) .unwrap_or(Value::Null)); } return Err(ValidationError::new(field_path, "Field is required")); } let num_value = if let Some(n) = value.as_f64() { n } else if let Some(s) = value.as_str() { s.parse::() .map_err(|_| ValidationError::new(field_path, "Invalid number format"))? } else { return Err(ValidationError::new(field_path, "Expected number")); }; validate_number(num_value, constraints) .map_err(|e| ValidationError::new(field_path, e.message))?; Ok(Value::Number(Number::from_f64(num_value).unwrap())) } SchemaType::Boolean { optional, default } => { if value.is_null() { if *optional { return Ok(default.as_ref().map(|d| Value::Bool(*d)).unwrap_or(Value::Null)); } return Err(ValidationError::new(field_path, "Field is required")); } if let Some(b) = value.as_bool() { Ok(Value::Bool(b)) } else if let Some(s) = value.as_str() { match s.to_lowercase().as_str() { "true" | "1" => Ok(Value::Bool(true)), "false" | "0" => Ok(Value::Bool(false)), _ => Err(ValidationError::new(field_path, "Invalid boolean value")), } } else { Err(ValidationError::new(field_path, "Expected boolean")) } } SchemaType::Array { optional, default, constraints, item } => { if value.is_null() { if *optional { return Ok(default.clone().unwrap_or(Value::Null)); } return Err(ValidationError::new(field_path, "Field is required")); } let arr = value .as_array() .ok_or_else(|| ValidationError::new(field_path, "Expected array"))?; for constraint in constraints { match constraint { ArrayConstraint::Min { value: min } => { if arr.len() < *min { return Err(ValidationError::new( field_path, format!("Array must have at least {min} items"), )); } } ArrayConstraint::Max { value: max } => { if arr.len() > *max { return Err(ValidationError::new( field_path, format!("Array must have at most {max} items"), )); } } ArrayConstraint::Length { value: length } => { if arr.len() != *length { return Err(ValidationError::new( field_path, format!("Array must have exactly {length} items"), )); } } } } let mut validated_items = Vec::new(); for (i, item_value) in arr.iter().enumerate() { let item_path = format!("{field_path}[{i}]"); let validated = validate_value(item_value, item, &item_path)?; validated_items.push(validated); } Ok(Value::Array(validated_items)) } SchemaType::Object { optional, default, shape } => { if value.is_null() || (value.is_object() && value.as_object().unwrap().is_empty()) { if *optional { return Ok(default.clone().unwrap_or(Value::Null)); } if shape.is_empty() { return Ok(Value::Object(Map::new())); } return Err(ValidationError::new(field_path, "Field is required")); } let obj = value .as_object() .ok_or_else(|| ValidationError::new(field_path, "Expected object"))?; let mut validated_obj = Map::new(); for (key, field_schema) in shape { let field_value = obj.get(key).cloned().unwrap_or(Value::Null); let new_path = if field_path.is_empty() { key.clone() } else { format!("{field_path}.{key}") }; let validated = validate_value(&field_value, field_schema, &new_path)?; if !validated.is_null() || matches!( field_schema, SchemaType::String { optional: false, .. } | SchemaType::Number { optional: false, .. } | SchemaType::Boolean { optional: false, .. } ) { validated_obj.insert(key.clone(), validated); } } Ok(Value::Object(validated_obj)) } SchemaType::Literal { optional, default, value: literal_value } => { if value.is_null() { if *optional { return Ok(default.clone().unwrap_or(Value::Null)); } return Err(ValidationError::new(field_path, "Field is required")); } let compare_value = if value.is_string() { value.clone() } else if literal_value.is_string() && !value.is_string() { Value::String(value.to_string()) } else { value.clone() }; if &compare_value != literal_value { return Err(ValidationError::new( field_path, format!("Value must be exactly {literal_value:?}"), )); } Ok(value.clone()) } SchemaType::Union { optional, default, schemas } => { if value.is_null() { if *optional { return Ok(default.clone().unwrap_or(Value::Null)); } return Err(ValidationError::new(field_path, "Field is required")); } for schema in schemas { if let Ok(validated) = validate_value(value, schema, field_path) { return Ok(validated); } } Err(ValidationError::new(field_path, "Value does not match any union type")) } } } ================================================ FILE: packages/core/src/validation.rs ================================================ pub mod parser; pub mod parser_tests; pub mod tests; pub mod types; pub mod validators; pub use parser::*; pub use types::*; pub use validators::*; ================================================ FILE: packages/core/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist" }, "include": ["src"], "references": [] } ================================================ FILE: packages/kitojs/.gitignore ================================================ dist/ docs/ ================================================ FILE: packages/kitojs/package.json ================================================ { "name": "kitojs", "version": "1.0.0-alpha.8", "description": "🐺 The high-performance, type-safe and modern TypeScript web framework written in Rust.", "scripts": { "build": "tsdown", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", "docs": "typedoc", "docs:watch": "typedoc --watch", "docs:serve": "pnpm dlx serve docs" }, "dependencies": { "@kitojs/kito-core": "workspace:*", "acorn": "^8.15.0", "acorn-walk": "^8.3.4" }, "devDependencies": { "@kitojs/types": "workspace:*", "@vitest/coverage-v8": "^4.0.9", "tsdown": "^0.15.0", "typedoc": "^0.28.14", "vitest": "^4.0.9" }, "peerDependencies": { "typescript": "^5.9.2" }, "repository": { "type": "git", "url": "https://github.com/kitojs/kito.git" }, "homepage": "https://kito.pages.dev", "bugs": { "url": "https://github.com/kitojs/kito/issues" }, "license": "MIT", "author": { "name": "Nehuén", "url": "https://github.com/nehu3n" }, "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }, "files": [ "dist", "readme.md", "package.json" ] } ================================================ FILE: packages/kitojs/readme.md ================================================


High-performance, fully type-safe, and modern web framework for TypeScript. Powered by Rust for extreme speed and low memory usage.

--- - **Extreme performance** – Rust core optimized for extreme speed & efficiency. **See the [benchmarks](https://github.com/kitojs/kito/tree/main/bench).** - **Type-safe** – full TypeScript support with end-to-end safety and exceptional DX. - **Schema validation** – built-in validation with zero bloat. - **Middleware system** – composable and flexible like you expect. - **Cross-platform** – runs on Node.js, Bun, and Deno. --- ## 🚀 Quick Start You can add **Kito** to an existing project: ```bash pnpm add kitojs # Or: npm/yarn/bun add kitojs # Or: deno add npm:kitojs ``` Or create a new project instantly with the [official starter](https://github.com/kitojs/create-kitojs): ```bash pnpm create kitojs # Or: npm/yarn/bun create kitojs # Or: deno init --npm kitojs ``` ### Minimal Example ```ts import { server } from "kitojs"; const app = server(); app.get("/", ({ res }) => { res.send("hello world!"); }); app.listen(3000); ```
Fluent style Kito also supports fluent style. You can chain all methods. **See the examples [here](https://github.com/kitojs/kito/tree/main/examples/fluent).** ```ts import { server } from "kitojs"; server() .get("/", ({ res }) => res.send("hello world!")) .listen(3000); ```
--- ## 📚 Documentation Full docs available at the [**official website**](https://kito.pages.dev). You can also explore ready-to-run [examples](https://github.com/kitojs/kito/tree/main/examples). --- ## 🤝 Contributing We welcome contributions! Check the [**contributing guide**](https://github.com/kitojs/kito/blob/main/contributing.md) to learn how to set up your environment and submit pull requests. --- ## 📄 License Licensed under the [MIT License](https://github.com/kitojs/kito/blob/main/license). --- ================================================ FILE: packages/kitojs/src/helpers/middleware.ts ================================================ import type { MiddlewareDefinition, MiddlewareHandler } from "@kitojs/types"; /** * Creates a typed middleware definition. * * @template TExtensions - Type of context extensions available in the middleware * @param handler - Middleware function that processes requests * @returns Middleware definition object * * @example * ```typescript * import { middleware } from 'kitojs'; * * // Basic middleware * const logger = middleware((ctx, next) => { * console.log(`${ctx.req.method} ${ctx.req.url}`); * next(); * }); * * // Authentication middleware * const auth = middleware((ctx, next) => { * const token = ctx.req.headers.authorization; * * if (!token || !verifyToken(token)) { * ctx.res.status(401).send('Unauthorized'); * return; * } * * next(); * }); * * // Async middleware * const asyncAuth = middleware(async (ctx, next) => { * const user = await validateUser(ctx.req.headers.authorization); * if (!user) { * ctx.res.status(401).send('Unauthorized'); * return; * } * await next(); * }); * * // Using middleware * app.use(logger); // global * * // Route middleware * app.get('/protected', [auth], ctx => { * ctx.res.send('Secret data'); * }); * ``` */ export function middleware( handler: MiddlewareHandler, ): MiddlewareDefinition { return { type: "function", handler: handler as MiddlewareHandler, global: false, }; } ================================================ FILE: packages/kitojs/src/helpers/schema.ts ================================================ // biome-ignore assist/source/organizeImports: ... import type { SchemaDefinition, JSONSchemaDefinition, InferJSONSchemaRequest, } from "@kitojs/types"; import { convertJSONSchema } from "../schemas/jsonSchema"; /** * Creates a typed schema definition for request validation. * This is a type helper that preserves schema structure for type inference. * * @template T - Schema definition type * @param definition - Schema definition object * @returns The same schema definition with preserved types * * @example * ```typescript * import { schema, t } from 'kitojs'; * * const userSchema = schema({ * params: t.object({ * id: t.str().uuid() * }), * query: t.object({ * limit: t.num().min(1).max(100).default(10) * }), * body: t.object({ * name: t.str().min(1), * email: t.str().email() * }), * headers: t.object({ * authorization: t.str() * }) * }); * * app.post('/users/:id', ({ req, res }) => { * // All request parts are type-safe and validated * const id = req.params.id; // string (UUID validated) * const limit = req.query.limit; // number * const name = req.body.name; // string * }, userSchema); * ``` */ export function schema(definition: T): T { return definition; } /** * Converts a JSON Schema definition to internal schema format * @template T - JSON Schema definition type * @param definition - JSON Schema definition object * @returns Converted schema definition with preserved types * * @example * ```typescript * import { schema } from 'kitojs'; * * const userSchema = schema.json({ * params: { * type: 'object', * properties: { * id: { type: 'string', format: 'uuid' } * }, * required: ['id'] * }, * body: { * type: 'object', * properties: { * name: { type: 'string', minLength: 1 }, * email: { type: 'string', format: 'email' } * }, * required: ['name', 'email'] * } * }); * * app.post('/users/:id', ({ req, res }) => { * // types are inferred from JSON Schema * const id = req.params.id; // string * const name = req.body.name; // string * }, userSchema); * ``` */ schema.json = ( definition: T, ): SchemaDefinition & { __jsonSchemaInfer: InferJSONSchemaRequest; } => { // biome-ignore lint/suspicious/noExplicitAny: ... const converted: any = {}; if (definition.params) { converted.params = convertJSONSchema(definition.params); } if (definition.query) { converted.query = convertJSONSchema(definition.query); } if (definition.body) { converted.body = convertJSONSchema(definition.body); } if (definition.headers) { converted.headers = convertJSONSchema(definition.headers); } if (definition.response) { converted.response = {}; for (const [statusCode, responseSchema] of Object.entries( definition.response, )) { converted.response[statusCode] = convertJSONSchema(responseSchema); } } // biome-ignore lint/suspicious/noExplicitAny: ... return converted as any; }; ================================================ FILE: packages/kitojs/src/index.ts ================================================ /** * @module kitojs * * Kito is a high-performance, type-safe TypeScript web framework with a Rust core. * * @example Basic usage * ```typescript * import { server } from 'kitojs'; * * const app = server(); * * app.get('/', ctx => { * ctx.res.send('Hello World!'); * }); * * app.listen(3000); * ``` * * @example With schema validation * ```typescript * import { server, schema, t } from 'kitojs'; * * const userSchema = schema({ * params: t.object({ id: t.str().uuid() }), * body: t.object({ * name: t.str().min(1), * email: t.str().email() * }) * }); * * app.post('/users/:id', [userSchema], ctx => { * ctx.res.json({ * id: ctx.req.params.id, * ...ctx.req.body * }); * }); * ``` * * @example With middleware * ```typescript * import { server, middleware } from 'kitojs'; * * const logger = middleware((ctx, next) => { * console.log(`${ctx.req.method} ${ctx.req.url}`); * next(); * }); * * app.use(logger); * ``` * * @example With context extensions * ```typescript * const app = server().extend<{ db: Database }>(ctx => { * ctx.db = createDatabase(); * }); * * app.get('/users', async ctx => { * const users = await ctx.db.query('SELECT * FROM users'); * ctx.res.json(users); * }); * ``` * * @example With routers * ```typescript * import { server, router } from 'kitojs'; * * const apiRouter = router(); * apiRouter.get('/users', ({ res }) => res.json({ users: [] })); * * const app = server(); * app.mount('/api', apiRouter); * app.listen(3000); * ``` */ // biome-ignore assist/source/organizeImports: ... export * from "./schemas/builders"; export * from "./helpers/schema"; export * from "./helpers/middleware"; export * from "./server/server"; export * from "./server/router"; export * from "./types"; ================================================ FILE: packages/kitojs/src/schemas/builders.ts ================================================ // biome-ignore assist/source/organizeImports: ... import { StringSchemaImpl } from "./primitives/string"; import { NumberSchemaImpl } from "./primitives/number"; import { BooleanSchemaImpl } from "./primitives/boolean"; import { ArraySchemaImpl } from "./primitives/array"; import { ObjectSchemaImpl } from "./primitives/object"; import { LiteralSchemaImpl } from "./primitives/literal"; import { UnionSchemaImpl } from "./primitives/union"; import type { StringSchema, NumberSchema, BooleanSchema, ArraySchema, ObjectSchema, LiteralSchema, UnionSchema, SchemaType, } from "@kitojs/types"; /** * Schema builder utilities for creating type-safe request validation schemas. * * @example * ```typescript * import { t, schema } from 'kitojs'; * * const userSchema = schema({ * params: t.object({ * id: t.str().uuid() * }), * body: t.object({ * name: t.str().min(1).max(50), * email: t.str().email(), * age: t.num().min(0).optional() * }) * }); * * app.post('/users/:id', [userSchema], ctx => { * // ctx.req.params.id is validated as UUID * // ctx.req.body is type-safe and validated * }); * ``` */ export const t = { /** * Creates a string schema builder. * * @returns String schema with validation methods * * @example * ```typescript * t.str() // basic string * t.str().min(3).max(50) // length constraints * t.str().email() // email validation * t.str().uuid() // UUID validation * t.str().optional() // optional field * t.str().default('hello') // default value * ``` */ str(): StringSchema { return new StringSchemaImpl(); }, /** * Creates a number schema builder. * * @returns Number schema with validation methods * * @example * ```typescript * t.num() // basic number * t.num().min(0).max(100) // range constraints * t.num().int() // integer only * t.num().positive() // positive numbers * t.num().optional() // optional field * t.num().default(0) // default value * ``` */ num(): NumberSchema { return new NumberSchemaImpl(); }, /** * Creates a boolean schema builder. * * @returns Boolean schema with validation methods * * @example * ```typescript * t.bool() // basic boolean * t.bool().optional() // optional field * t.bool().default(false) // default value * ``` */ bool(): BooleanSchema { return new BooleanSchemaImpl(); }, /** * Creates an array schema builder. * * @template T - Type of array items * @param item - Schema for array items * @returns Array schema with validation methods * * @example * ```typescript * t.array(t.str()) // string array * t.array(t.num().positive()) // array of positive numbers * t.array(t.object({ name: t.str() })) // array of objects * t.array(t.str()).min(1).max(10) // length constraints * ``` */ array(item: T): ArraySchema { return new ArraySchemaImpl(item); }, /** * Creates an object schema builder. * * @template T - Object shape definition * @param shape - Object property schemas * @returns Object schema with validation methods * * @example * ```typescript * t.object({ * name: t.str(), * age: t.num(), * email: t.str().email().optional() * }) * * // Nested objects * t.object({ * user: t.object({ * profile: t.object({ * bio: t.str() * }) * }) * }) * ``` */ object>(shape: T): ObjectSchema { return new ObjectSchemaImpl(shape); }, /** * Creates a literal schema for exact value matching. * * @template T - Literal value type * @param value - Exact value to match * @returns Literal schema * * @example * ```typescript * t.literal('admin') // matches only "admin" * t.literal(42) // matches only 42 * t.literal(true) // matches only true * ``` */ literal(value: T): LiteralSchema { return new LiteralSchemaImpl(value); }, /** * Creates a union schema for multiple possible types. * * @template T - Array of possible schema types * @param schemas - Schemas to union * @returns Union schema * * @example * ```typescript * // String or number * t.union(t.str(), t.num()) * * // Enum-like literals * t.union( * t.literal('admin'), * t.literal('user'), * t.literal('guest') * ) * * // Complex types * t.union( * t.object({ type: t.literal('user'), name: t.str() }), * t.object({ type: t.literal('guest') }) * ) * ``` */ union(...schemas: T): UnionSchema { return new UnionSchemaImpl(schemas); }, }; ================================================ FILE: packages/kitojs/src/schemas/index.ts ================================================ // biome-ignore assist/source/organizeImports: ... export * from "./builders"; export * from "./primitives/string"; export * from "./primitives/number"; export * from "./primitives/boolean"; export * from "./primitives/array"; export * from "./primitives/object"; export * from "./primitives/literal"; export * from "./primitives/union"; ================================================ FILE: packages/kitojs/src/schemas/jsonSchema.ts ================================================ // biome-ignore assist/source/organizeImports: ... import type { SchemaType, JSONSchema, JSONSchemaString, JSONSchemaNumber, JSONSchemaBoolean, JSONSchemaArray, JSONSchemaObject, } from "@kitojs/types"; import { t } from "./builders"; export function convertJSONSchema(schema: JSONSchema): SchemaType { switch (schema.type) { case "string": return convertStringSchema(schema as JSONSchemaString); case "number": case "integer": return convertNumberSchema(schema as JSONSchemaNumber); case "boolean": return convertBooleanSchema(schema as JSONSchemaBoolean); case "array": return convertArraySchema(schema as JSONSchemaArray); case "object": return convertObjectSchema(schema as JSONSchemaObject); default: // biome-ignore lint/suspicious/noExplicitAny: ... throw new Error(`Unsupported JSON Schema type: ${(schema as any).type}`); } } function convertStringSchema(schema: JSONSchemaString): SchemaType { if (schema.const !== undefined) { let schemaBuilder = t.literal(schema.const); if (schema.default !== undefined) { // biome-ignore lint/suspicious/noExplicitAny: ... schemaBuilder = schemaBuilder.default(schema.default as string) as any; } return schemaBuilder as SchemaType; } if (schema.enum && schema.enum.length > 0) { const literals = schema.enum.map((value) => t.literal(value)); let schemaBuilder = t.union(...literals); if (schema.default !== undefined) { schemaBuilder = schemaBuilder.default( schema.default as (typeof schema.enum)[number], // biome-ignore lint/suspicious/noExplicitAny: ... ) as any; } return schemaBuilder as SchemaType; } let schemaBuilder = t.str(); if (schema.minLength !== undefined) { schemaBuilder = schemaBuilder.min(schema.minLength); } if (schema.maxLength !== undefined) { schemaBuilder = schemaBuilder.max(schema.maxLength); } if (schema.format) { switch (schema.format) { case "email": schemaBuilder = schemaBuilder.email(); break; case "uuid": schemaBuilder = schemaBuilder.uuid(); break; case "uri": schemaBuilder = schemaBuilder.url(); break; } } if (schema.pattern) { schemaBuilder = schemaBuilder.regex(new RegExp(schema.pattern)); } if (schema.default !== undefined) { // biome-ignore lint/suspicious/noExplicitAny: ... schemaBuilder = schemaBuilder.default(schema.default as string) as any; } return schemaBuilder as SchemaType; } function convertNumberSchema(schema: JSONSchemaNumber): SchemaType { if (schema.const !== undefined) { let schemaBuilder = t.literal(schema.const); if (schema.default !== undefined) { // biome-ignore lint/suspicious/noExplicitAny: ... schemaBuilder = schemaBuilder.default(schema.default as number) as any; } return schemaBuilder as SchemaType; } if (schema.enum && schema.enum.length > 0) { const literals = schema.enum.map((value) => t.literal(value)); let schemaBuilder = t.union(...literals); if (schema.default !== undefined) { schemaBuilder = schemaBuilder.default( schema.default as (typeof schema.enum)[number], // biome-ignore lint/suspicious/noExplicitAny: ... ) as any; } return schemaBuilder as SchemaType; } let schemaBuilder = t.num(); if (schema.type === "integer") { schemaBuilder = schemaBuilder.int(); } if (schema.minimum !== undefined) { schemaBuilder = schemaBuilder.min(schema.minimum); } if (schema.maximum !== undefined) { schemaBuilder = schemaBuilder.max(schema.maximum); } if (schema.default !== undefined) { // biome-ignore lint/suspicious/noExplicitAny: ... schemaBuilder = schemaBuilder.default(schema.default as number) as any; } return schemaBuilder as SchemaType; } function convertBooleanSchema(schema: JSONSchemaBoolean): SchemaType { if (schema.const !== undefined) { let schemaBuilder = t.literal(schema.const); if (schema.default !== undefined) { // biome-ignore lint/suspicious/noExplicitAny: ... schemaBuilder = schemaBuilder.default(schema.default as boolean) as any; } return schemaBuilder as SchemaType; } let schemaBuilder = t.bool(); if (schema.default !== undefined) { // biome-ignore lint/suspicious/noExplicitAny: ... schemaBuilder = schemaBuilder.default(schema.default as boolean) as any; } return schemaBuilder as SchemaType; } function convertArraySchema(schema: JSONSchemaArray): SchemaType { const itemSchema = convertJSONSchema(schema.items); let schemaBuilder = t.array(itemSchema); if (schema.minItems !== undefined) { schemaBuilder = schemaBuilder.min(schema.minItems); } if (schema.maxItems !== undefined) { schemaBuilder = schemaBuilder.max(schema.maxItems); } if (schema.default !== undefined) { // biome-ignore lint/suspicious/noExplicitAny: ... schemaBuilder = schemaBuilder.default(schema.default as any[]) as any; } return schemaBuilder as SchemaType; } function convertObjectSchema(schema: JSONSchemaObject): SchemaType { const shape: Record = {}; const requiredFields = new Set(schema.required || []); for (const [key, propSchema] of Object.entries(schema.properties)) { let convertedSchema = convertJSONSchema(propSchema); if (!requiredFields.has(key)) { // biome-ignore lint/suspicious/noExplicitAny: ... convertedSchema = (convertedSchema as any).optional(); } shape[key] = convertedSchema; } let schemaBuilder = t.object(shape); if (schema.default !== undefined && schema.default !== null) { // biome-ignore lint/suspicious/noExplicitAny: ... schemaBuilder = schemaBuilder.default(schema.default as any) as any; } return schemaBuilder as SchemaType; } ================================================ FILE: packages/kitojs/src/schemas/primitives/array.ts ================================================ import type { ArraySchema, SchemaType } from "@kitojs/types"; export class ArraySchemaImpl implements ArraySchema { // biome-ignore lint/suspicious/noExplicitAny: ... _type!: any[]; _optional = false; _default: unknown = undefined; // biome-ignore lint/suspicious/noExplicitAny: ... private constraints: any[] = []; constructor(private item: T) {} min(length: number): ArraySchema { this.constraints.push({ type: "min", value: length }); return this; } max(length: number): ArraySchema { this.constraints.push({ type: "max", value: length }); return this; } length(length: number): ArraySchema { this.constraints.push({ type: "length", value: length }); return this; } // biome-ignore lint/suspicious/noExplicitAny: ... optional(): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; return clone; } // biome-ignore lint/suspicious/noExplicitAny: ... default(value: any[]): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; clone._default = value; return clone; } _serialize() { return { type: "array", optional: this._optional, default: this._default, constraints: this.constraints, // biome-ignore lint/suspicious/noExplicitAny: ... item: (this.item as any)._serialize(), }; } } ================================================ FILE: packages/kitojs/src/schemas/primitives/boolean.ts ================================================ import type { BooleanSchema } from "@kitojs/types"; export class BooleanSchemaImpl implements BooleanSchema { _type!: boolean; _optional = false; _default: unknown = undefined; // biome-ignore lint/suspicious/noExplicitAny: ... optional(): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; return clone; } // biome-ignore lint/suspicious/noExplicitAny: ... default(value: boolean): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; clone._default = value; return clone; } _serialize() { return { type: "boolean", optional: this._optional, default: this._default, }; } } ================================================ FILE: packages/kitojs/src/schemas/primitives/literal.ts ================================================ import type { LiteralSchema } from "@kitojs/types"; export class LiteralSchemaImpl implements LiteralSchema { _type!: T; _optional = false; _default: unknown = undefined; constructor(private value: T) {} // biome-ignore lint/suspicious/noExplicitAny: ... optional(): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; return clone; } // biome-ignore lint/suspicious/noExplicitAny: ... default(value: T): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; clone._default = value; return clone; } _serialize() { return { type: "literal", optional: this._optional, default: this._default, value: this.value, }; } } ================================================ FILE: packages/kitojs/src/schemas/primitives/number.ts ================================================ import type { NumberSchema } from "@kitojs/types"; export class NumberSchemaImpl implements NumberSchema { _type!: number; _optional = false; _default: unknown = undefined; // biome-ignore lint/suspicious/noExplicitAny: ... private constraints: any[] = []; min(value: number): NumberSchema { this.constraints.push({ type: "min", value }); return this; } max(value: number): NumberSchema { this.constraints.push({ type: "max", value }); return this; } int(): NumberSchema { this.constraints.push({ type: "int" }); return this; } positive(): NumberSchema { this.constraints.push({ type: "positive" }); return this; } negative(): NumberSchema { this.constraints.push({ type: "negative" }); return this; } // biome-ignore lint/suspicious/noExplicitAny: ... optional(): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; return clone; } // biome-ignore lint/suspicious/noExplicitAny: ... default(value: number): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; clone._default = value; return clone; } _serialize() { return { type: "number", optional: this._optional, default: this._default, constraints: this.constraints, }; } } ================================================ FILE: packages/kitojs/src/schemas/primitives/object.ts ================================================ import type { InferType, ObjectSchema, SchemaType } from "@kitojs/types"; export class ObjectSchemaImpl> implements ObjectSchema { _type!: { [K in keyof T]: InferType }; _optional = false; _default: unknown = undefined; shape: T; constructor(shape: T) { this.shape = shape; } // biome-ignore lint/suspicious/noExplicitAny: ... optional(): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; return clone; } // biome-ignore lint/suspicious/noExplicitAny: ... default(value: any): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; clone._default = value; return clone; } _serialize() { // biome-ignore lint/suspicious/noExplicitAny: ... const shape: any = {}; for (const [key, schema] of Object.entries(this.shape)) { // biome-ignore lint/suspicious/noExplicitAny: ... shape[key] = (schema as any)._serialize(); } return { type: "object", optional: this._optional, default: this._default, shape, }; } } ================================================ FILE: packages/kitojs/src/schemas/primitives/string.ts ================================================ import type { StringSchema } from "@kitojs/types"; export class StringSchemaImpl implements StringSchema { _type!: string; _optional = false; _default: unknown = undefined; // biome-ignore lint/suspicious/noExplicitAny: ... private constraints: any[] = []; min(length: number): StringSchema { this.constraints.push({ type: "min", value: length }); return this; } max(length: number): StringSchema { this.constraints.push({ type: "max", value: length }); return this; } length(length: number): StringSchema { this.constraints.push({ type: "length", value: length }); return this; } email(): StringSchema { this.constraints.push({ type: "email" }); return this; } url(): StringSchema { this.constraints.push({ type: "url" }); return this; } uuid(): StringSchema { this.constraints.push({ type: "uuid" }); return this; } regex(pattern: RegExp): StringSchema { this.constraints.push({ type: "regex", value: pattern }); return this; } // biome-ignore lint/suspicious/noExplicitAny: ... optional(): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; return clone; } // biome-ignore lint/suspicious/noExplicitAny: ... default(value: string): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; clone._default = value; return clone; } _serialize() { return { type: "string", optional: this._optional, default: this._default, constraints: this.constraints, }; } } ================================================ FILE: packages/kitojs/src/schemas/primitives/union.ts ================================================ import type { SchemaType, UnionSchema } from "@kitojs/types"; export class UnionSchemaImpl implements UnionSchema { // biome-ignore lint/suspicious/noExplicitAny: ... _type!: any; _optional = false; _default: unknown = undefined; constructor(private schemas: T) {} // biome-ignore lint/suspicious/noExplicitAny: ... optional(): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; return clone; } // biome-ignore lint/suspicious/noExplicitAny: ... default(value: any): any { const clone = Object.assign( Object.create(Object.getPrototypeOf(this)), this, ); clone._optional = true; clone._default = value; return clone; } _serialize() { return { type: "union", optional: this._optional, default: this._default, // biome-ignore lint/suspicious/noExplicitAny: ... schemas: this.schemas.map((s) => (s as any)._serialize()), }; } } ================================================ FILE: packages/kitojs/src/server/analyzer.ts ================================================ import * as acorn from "acorn"; import * as walk from "acorn-walk"; export type StaticResponseType = | { type: "none" } | { type: "full_static"; method: string; status: number; headers: Record; body: string; } | { type: "param_template"; method: string; status: number; headers: Record; template: string; params: string[]; }; interface ResponseCall { method: "send" | "json" | "text" | "html"; arg?: any; usesContext: boolean; } export function analyzeHandler(handler: Function): StaticResponseType { const source = handler.toString(); try { const ast = acorn.parse(source, { ecmaVersion: 2022, sourceType: "module", }) as any; let responseCall: ResponseCall | null = null; let hasLogic = false; const usedParams = new Set(); const usedQuery = new Set(); walk.simple(ast, { CallExpression(node: any) { if ( node.callee.type === "MemberExpression" && node.callee.object.type === "MemberExpression" && node.callee.object.property.name === "res" ) { const method = node.callee.property.name; if (["send", "json", "text", "html"].includes(method)) { if (responseCall !== null) { hasLogic = true; return; } const newResponseCall: ResponseCall = { method, arg: node.arguments[0], usesContext: false, }; walk.simple(node.arguments[0], { MemberExpression(argNode: any) { if ( argNode.object.type === "Identifier" && argNode.object.name === "ctx" ) { newResponseCall.usesContext = true; } }, }); responseCall = newResponseCall; } } }, MemberExpression(node: any) { if ( node.object.type === "MemberExpression" && node.object.object.type === "MemberExpression" && node.object.object.property.name === "req" && node.object.property.name === "params" ) { if (node.property.type === "Identifier") { usedParams.add(node.property.name); } } if ( node.object.type === "MemberExpression" && node.object.object.type === "MemberExpression" && node.object.object.property.name === "req" && node.object.property.name === "query" ) { if (node.property.type === "Identifier") { usedQuery.add(node.property.name); } } }, VariableDeclaration() { hasLogic = true; }, IfStatement() { hasLogic = true; }, ForStatement() { hasLogic = true; }, WhileStatement() { hasLogic = true; }, }); if (!responseCall) { return { type: "none" }; } const rc = responseCall as ResponseCall; if (hasLogic) { return { type: "none" }; } if (!rc.usesContext) { const value = evaluateStaticValue(rc.arg, source); if (value === null) { return { type: "none" }; } const { body, contentType } = serializeResponseValue(rc.method, value); return { type: "full_static", method: rc.method, status: 200, headers: { "content-type": contentType, "content-length": body.length.toString(), }, body: body.toString("base64"), }; } if (usedParams.size > 0 || usedQuery.size > 0) { if (rc.method === "json" && rc.arg?.type === "ObjectExpression") { const template = extractTemplate(rc.arg); return { type: "param_template", method: "json", status: 200, headers: { "content-type": "application/json", }, template, params: Array.from(usedParams), }; } } return { type: "none" }; } catch (error) { console.warn("Failed to analyze handler:", error); return { type: "none" }; } } function evaluateStaticValue(node: any, source: string): any { if (!node) return null; try { switch (node.type) { case "Literal": return node.value; case "ObjectExpression": const obj: any = {}; for (const prop of node.properties) { if (prop.type === "Property" && prop.key.type === "Identifier") { const value = evaluateStaticValue(prop.value, source); if (value === null) return null; obj[prop.key.name] = value; } } return obj; case "ArrayExpression": const arr: any[] = []; for (const element of node.elements) { const value = evaluateStaticValue(element, source); if (value === null) return null; arr.push(value); } return arr; case "TemplateLiteral": if (node.expressions.length === 0) { return node.quasis[0].value.cooked; } return null; default: return null; } } catch { return null; } } function serializeResponseValue( method: string, value: any, ): { body: Buffer; contentType: string } { switch (method) { case "json": return { body: Buffer.from(JSON.stringify(value), "utf-8"), contentType: "application/json", }; case "text": case "send": return { body: Buffer.from(String(value), "utf-8"), contentType: "text/plain", }; case "html": return { body: Buffer.from(String(value), "utf-8"), contentType: "text/html", }; default: return { body: Buffer.from(String(value), "utf-8"), contentType: "text/plain", }; } } function extractTemplate(node: any): string { const parts: string[] = []; function processNode(n: any): string { if (n.type === "Literal") return JSON.stringify(n.value); if ( n.type === "MemberExpression" && n.object.type === "MemberExpression" && n.object.object.type === "MemberExpression" && n.object.object.property.name === "req" ) { const source = n.object.property.name; const key = n.property.name; return `"{{${source}.${key}}}"`; } return "null"; } const props = node.properties.map((prop: any) => { const key = prop.key.name; const value = processNode(prop.value); return `"${key}":${value}`; }); return `{${props.join(",")}}`; } ================================================ FILE: packages/kitojs/src/server/request.ts ================================================ // biome-ignore assist/source/organizeImports: ... import type { CommonHeaderNames, KitoRequest, RequestHeaders, } from "@kitojs/types"; import { getBodyBuffer, getHeader, getAllHeaders, getQueryParam, getAllQuery, getParam, getAllParams, getCookie, getAllCookies, getMethod, getUrl, getPathname, getSearch, getProtocol, getHostname, getIp, getIps, getSecure, getXhr, } from "@kitojs/kito-core"; export class RequestBuilder implements KitoRequest { // biome-ignore lint/suspicious/noExplicitAny: ... private core: any; // biome-ignore lint/complexity/noBannedTypes: ... private _body?: Buffer | JSON | {}; private _headers?: Record; private _query?: Record; private _params?: Record; private _cookies?: Record; private _method?: string; private _url?: string; private _pathname?: string; private _search?: string | null; private _protocol?: string; private _hostname?: string; private _ip?: string; private _ips?: string[]; private _secure?: boolean; private _xhr?: boolean; // biome-ignore lint/suspicious/noExplicitAny: ... constructor(requestCore: any) { this.core = requestCore; } // biome-ignore lint/suspicious/noExplicitAny: ... get body(): any { if (this._body) return this._body; const buf = getBodyBuffer(this.core); const type = this.header("content-type") ?? ""; if (type.includes("application/json")) { try { this._body = JSON.parse(buf.toString("utf-8")); } catch { this._body = {}; } } else { this._body = buf; } return this._body; } json(): T { if (typeof this.body === "object" && !Buffer.isBuffer(this.body)) { return this.body as T; } if (Buffer.isBuffer(this.body)) { return JSON.parse(this.body.toString("utf-8")) as T; } if (typeof this.body === "string") { return JSON.parse(this.body) as T; } return this.body as T; } text(): string { if (Buffer.isBuffer(this.body)) { return this.body.toString("utf-8"); } return String(this.body); } get headers(): RequestHeaders { if (!this._headers) { this._headers = getAllHeaders(this.core); } return this._headers; } header(name: CommonHeaderNames): string | undefined; header(name: string): string | undefined; header(name: string): string | undefined { const value = getHeader(this.core, name.toLowerCase()); return value ?? undefined; } get query(): Record { if (!this._query) { this._query = getAllQuery(this.core); } return this._query; } queryParam(name: string): string | string[] | undefined { const value = getQueryParam(this.core, name); if (!value) return undefined; return value.length === 1 ? value[0] : value; } get params(): Record { if (!this._params) { this._params = getAllParams(this.core); } return this._params; } param(name: string): string | undefined { return getParam(this.core, name) ?? undefined; } get cookies(): Record { if (!this._cookies) { this._cookies = getAllCookies(this.core); } return this._cookies; } cookie(name: string): string | undefined { return getCookie(this.core, name) ?? undefined; } get method(): string { if (!this._method) { this._method = getMethod(this.core); } return this._method; } get url(): string { if (!this._url) { this._url = getUrl(this.core); } return this._url; } get pathname(): string { if (!this._pathname) { this._pathname = getPathname(this.core); } return this._pathname; } get search(): string | null { if (this._search === undefined) { this._search = getSearch(this.core) ?? null; } return this._search; } get protocol(): string { if (!this._protocol) { this._protocol = getProtocol(this.core); } return this._protocol; } get hostname(): string { if (!this._hostname) { this._hostname = getHostname(this.core); } return this._hostname; } get ip(): string { if (!this._ip) { this._ip = getIp(this.core); } return this._ip; } get ips(): string[] { if (!this._ips) { this._ips = getIps(this.core); } return this._ips; } get secure(): boolean { if (this._secure === undefined) { this._secure = getSecure(this.core); } return this._secure; } get xhr(): boolean { if (this._xhr === undefined) { this._xhr = getXhr(this.core); } return this._xhr; } get originalUrl(): string { return this.url; } get raw(): { body: Buffer; headers: RequestHeaders; url: string; method: string; } { return { body: this.body, headers: this.headers, url: this.url, method: this.method, }; } } ================================================ FILE: packages/kitojs/src/server/response.ts ================================================ // biome-ignore assist/source/organizeImports: ... import type { KitoResponse, CookieOptions, SendFileOptions, CommonResponseHeaderNames, StreamWriter, SSEWriter, } from "@kitojs/types"; import { sendResponse, startStream, sendChunk, endStream, } from "@kitojs/kito-core"; import { readFileSync, statSync } from "node:fs"; const HTTP_STATUS_MESSAGES: Record = { 100: "Continue", 101: "Switching Protocols", 200: "OK", 201: "Created", 202: "Accepted", 204: "No Content", 301: "Moved Permanently", 302: "Found", 304: "Not Modified", 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable", }; interface ResponseState { status: number; headers: Map; body?: Buffer; streaming: boolean; } class StreamWriterImpl implements StreamWriter { // biome-ignore lint/suspicious/noExplicitAny: ... constructor(private channel: any) {} write(data: string | Buffer): void { const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, "utf-8"); sendChunk(this.channel, buffer); } end(data?: string | Buffer): void { if (data !== undefined) { this.write(data); } endStream(this.channel); } } class SSEWriterImpl implements SSEWriter { // biome-ignore lint/suspicious/noExplicitAny: ... constructor(private channel: any) {} send(data: unknown, event?: string, id?: string, retry?: number): void { let message = ""; if (event) { message += `event: ${event}\n`; } if (id) { message += `id: ${id}\n`; } if (retry !== undefined) { message += `retry: ${retry}\n`; } const dataStr = typeof data === "string" ? data : JSON.stringify(data); const lines = dataStr.split("\n"); for (const line of lines) { message += `data: ${line}\n`; } message += "\n"; sendChunk(this.channel, Buffer.from(message, "utf-8")); } comment(text: string): void { const message = `: ${text}\n\n`; sendChunk(this.channel, Buffer.from(message, "utf-8")); } close(): void { endStream(this.channel); } } export class ResponseBuilder implements KitoResponse { // biome-ignore lint/suspicious/noExplicitAny: ... private channel: any; private state: ResponseState; private finished = false; // biome-ignore lint/suspicious/noExplicitAny: ... constructor(responseChannel: any) { this.channel = responseChannel; this.state = { status: 200, headers: new Map(), streaming: false, }; } private checkFinished(): void { if (this.finished) { throw new Error("Response already sent"); } } private serializeAndSend(): void { const headersArray = Array.from(this.state.headers.entries()); const headersJson = JSON.stringify(headersArray); const headersBytes = Buffer.from(headersJson, "utf-8"); const body = this.state.body || Buffer.alloc(0); const totalSize = 2 + 4 + headersBytes.length + body.length; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt16LE(this.state.status, offset); offset += 2; buffer.writeUInt32LE(headersBytes.length, offset); offset += 4; headersBytes.copy(buffer, offset); offset += headersBytes.length; if (body.length > 0) { body.copy(buffer, offset); } sendResponse(this.channel, buffer); this.finished = true; } private startStreamingResponse(): void { const headersArray = Array.from(this.state.headers.entries()); const headersJson = JSON.stringify(headersArray); const headersBytes = Buffer.from(headersJson, "utf-8"); const totalSize = 2 + 4 + headersBytes.length; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt16LE(this.state.status, offset); offset += 2; buffer.writeUInt32LE(headersBytes.length, offset); offset += 4; headersBytes.copy(buffer, offset); startStream(this.channel, buffer); this.state.streaming = true; this.finished = true; } status(code: number): KitoResponse { this.checkFinished(); this.state.status = code; return this; } sendStatus(code: number): void { this.checkFinished(); const message = HTTP_STATUS_MESSAGES[code] || "Unknown"; this.state.status = code; this.state.body = Buffer.from(message, "utf-8"); this.serializeAndSend(); } header(name: CommonResponseHeaderNames, value: string): KitoResponse; header(name: string, value: string): KitoResponse; header(name: string, value: string): KitoResponse { this.checkFinished(); this.state.headers.set(name.toLowerCase(), value); return this; } headers(headers: Record): KitoResponse; headers(headers: Record): KitoResponse; headers(headers: Record): KitoResponse { this.checkFinished(); for (const [name, value] of Object.entries(headers)) { this.state.headers.set(name.toLowerCase(), value); } return this; } append(field: CommonResponseHeaderNames, value: string): KitoResponse; append(field: string, value: string): KitoResponse; append(field: string, value: string): KitoResponse { this.checkFinished(); const key = field.toLowerCase(); const existing = this.state.headers.get(key); if (existing) { this.state.headers.set(key, `${existing}, ${value}`); } else { this.state.headers.set(key, value); } return this; } set(field: CommonResponseHeaderNames, value: string): KitoResponse; set(field: string, value: string): KitoResponse; set(field: string, value: string): KitoResponse { return this.header(field, value); } get(field: CommonResponseHeaderNames): string | undefined; get(field: string): string | undefined; get(field: string): string | undefined { return this.state.headers.get(field.toLowerCase()); } type(contentType: string): KitoResponse { this.checkFinished(); if (!contentType.includes("/")) { const mimeTypes: Record = { html: "text/html", json: "application/json", xml: "application/xml", text: "text/plain", txt: "text/plain", js: "application/javascript", css: "text/css", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", svg: "image/svg+xml", pdf: "application/pdf", zip: "application/zip", }; contentType = mimeTypes[contentType] || contentType; } return this.header("content-type", contentType); } contentType(contentType: string): KitoResponse { return this.type(contentType); } cookie(name: string, value: string, options?: CookieOptions): KitoResponse { this.checkFinished(); const cookie = this.serializeCookie(name, value, options); const existing = this.state.headers.get("set-cookie"); if (existing) { this.state.headers.set("set-cookie", `${existing}, ${cookie}`); } else { this.state.headers.set("set-cookie", cookie); } return this; } private serializeCookie( name: string, value: string, options?: CookieOptions, ): string { let cookie = `${name}=${value}`; if (options?.maxAge !== undefined) { cookie += `; Max-Age=${options.maxAge}`; } if (options?.path) { cookie += `; Path=${options.path}`; } else { cookie += "; Path=/"; } if (options?.domain) { cookie += `; Domain=${options.domain}`; } if (options?.httpOnly) { cookie += "; HttpOnly"; } if (options?.secure) { cookie += "; Secure"; } if (options?.sameSite) { const sameSite = typeof options.sameSite === "string" ? options.sameSite : options.sameSite ? "Strict" : "Lax"; cookie += `; SameSite=${sameSite}`; } return cookie; } clearCookie(name: string, options?: CookieOptions): KitoResponse { this.checkFinished(); const clearOptions = { ...options, maxAge: 0, expires: new Date(0) }; return this.cookie(name, "", clearOptions); } // end methods end(): void { this.checkFinished(); this.serializeAndSend(); } send(data: unknown): void { this.checkFinished(); if (Buffer.isBuffer(data)) { this.state.body = data; } else if (typeof data === "string") { this.state.body = Buffer.from(data, "utf-8"); } else { this.state.body = Buffer.from(String(data), "utf-8"); } this.serializeAndSend(); } json(data: unknown): void { this.checkFinished(); this.type("application/json"); const jsonStr = JSON.stringify(data); this.state.body = Buffer.from(jsonStr, "utf-8"); this.serializeAndSend(); } text(data: string): void { this.checkFinished(); this.type("text/plain"); this.state.body = Buffer.from(data, "utf-8"); this.serializeAndSend(); } html(data: string): void { this.checkFinished(); this.type("text/html"); this.state.body = Buffer.from(data, "utf-8"); this.serializeAndSend(); } redirect(url: string, code?: number): void { this.checkFinished(); this.state.status = code || 302; this.header("location", url); this.serializeAndSend(); } location(url: string): KitoResponse { this.checkFinished(); return this.header("location", url); } attachment(filename?: string): KitoResponse { this.checkFinished(); if (filename) { const encodedFilename = encodeURIComponent(filename); return this.header( "content-disposition", `attachment; filename="${encodedFilename}"`, ); } return this.header("content-disposition", "attachment"); } download(path: string, filename?: string, options?: SendFileOptions): void { const name = filename || path.split("/").pop() || "download"; this.attachment(name); this.sendFile(path, options); } sendFile(path: string, options: SendFileOptions = {}): void { this.checkFinished(); try { const fullPath = options.root ? `${options.root}/${path}` : path; const stats = statSync(fullPath); this.state.body = readFileSync(fullPath); const mimeType = this.getMimeType(path); this.type(mimeType); if (options.headers) { this.headers(options.headers); } if (options.acceptRanges) { this.header("accept-ranges", "bytes"); } if (options.cacheControl !== false) { const maxAge = options.maxAge || 0; let cacheControl = `public, max-age=${Math.floor(maxAge / 1000)}`; if (options.immutable) { cacheControl += ", immutable"; } this.header("cache-control", cacheControl); } if (options.lastModified !== false) { this.header("last-modified", stats.mtime.toUTCString()); } if (options.etag !== false) { const etag = `W/"${stats.size.toString(16)}-${stats.mtime.getTime().toString(16)}"`; this.header("etag", etag); } this.serializeAndSend(); } catch (_) { this.state.status = 404; this.state.body = Buffer.from("File Not Found", "utf-8"); this.serializeAndSend(); } } private getMimeType(path: string): string { const extension = path.split(".").pop()?.toLowerCase() || ""; const mimeTypes: Record = { html: "text/html", htm: "text/html", css: "text/css", js: "application/javascript", mjs: "application/javascript", json: "application/json", xml: "application/xml", txt: "text/plain", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", svg: "image/svg+xml", webp: "image/webp", ico: "image/x-icon", pdf: "application/pdf", zip: "application/zip", wasm: "application/wasm", mp4: "video/mp4", webm: "video/webm", mp3: "audio/mpeg", wav: "audio/wav", woff: "font/woff2", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", }; return mimeTypes[extension] || "application/octet-stream"; } vary(field: string): KitoResponse { this.checkFinished(); return this.append("vary", field); } links(links: Record): KitoResponse { this.checkFinished(); const linkHeader = Object.entries(links) .map(([rel, url]) => `<${url}>; rel="${rel}"`) .join(", "); return this.header("link", linkHeader); } format(obj: Record void>): KitoResponse { this.checkFinished(); const acceptHeader = this.get("accept") || "*/*"; for (const [type, handler] of Object.entries(obj)) { if (acceptHeader.includes(type) || acceptHeader.includes("*/*")) { handler(); return this; } } const firstHandler = Object.values(obj)[0]; if (firstHandler) { firstHandler(); } return this; } stream(): StreamWriter { this.checkFinished(); if (!this.state.headers.has("content-type")) { this.type("application/octet-stream"); } this.startStreamingResponse(); return new StreamWriterImpl(this.channel); } sse(): SSEWriter { this.checkFinished(); this.type("text/event-stream"); this.header("cache-control", "no-cache"); this.header("connection", "keep-alive"); this.startStreamingResponse(); return new SSEWriterImpl(this.channel); } } ================================================ FILE: packages/kitojs/src/server/router.ts ================================================ // biome-ignore assist/source/organizeImports: ... import type { HttpMethod, MiddlewareDefinition, SchemaDefinition, RouteHandler, MiddlewareHandler, RouteChain, KitoRouterInstance, RouteDefinition, } from "@kitojs/types"; /** * Router class for Kito. * Provides HTTP routing and middleware support with the ability to mount sub-routers. * * @template TExtensions - Type of custom extensions added to the context * * @example * ```typescript * const router = new KitoRouter(); * * router.get('/', ({ res }) => { * res.send('Hello from router!'); * }); * * export default router; * ``` */ // biome-ignore lint/complexity/noBannedTypes: ... export class KitoRouter implements KitoRouterInstance { protected routes: RouteDefinition[] = []; protected middlewares: MiddlewareDefinition[] = []; protected prefix = ""; /** * Registers a global middleware that runs for all routes in this router. * * @param middleware - Middleware function or definition * @returns The router instance for chaining * * @example * ```typescript * router.use((ctx, next) => { * console.log(`${ctx.req.method} ${ctx.req.url}`); * next(); * }); * ``` */ use(middleware: MiddlewareDefinition | MiddlewareHandler): this { if (typeof middleware === "function") { this.middlewares.push({ type: "function", handler: middleware, global: true, }); } else { this.middlewares.push({ ...middleware, global: true }); } return this; } /** * Mounts a sub-router at the specified path. * * @param path - Base path for the sub-router * @param router - Router instance to mount * @returns The router instance for chaining * * @example * ```typescript * const apiRouter = router(); * apiRouter.get('/users', ({ res }) => res.json({ users: [] })); * * const app = server(); * app.mount('/api', apiRouter); * ``` */ mount(path: string, router: KitoRouter): this { const normalizedPath = this.normalizePath(path); const prefix = normalizedPath === "/" ? "" : normalizedPath; const subRouterMiddlewares = router.getMiddlewares(); const mountedRoutes = router.getRoutes().map((route) => ({ ...route, path: prefix + route.path, middlewares: [...subRouterMiddlewares, ...route.middlewares], })); this.routes.push(...mountedRoutes); return this; } /** * Registers a GET route. */ // biome-ignore lint/complexity/noBannedTypes: ... get( path: string, handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... get( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... get( path: string, handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... get( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... get( path: string, middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, schema?: TSchema, ): this { this.addRoute( "GET", path, middlewaresOrHandler, handlerOrSchema, schema, ); return this; } /** * Registers a POST route. */ // biome-ignore lint/complexity/noBannedTypes: ... post( path: string, handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... post( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... post( path: string, handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... post( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... post( path: string, middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, schema?: TSchema, ): this { this.addRoute( "POST", path, middlewaresOrHandler, handlerOrSchema, schema, ); return this; } /** * Registers a PUT route. */ // biome-ignore lint/complexity/noBannedTypes: ... put( path: string, handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... put( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... put( path: string, handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... put( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... put( path: string, middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, schema?: TSchema, ): this { this.addRoute( "PUT", path, middlewaresOrHandler, handlerOrSchema, schema, ); return this; } /** * Registers a DELETE route. */ // biome-ignore lint/complexity/noBannedTypes: ... delete( path: string, handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... delete( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... delete( path: string, handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... delete( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... delete( path: string, middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, schema?: TSchema, ): this { this.addRoute( "DELETE", path, middlewaresOrHandler, handlerOrSchema, schema, ); return this; } /** * Registers a PATCH route. */ // biome-ignore lint/complexity/noBannedTypes: ... patch( path: string, handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... patch( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... patch( path: string, handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... patch( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... patch( path: string, middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, schema?: TSchema, ): this { this.addRoute( "PATCH", path, middlewaresOrHandler, handlerOrSchema, schema, ); return this; } /** * Registers a HEAD route. */ // biome-ignore lint/complexity/noBannedTypes: ... head( path: string, handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... head( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... head( path: string, handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... head( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... head( path: string, middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, schema?: TSchema, ): this { this.addRoute( "HEAD", path, middlewaresOrHandler, handlerOrSchema, schema, ); return this; } /** * Registers an OPTIONS route. */ // biome-ignore lint/complexity/noBannedTypes: ... options( path: string, handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... options( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): this; // biome-ignore lint/complexity/noBannedTypes: ... options( path: string, handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... options( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): this; // biome-ignore lint/complexity/noBannedTypes: ... options( path: string, middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, schema?: TSchema, ): this { this.addRoute( "OPTIONS", path, middlewaresOrHandler, handlerOrSchema, schema, ); return this; } /** * Creates a route builder for chaining multiple HTTP methods on the same path. * * @param path - Base path for all routes in the chain * @param routeMiddlewares - Optional middleware to apply to all routes in the chain * @returns Route chain builder * * @example * ```typescript * router.route('/api/users') * .get(({ res }) => res.json(users)) * .post(({ res }) => res.json({ created: true })) * .end(); * ``` * * @example With middleware * ```typescript * const auth = middleware((ctx, next) => { * // authentication logic * next(); * }); * * router.route('/admin', [auth]) * .get(({ res }) => res.send('Admin dashboard')) * .post(({ res }) => res.send('Admin create')); * ``` */ route( path: string, routeMiddlewares?: MiddlewareDefinition[] | MiddlewareDefinition, ): RouteChain { const self = this; const mergeMiddlewares = ( callMiddlewares?: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), ): (MiddlewareDefinition | TSchema)[] => { const normalizedRouteMiddlewares = Array.isArray(routeMiddlewares) ? routeMiddlewares : routeMiddlewares ? [routeMiddlewares] : []; const normalizedCallMiddlewares = Array.isArray(callMiddlewares) ? callMiddlewares : callMiddlewares ? [callMiddlewares] : []; return [...normalizedRouteMiddlewares, ...normalizedCallMiddlewares] as ( | MiddlewareDefinition | TSchema )[]; }; const chain: RouteChain = { // biome-ignore lint/complexity/noBannedTypes: ... get( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain { if (typeof middlewaresOrHandler === "function") { self.addRoute( "GET", path, mergeMiddlewares(), middlewaresOrHandler, handlerOrSchema as TSchema, ); } else { self.addRoute( "GET", path, mergeMiddlewares(middlewaresOrHandler), handlerOrSchema as RouteHandler, ); } return chain; }, // biome-ignore lint/complexity/noBannedTypes: ... post( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain { if (typeof middlewaresOrHandler === "function") { self.addRoute( "POST", path, mergeMiddlewares(), middlewaresOrHandler, handlerOrSchema as TSchema, ); } else { self.addRoute( "POST", path, mergeMiddlewares(middlewaresOrHandler), handlerOrSchema as RouteHandler, ); } return chain; }, // biome-ignore lint/complexity/noBannedTypes: ... put( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain { if (typeof middlewaresOrHandler === "function") { self.addRoute( "PUT", path, mergeMiddlewares(), middlewaresOrHandler, handlerOrSchema as TSchema, ); } else { self.addRoute( "PUT", path, mergeMiddlewares(middlewaresOrHandler), handlerOrSchema as RouteHandler, ); } return chain; }, // biome-ignore lint/complexity/noBannedTypes: ... delete( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain { if (typeof middlewaresOrHandler === "function") { self.addRoute( "DELETE", path, mergeMiddlewares(), middlewaresOrHandler, handlerOrSchema as TSchema, ); } else { self.addRoute( "DELETE", path, mergeMiddlewares(middlewaresOrHandler), handlerOrSchema as RouteHandler, ); } return chain; }, // biome-ignore lint/complexity/noBannedTypes: ... patch( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain { if (typeof middlewaresOrHandler === "function") { self.addRoute( "PATCH", path, mergeMiddlewares(), middlewaresOrHandler, handlerOrSchema as TSchema, ); } else { self.addRoute( "PATCH", path, mergeMiddlewares(middlewaresOrHandler), handlerOrSchema as RouteHandler, ); } return chain; }, // biome-ignore lint/complexity/noBannedTypes: ... options( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain { if (typeof middlewaresOrHandler === "function") { self.addRoute( "OPTIONS", path, mergeMiddlewares(), middlewaresOrHandler, handlerOrSchema as TSchema, ); } else { self.addRoute( "OPTIONS", path, mergeMiddlewares(middlewaresOrHandler), handlerOrSchema as RouteHandler, ); } return chain; }, // biome-ignore lint/complexity/noBannedTypes: ... head( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain { if (typeof middlewaresOrHandler === "function") { self.addRoute( "HEAD", path, mergeMiddlewares(), middlewaresOrHandler, handlerOrSchema as TSchema, ); } else { self.addRoute( "HEAD", path, mergeMiddlewares(middlewaresOrHandler), handlerOrSchema as RouteHandler, ); } return chain; }, end(): KitoRouter { return self; }, }; return chain; } // biome-ignore lint/complexity/noBannedTypes: ... protected addRoute( method: HttpMethod, path: string, middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, schema?: TSchema, ): void { let finalHandler: RouteHandler; let middlewares: (MiddlewareDefinition | TSchema)[] = []; if (typeof middlewaresOrHandler === "function") { finalHandler = middlewaresOrHandler as RouteHandler; if (handlerOrSchema && this.isSchemaDefinition(handlerOrSchema)) { middlewares = [handlerOrSchema as TSchema]; } } else { if (Array.isArray(middlewaresOrHandler)) { middlewares = middlewaresOrHandler as ( | MiddlewareDefinition | TSchema )[]; } else { middlewares = [middlewaresOrHandler as MiddlewareDefinition | TSchema]; } finalHandler = handlerOrSchema as RouteHandler; } if (schema) { middlewares.push(schema); } const normalizedPath = this.normalizePath(path); this.routes.push({ method, path: normalizedPath, middlewares, handler: finalHandler as RouteHandler, }); } // biome-ignore lint/suspicious/noExplicitAny: ... protected isSchemaDefinition(item: any): item is SchemaDefinition { return item && (item.params || item.query || item.body || item.headers); } protected getRoutes(): RouteDefinition[] { return this.routes; } protected getMiddlewares(): MiddlewareDefinition[] { return this.middlewares; } private normalizePath(path: string): string { let normalized = path.startsWith("/") ? path : `/${path}`; if (normalized.length > 1 && normalized.endsWith("/")) { normalized = normalized.slice(0, -1); } return normalized; } } /** * Creates a new Router instance. * * @returns New router instance * * @example * ```typescript * import { router } from 'kitojs'; * * const cats = router(); * * cats.get('/', ({ res }) => { * res.send('hello from cats!'); * }); * * export default cats; * ``` */ // biome-ignore lint/complexity/noBannedTypes: ... export function router(): KitoRouter { return new KitoRouter(); } ================================================ FILE: packages/kitojs/src/server/server.ts ================================================ // biome-ignore assist/source/organizeImports: ... import type { HttpMethod, MiddlewareDefinition, SchemaDefinition, ServerOptions, RouteHandler, KitoContext, KitoServerInstance, } from "@kitojs/types"; import { ServerCore, type ServerOptionsCore } from "@kitojs/kito-core"; import { RequestBuilder } from "./request"; import { ResponseBuilder } from "./response"; import { analyzeHandler, type StaticResponseType } from "./analyzer"; import { KitoRouter } from "./router"; /** * Main server class for Kito framework. * Extends Router to provide HTTP routing, middleware support, and adds server-specific functionality. * * @template TExtensions - Type of custom extensions added to the context * * @example * ```typescript * const app = new KitoServer(); * * app.get('/', ctx => { * ctx.res.send('Hello World!'); * }); * * app.listen(3000); * ``` */ // biome-ignore lint/complexity/noBannedTypes: ... export class KitoServer extends KitoRouter implements KitoServerInstance { private serverOptions: ServerOptions = {}; private coreServer: ServerCore; // biome-ignore lint/suspicious/noExplicitAny: ... private extensionFn?: (ctx: any) => void; /** * Creates a new Kito server instance. * * @param options - Server configuration options * @param options.port - Port to listen on (default: 3000) * @param options.host - Host to bind to (default: "0.0.0.0") * @param options.trustProxy - Trust X-Forwarded-* headers * @param options.maxRequestSize - Maximum request body size in bytes * @param options.timeout - Request timeout in milliseconds */ constructor(options?: ServerOptions) { super(); this.serverOptions = { ...this.serverOptions, ...options }; this.coreServer = new ServerCore({ port: options?.unixSocket ? undefined : options?.port, host: options?.unixSocket ? undefined : options?.host, unixSocket: options?.unixSocket, reusePort: options?.reusePort, trustProxy: options?.trustProxy, maxRequestSize: options?.maxRequestSize, timeout: options?.timeout, }); } /** * Extends the request context with custom properties or methods. * * @template TNewExtensions - Type of the new extensions * @param fn - Function that adds extensions to the context * @returns A new server instance with extended context type * * @example * ```typescript * interface Database { * query: (sql: string) => Promise; * } * * const app = server().extend<{ db: Database }>(ctx => { * ctx.db = createDatabase(); * }); * * app.get('/users', ctx => { * const users = await ctx.db.query('SELECT * FROM users'); * ctx.res.json(users); * }); * ``` */ extend( fn: ( ctx: KitoContext & TExtensions & Partial, // biome-ignore lint/suspicious/noConfusingVoidType: ... ) => TNewExtensions | void, ): KitoServer { const newServer = new KitoServer( this.serverOptions, ); newServer.middlewares = [...this.middlewares]; newServer.routes = [...this.routes]; newServer.coreServer = this.coreServer; const previousExtensionFn = this.extensionFn; // biome-ignore lint/suspicious/noExplicitAny: ... newServer.extensionFn = (ctx: any) => { if (previousExtensionFn) { previousExtensionFn(ctx); } const result = fn(ctx); if (result && typeof result === "object") { Object.assign(ctx, result); } }; return newServer; } // biome-ignore lint/complexity/noBannedTypes: ... protected override addRoute( method: HttpMethod, path: string, middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | RouteHandler, handler?: RouteHandler, schema?: TSchema, ): void { super.addRoute(method, path, middlewaresOrHandler, handler, schema); const route = this.routes[this.routes.length - 1]; this.registerRouteWithCore(route); } override mount(path: string, router: KitoRouter): this { const routeIndex = this.routes.length; super.mount(path, router); for (let i = routeIndex; i < this.routes.length; i++) { this.registerRouteWithCore(this.routes[i]); } return this; } private registerRouteWithCore( // biome-ignore lint/suspicious/noExplicitAny: ... route: any, ): void { let finalHandler: RouteHandler; let middlewares: (MiddlewareDefinition | SchemaDefinition)[] = []; finalHandler = route.handler; middlewares = route.middlewares || []; const routeMiddlewares: MiddlewareDefinition[] = []; let routeSchema: SchemaDefinition | undefined; for (const item of middlewares) { if (this.isSchemaDefinition(item)) { routeSchema = item as SchemaDefinition; routeMiddlewares.push({ type: "schema", // biome-ignore lint/suspicious/noExplicitAny: ... schema: item as any, global: false, }); } else { routeMiddlewares.push({ ...(item as MiddlewareDefinition), global: false, }); } } let staticResponse: StaticResponseType = { type: "none" }; if (this.middlewares.length === 0 && routeMiddlewares.length === 0) { staticResponse = analyzeHandler(finalHandler); } const fusedHandler = this.fuseMiddlewares( this.middlewares, routeMiddlewares, finalHandler, ); const routeHandler = async (ctx: KitoContext) => { const reqBuilder = new RequestBuilder(ctx.req); const resBuilder = new ResponseBuilder(ctx.res); // biome-ignore lint/suspicious/noExplicitAny: ... const context: any = { req: reqBuilder, res: resBuilder }; if (this.extensionFn) { this.extensionFn(context); } await fusedHandler(context); }; const schemaJson = routeSchema ? this.serializeSchema(routeSchema) : undefined; const staticResponseJson = staticResponse.type !== "none" ? JSON.stringify(staticResponse) : undefined; this.coreServer.addRoute({ method: route.method, path: route.path, handler: routeHandler, schema: schemaJson, staticResponse: staticResponseJson, }); } private serializeSchema(schema: SchemaDefinition): string { // biome-ignore lint/suspicious/noExplicitAny: ... const serialized: any = {}; if (schema.params) { // biome-ignore lint/suspicious/noExplicitAny: ... serialized.params = (schema.params as any)._serialize(); } if (schema.query) { // biome-ignore lint/suspicious/noExplicitAny: ... serialized.query = (schema.query as any)._serialize(); } if (schema.body) { // biome-ignore lint/suspicious/noExplicitAny: ... serialized.body = (schema.body as any)._serialize(); } if (schema.headers) { // biome-ignore lint/suspicious/noExplicitAny: ... serialized.headers = (schema.headers as any)._serialize(); } return JSON.stringify(serialized); } private registerCatchAllRoute(): void { const hasCatchAll = this.routes.some( (route) => route.path === "{*path}" || route.path === "/*" || route.path === "*", ); if (hasCatchAll) return; const catchAllHandler: RouteHandler = ( ctx, ) => { try { ctx.res.status(404).send("Not Found"); } catch {} }; const fusedHandler = this.fuseMiddlewares( this.middlewares, [], catchAllHandler, ); const routeHandler = async (ctx: KitoContext) => { const reqBuilder = new RequestBuilder(ctx.req); const resBuilder = new ResponseBuilder(ctx.res); // biome-ignore lint/suspicious/noExplicitAny: ... const context: any = { req: reqBuilder, res: resBuilder }; if (this.extensionFn) { this.extensionFn(context); } await fusedHandler(context); }; const methods: HttpMethod[] = [ "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", ]; for (const method of methods) { try { this.coreServer.addRoute({ method, path: "/{*path}", handler: routeHandler, schema: undefined, staticResponse: undefined, }); } catch (e) { // Suppress conflict error } } } private fuseMiddlewares( globals: MiddlewareDefinition[], routeMiddlewares: MiddlewareDefinition[], handler: RouteHandler, ): RouteHandler { const functions = [ ...globals .filter((m) => m.type === "function" && m.handler) // biome-ignore lint/style/noNonNullAssertion: ... .map((m) => m.handler!), ...routeMiddlewares .filter((m) => m.type === "function" && m.handler) // biome-ignore lint/style/noNonNullAssertion: ... .map((m) => m.handler!), ]; if (functions.length === 0) return handler as RouteHandler; // biome-ignore lint/suspicious/noExplicitAny: ... return async (ctx: any) => { let i = 0; // biome-ignore lint/suspicious/noExplicitAny: ... const next = async (): Promise => { if (i < functions.length) { const fn = functions[i++]; return fn(ctx, next); } else { return handler(ctx); } }; return next(); }; } /** * Starts the server. * * You can call `listen` in multiple ways: * * **1. Using a port (classic usage)** * ```ts * app.listen(3000); * app.listen(3000, "0.0.0.0"); * app.listen(3000, () => console.log("Ready")); * app.listen(3000, "0.0.0.0", () => console.log("Ready")); * ``` * * **2. Using an options object** * ```ts * app.listen({ * port: 3000, * host: "0.0.0.0", * }); * ``` * * **3. Listening on a Unix Domain Socket** * ```ts * app.listen({ * unixSocket: "/tmp/kito.sock", * }); * ``` * * When `unixSocket` is provided: * - `port` and `host` are ignored * - The server binds exclusively to the provided socket path * * You may also pass a callback as the last argument in all forms. * * @param portOrCallbackOrOptions Port number, callback, or a full `ServerOptions` object. * @param hostOrCallback Hostname or callback. * @param maybeCallback Optional callback executed once the server starts. * @returns The resolved server configuration. */ async listen( portOrCallbackOrOptions?: number | (() => void) | ServerOptions, hostOrCallback?: string | (() => void), maybeCallback?: () => void, ): Promise { let port: number | undefined; let host: string | undefined; let unixSocket: string | undefined; let reusePort: boolean | undefined; let ready: (() => void) | undefined; if (typeof portOrCallbackOrOptions === "object") { const options = portOrCallbackOrOptions; port = options.port; host = options.host; unixSocket = options.unixSocket; reusePort = options.reusePort; ready = hostOrCallback as (() => void) | undefined; } else if (typeof portOrCallbackOrOptions === "function") { ready = portOrCallbackOrOptions; } else { port = portOrCallbackOrOptions; if (typeof hostOrCallback === "function") { ready = hostOrCallback; } else { host = hostOrCallback; ready = maybeCallback; } } const finalUnixSocket = unixSocket ?? this.serverOptions.unixSocket; const finalReusePort = reusePort ?? this.serverOptions.reusePort ?? false; const finalPort = finalUnixSocket ? undefined : (port ?? this.serverOptions.port ?? 3000); const finalHost = finalUnixSocket ? undefined : (host ?? this.serverOptions.host ?? "0.0.0.0"); if (this.middlewares.length > 0) { this.registerCatchAllRoute(); } const configuration: ServerOptionsCore = { port: finalPort, host: finalHost, unixSocket: finalUnixSocket, reusePort: finalReusePort, trustProxy: this.serverOptions.trustProxy, maxRequestSize: this.serverOptions.maxRequestSize, timeout: this.serverOptions.timeout, }; this.coreServer.setConfig(configuration); await this.coreServer.start(ready); return configuration; } /** * Closes the server and stops accepting new connections. */ close(): void { this.coreServer.close(); } } /** * Creates a new Kito server instance. * * @param options - Server configuration options * @returns New server instance * * @example * ```typescript * import { server } from 'kitojs'; * * const app = server(); * * app.get('/', ctx => { * ctx.res.send('Hello World!'); * }); * * app.listen(3000); * ``` */ // biome-ignore lint/complexity/noBannedTypes: ... export function server(options?: ServerOptions): KitoServer<{}> { return new KitoServer(options); } ================================================ FILE: packages/kitojs/src/types.ts ================================================ export type { Context, KitoContext } from "@kitojs/types"; export type { CommonHeaderNames, RequestHeaders, ParsedUrl, KitoRequest, } from "@kitojs/types"; export type { CommonResponseHeaderNames, SendFileOptions, CookieOptions, StreamWriter, SSEWriter, KitoResponse, } from "@kitojs/types"; export type { NextFunction, MiddlewareHandler, RouteHandler, } from "@kitojs/types"; export type { HttpMethod, RouteDefinition, MiddlewareDefinition, RouteChain, KitoRouterInstance, ServerOptions, KitoServerInstance, } from "@kitojs/types"; ================================================ FILE: packages/kitojs/tests/analyzer.test.ts ================================================ import { describe, expect, it } from "vitest"; import { analyzeHandler } from "../src/server/analyzer"; import type { Context } from "@kitojs/types"; describe("Static Response Analyzer", () => { it("should detect full static response", () => { const handler = (ctx: Context) => { ctx.res.json({ message: "hello" }); }; const result = analyzeHandler(handler); expect(result.type).toBe("full_static"); if (result.type === "full_static") { expect(result.status).toBe(200); expect(result.method).toBe("json"); expect(result.headers["content-type"]).toBe("application/json"); } }); it("should detect static text response", () => { const handler = (ctx: Context) => { ctx.res.text("hello world"); }; const result = analyzeHandler(handler); expect(result.type).toBe("full_static"); if (result.type === "full_static") { expect(result.method).toBe("text"); expect(result.headers["content-type"]).toBe("text/plain"); } }); it("should detect static HTML response", () => { const handler = (ctx: Context) => { ctx.res.html("

Hello

"); }; const result = analyzeHandler(handler); expect(result.type).toBe("full_static"); if (result.type === "full_static") { expect(result.method).toBe("html"); expect(result.headers["content-type"]).toBe("text/html"); } }); it("should detect non-static response with logic", () => { const handler = (ctx: Context) => { const data = Math.random(); ctx.res.json({ data }); }; const result = analyzeHandler(handler); expect(result.type).toBe("none"); }); it("should detect non-static response with if statement", () => { const handler = (ctx: Context) => { if (ctx.req.query.admin) { ctx.res.send("admin"); } else { ctx.res.send("user"); } }; const result = analyzeHandler(handler); expect(result.type).toBe("none"); }); it("should detect param template response", () => { const handler = (ctx: Context) => { ctx.res.json({ id: ctx.req.params.id, name: ctx.req.params.name, }); }; const result = analyzeHandler(handler); expect(result.type).toBe("param_template"); if (result.type === "param_template") { expect(result.params).toContain("id"); expect(result.params).toContain("name"); expect(result.template).toContain("{{params.id}}"); expect(result.template).toContain("{{params.name}}"); } }); it("should detect none for complex handlers", () => { const handler = async (ctx: Context) => { const user = await fetchUser(ctx.req.params.id); ctx.res.json(user); }; const result = analyzeHandler(handler); expect(result.type).toBe("none"); }); it("should detect none for multiple response calls", () => { const handler = (ctx: Context) => { ctx.res.send("first"); ctx.res.send("second"); }; const result = analyzeHandler(handler); expect(result.type).toBe("none"); }); it("should handle arrow functions", () => { const handler = (ctx: Context) => { ctx.res.send("arrow"); }; const result = analyzeHandler(handler); expect(result.type).toBe("full_static"); }); it("should detect static object response", () => { const handler = (ctx: Context) => { ctx.res.json({ status: "ok", version: "1.0.0", features: ["fast", "secure"], }); }; const result = analyzeHandler(handler); expect(result.type).toBe("full_static"); }); it("should detect none for variable declarations", () => { const handler = (ctx: Context) => { const message = "hello"; ctx.res.send(message); }; const result = analyzeHandler(handler); expect(result.type).toBe("none"); }); }); function fetchUser(id: string) { return Promise.resolve({ id, name: "User" }); } ================================================ FILE: packages/kitojs/tests/middleware.test.ts ================================================ import { describe, expect, it } from "vitest"; import { middleware } from "../src"; describe("Middleware Helper", () => { it("should create middleware definition", () => { const mw = middleware((_, next) => { next(); }); expect(mw).toHaveProperty("type", "function"); expect(mw).toHaveProperty("handler"); expect(mw).toHaveProperty("global", false); expect(typeof mw.handler).toBe("function"); }); it("should preserve handler function", () => { // biome-ignore lint/suspicious/noExplicitAny: ... const handler = (ctx: any, next: any) => { ctx.custom = "value"; next(); }; const mw = middleware(handler); expect(mw.handler).toBe(handler); }); it("should have correct structure", () => { const mw = middleware((_, next) => next()); expect(mw.type).toBe("function"); expect(mw.global).toBe(false); expect(mw.handler).toBeDefined(); }); it("should support async handlers", () => { const asyncMw = middleware(async (_, next) => { await Promise.resolve(); next(); }); expect(asyncMw.handler).toBeDefined(); expect(asyncMw.handler?.constructor.name).toBe("AsyncFunction"); }); it("should work with route() method", async () => { const { server } = await import("../src"); const executionLog: string[] = []; const authMiddleware = middleware((ctx, next) => { executionLog.push("auth"); next(); }); const app = server(); app .route("/admin", [authMiddleware]) .get(({ res }) => { executionLog.push("get-handler"); res.send("Admin GET"); }) .post(({ res }) => { executionLog.push("post-handler"); res.send("Admin POST"); }); const routes = app["routes"]; expect(routes).toHaveLength(2); expect(routes[0].middlewares).toHaveLength(1); expect(routes[1].middlewares).toHaveLength(1); expect(routes[0].middlewares[0]).toBe(authMiddleware); expect(routes[1].middlewares[0]).toBe(authMiddleware); }); it("should combine route and call-level middlewares", async () => { const { server, middleware } = await import("../src"); const routeLevelMw = middleware((_, next) => next()); const callLevelMw = middleware((_, next) => next()); const app = server(); app.route("/admin", [routeLevelMw]).get([callLevelMw], ({ res }) => { res.send("Admin"); }); const routes = app["routes"]; expect(routes).toHaveLength(1); expect(routes[0].middlewares).toHaveLength(2); expect(routes[0].middlewares[0]).toBe(routeLevelMw); expect(routes[0].middlewares[1]).toBe(callLevelMw); }); it("should execute route middleware before handler", async () => { const { server, middleware } = await import("../src"); const executionOrder: string[] = []; const authMw = middleware((_, next) => { executionOrder.push("route-middleware"); next(); }); const app = server(); app.route("/protected", [authMw]).get(({ res }) => { executionOrder.push("handler"); res.send("Protected"); }); const routes = app["routes"]; expect(routes[0].middlewares[0]).toBe(authMw); }); }); ================================================ FILE: packages/kitojs/tests/route-chain.test.ts ================================================ import type { SchemaDefinition } from "@kitojs/types"; import { describe, expect, it, vi } from "vitest"; import { middleware, router, server } from "../src"; describe("Route Chaining", () => { describe("Server Chaining", () => { it("should support fluent API (method chaining)", () => { const app = server(); expect(() => { app .get("/", (ctx) => ctx.res.send("home")) .post("/users", (ctx) => ctx.res.json({ created: true })) .get("/about", (ctx) => ctx.res.send("about")); }).not.toThrow(); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; expect(routes).toHaveLength(3); }); it("should use route() builder", () => { const app = server(); const routes = app.route("/api"); expect(typeof routes.get).toBe("function"); expect(typeof routes.post).toBe("function"); expect(typeof routes.end).toBe("function"); }); it("should chain routes and end", () => { const app = server(); expect(() => { app .route("/api") .get((ctx) => ctx.res.send("get")) .post((ctx) => ctx.res.send("post")) .end() .get("/", (ctx) => ctx.res.send("home")); }).not.toThrow(); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; expect(routes).toHaveLength(3); }); it("should work correctly with server().route() chaining and core registration", async () => { const app = server(); // biome-ignore lint/complexity/useLiteralKeys: ... const spy = vi.spyOn(app["coreServer"], "addRoute"); app .route("/direct") .get((ctx) => ctx.res.send("get")) .post((ctx) => ctx.res.send("post")); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; expect( routes.find((r) => r.path === "/direct" && r.method === "GET"), ).toBeDefined(); expect( routes.find((r) => r.path === "/direct" && r.method === "POST"), ).toBeDefined(); // Verify it's actually registered in the core server via spy expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ path: "/direct", method: "GET" }), ); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ path: "/direct", method: "POST" }), ); }); it("should correctly handle schema in server().route() chaining", () => { const app = server(); // biome-ignore lint/complexity/useLiteralKeys: ... const spy = vi.spyOn(app["coreServer"], "addRoute"); // Use proper schema builder mock const userSchema = { params: { _serialize: () => JSON.stringify({ id: "string" }), }, }; app.route("/search").get((ctx) => ctx.res.send("ok"), userSchema as any); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ path: "/search", method: "GET", schema: JSON.stringify({ params: JSON.stringify({ id: "string" }) }), }), ); }); }); describe("Router Chaining", () => { it("should handle route chaining", () => { const r = router(); r.route("/users") .get((ctx) => ctx.res.send("list")) .post((ctx) => ctx.res.send("create")); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = r["routes"]; expect( routes.find((r) => r.method === "GET" && r.path === "/users"), ).toBeDefined(); expect( routes.find((r) => r.method === "POST" && r.path === "/users"), ).toBeDefined(); }); it("should support all HTTP methods in a chain", () => { const r = router(); r.route("/multi") .get((ctx) => ctx.res.send("get")) .post((ctx) => ctx.res.send("post")) .put((ctx) => ctx.res.send("put")) .delete((ctx) => ctx.res.send("delete")) .patch((ctx) => ctx.res.send("patch")) .head((ctx) => ctx.res.send("head")) .options((ctx) => ctx.res.send("options")); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = r["routes"]; expect(routes).toHaveLength(7); const methods = routes.map((r) => r.method); expect(methods).toContain("GET"); expect(methods).toContain("POST"); expect(methods).toContain("PUT"); expect(methods).toContain("DELETE"); expect(methods).toContain("PATCH"); expect(methods).toContain("HEAD"); expect(methods).toContain("OPTIONS"); }); it("should apply route-level middlewares to all methods in the chain", () => { const mw = middleware((_ctx, next) => next()); const r = router(); r.route("/chain-mw", [mw]) .get((ctx) => ctx.res.send("ok")) .post((ctx) => ctx.res.send("ok")); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = r["routes"]; expect(routes[0].middlewares).toHaveLength(1); expect(routes[1].middlewares).toHaveLength(1); expect(routes[0].middlewares[0]).toBe(mw); expect(routes[1].middlewares[0]).toBe(mw); }); it("should correctly combine route-level and method-level middlewares", () => { const routeMw = middleware((_ctx, next) => next()); const methodMw = middleware((_ctx, next) => next()); const r = router(); r.route("/combined", [routeMw]).get([methodMw], (ctx) => ctx.res.send("ok"), ); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = r["routes"]; expect(routes[0].middlewares).toHaveLength(2); expect(routes[0].middlewares[0]).toBe(routeMw); expect(routes[0].middlewares[1]).toBe(methodMw); }); it("should support schema application for individual methods in a chain", () => { const schema = { params: { id: "string" } }; const r = router(); r.route("/schema").get( (ctx) => ctx.res.send("ok"), schema as unknown as SchemaDefinition, ); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = r["routes"]; // middlewares should contain the schema if provided expect(routes[0].middlewares).toContain(schema); }); it("should work correctly when the router is mounted on a server", () => { const sub = router(); sub .route("/resource") .get((ctx) => ctx.res.send("get")) .post((ctx) => ctx.res.send("post")); const app = server(); app.mount("/api", sub); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; expect( routes.find((r) => r.path === "/api/resource" && r.method === "GET"), ).toBeDefined(); expect( routes.find((r) => r.path === "/api/resource" && r.method === "POST"), ).toBeDefined(); }); }); }); ================================================ FILE: packages/kitojs/tests/router.test.ts ================================================ import type { Route } from "@kitojs/kito-core"; import type { MiddlewareDefinition } from "@kitojs/types"; import { describe, expect, it, vi } from "vitest"; import { middleware, router, server } from "../src"; describe("Router", () => { describe("Basic Routing", () => { it("should register routes with different HTTP methods", () => { const r = router(); r.get("/get", (ctx) => ctx.res.send("get")); r.post("/post", (ctx) => ctx.res.send("post")); r.put("/put", (ctx) => ctx.res.send("put")); r.delete("/delete", (ctx) => ctx.res.send("delete")); r.patch("/patch", (ctx) => ctx.res.send("patch")); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = r["routes"]; expect( routes.find((r) => r.method === "GET" && r.path === "/get"), ).toBeDefined(); expect( routes.find((r) => r.method === "POST" && r.path === "/post"), ).toBeDefined(); expect( routes.find((r) => r.method === "PUT" && r.path === "/put"), ).toBeDefined(); expect( routes.find((r) => r.method === "DELETE" && r.path === "/delete"), ).toBeDefined(); expect( routes.find((r) => r.method === "PATCH" && r.path === "/patch"), ).toBeDefined(); }); it("should handle route chaining", () => { const r = router(); r.route("/users") .get((ctx) => ctx.res.send("list")) .post((ctx) => ctx.res.send("create")); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = r["routes"]; expect( routes.find((r) => r.method === "GET" && r.path === "/users"), ).toBeDefined(); expect( routes.find((r) => r.method === "POST" && r.path === "/users"), ).toBeDefined(); }); }); describe("Middleware", () => { it("should work with NO middleware", async () => { const api = router(); api.get("/ping", ({ res }) => res.send("pong")); const app = server(); app.mount("/api", api); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; const route = routes.find((r) => r.path === "/api/ping"); expect(route).toBeDefined(); expect(route?.middlewares).toHaveLength(0); }); it("should register local middlewares for a route", () => { const mw = middleware((_ctx, next) => next()); const r = router(); r.get("/test", [mw], (ctx) => ctx.res.send("ok")); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = r["routes"]; const route = routes.find((r) => r.path === "/test"); expect(route?.middlewares).toHaveLength(1); expect((route?.middlewares[0] as MiddlewareDefinition).handler).toBe( mw.handler, ); }); it("should register global middlewares for all routes", () => { const mw = middleware((_ctx, next) => next()); const r = router(); r.use(mw); r.get("/test", (ctx) => ctx.res.send("ok")); // biome-ignore lint/complexity/useLiteralKeys: ... expect(r["getMiddlewares"]()).toHaveLength(1); }); it("should handle middleware that throws an error", async () => { const app = server(); const mw = middleware(() => { throw new Error("Middleware Error"); }); // biome-ignore lint/complexity/useLiteralKeys: ... const addRouteSpy = vi.spyOn(app["coreServer"], "addRoute"); app.get("/error", [mw], (ctx) => ctx.res.send("ok")); const call = addRouteSpy.mock.calls.find( (c) => (c[0] as Route).path === "/error", ); if (!call) throw new Error("Route not registered"); const handler = (call[0] as Route).handler; const mockCtx = { req: { method: "GET", url: "/error", headers: {}, params: {}, query: {}, body: {}, }, res: { status: vi.fn().mockReturnThis(), send: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(), header: vi.fn().mockReturnThis(), cookie: vi.fn().mockReturnThis(), }, }; await expect(handler(mockCtx)).rejects.toThrow("Middleware Error"); }); }); describe("Router Mounting", () => { describe("Middleware Inheritance", () => { it("should correctly propagate sub-router middleware", async () => { const mw = middleware((_ctx, next) => next()); const api = router(); api.use(mw); api.get("/test", ({ res }) => res.send("ok")); const app = server(); app.mount("/api", api); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; const route = routes.find((r) => r.path === "/api/test"); expect(route).toBeDefined(); expect(route?.middlewares).toHaveLength(1); const firstMw = route?.middlewares[0] as MiddlewareDefinition; if (firstMw) { expect(firstMw.handler).toBe(mw.handler); expect(firstMw.global).toBe(true); } else { throw new Error("Middleware should be defined"); } }); it("should preserve order: sub-router middleware then route middleware", async () => { const mwGlobal = middleware((_ctx, next) => next()); const mwRoute = middleware((_ctx, next) => next()); const api = router(); api.use(mwGlobal); api.get("/test", [mwRoute], ({ res }) => res.send("ok")); const app = server(); app.mount("/api", api); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; const route = routes.find((r) => r.path === "/api/test"); expect(route?.middlewares).toHaveLength(2); const firstMw = route?.middlewares[0] as MiddlewareDefinition; const secondMw = route?.middlewares[1] as MiddlewareDefinition; if (firstMw && secondMw) { expect(firstMw.handler).toBe(mwGlobal.handler); expect(secondMw.handler).toBe(mwRoute.handler); } else { throw new Error("Middlewares should be defined"); } }); it("should handle nested router mounting and maintain order", async () => { const mwA = middleware((_ctx, next) => next()); const mwB = middleware((_ctx, next) => next()); const mwC = middleware((_ctx, next) => next()); const routerC = router(); routerC.use(mwC); routerC.get("/end", ({ res }) => res.send("done")); const routerB = router(); routerB.use(mwB); routerB.mount("/c", routerC); const routerA = router(); routerA.use(mwA); routerA.mount("/b", routerB); const app = server(); app.mount("/sub", routerA); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; const route = routes.find((r) => r.path === "/sub/b/c/end"); expect(route).toBeDefined(); expect(route?.middlewares).toHaveLength(3); if (route) { const mws = route.middlewares as MiddlewareDefinition[]; expect(mws[0].handler).toBe(mwA.handler); expect(mws[1].handler).toBe(mwB.handler); expect(mws[2].handler).toBe(mwC.handler); } }); it("should handle deep nested mounting with correct middleware order", async () => { const m1 = middleware((_ctx, next) => next()); const m2 = middleware((_ctx, next) => next()); const m3 = middleware((_ctx, next) => next()); const r3 = router() .use(m3) .get("/p3", ({ res }) => res.send("3")); const r2 = router().use(m2).mount("/r3", r3); const r1 = router().use(m1).mount("/r2", r2); const app = server(); app.mount("/r1", r1); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; const route = routes.find((r) => r.path === "/r1/r2/r3/p3"); expect(route).toBeDefined(); expect(route?.middlewares).toHaveLength(3); if (route) { const mws = route.middlewares as MiddlewareDefinition[]; expect(mws[0].handler).toBe(m1.handler); expect(mws[1].handler).toBe(m2.handler); expect(mws[2].handler).toBe(m3.handler); } }); }); describe("Isolation", () => { it("should ensure middleware isolation between sub-routers", async () => { const mw1 = middleware((_ctx, next) => next()); const mw2 = middleware((_ctx, next) => next()); const api1 = router(); api1.use(mw1); api1.get("/r1", ({ res }) => res.send("ok")); const api2 = router(); api2.use(mw2); api2.get("/r2", ({ res }) => res.send("ok")); const app = server(); app.mount("/api1", api1); app.mount("/api2", api2); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; const route1 = routes.find((r) => r.path === "/api1/r1"); const route2 = routes.find((r) => r.path === "/api2/r2"); expect(route1?.middlewares).toHaveLength(1); expect(route2?.middlewares).toHaveLength(1); const mw1Entry = route1?.middlewares[0] as MiddlewareDefinition; const mw2Entry = route2?.middlewares[0] as MiddlewareDefinition; if (mw1Entry) expect(mw1Entry.handler).toBe(mw1.handler); if (mw2Entry) expect(mw2Entry.handler).toBe(mw2.handler); // Ensure NO bleeding expect( route1?.middlewares.some( (m) => (m as MiddlewareDefinition).handler === mw2.handler, ), ).toBe(false); expect( route2?.middlewares.some( (m) => (m as MiddlewareDefinition).handler === mw1.handler, ), ).toBe(false); }); it("should avoid middleware bleeding between siblings in deep nesting", async () => { const common = middleware((_ctx, next) => next()); const leftMw = middleware((_ctx, next) => next()); const rightMw = middleware((_ctx, next) => next()); const left = router() .use(leftMw) .get("/l", ({ res }) => res.send("l")); const right = router() .use(rightMw) .get("/r", ({ res }) => res.send("r")); const root = router() .use(common) .mount("/left", left) .mount("/right", right); const app = server(); app.mount("/api", root); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; const leftRoute = routes.find((r) => r.path === "/api/left/l"); const rightRoute = routes.find((r) => r.path === "/api/right/r"); expect(leftRoute?.middlewares).toHaveLength(2); expect(rightRoute?.middlewares).toHaveLength(2); const leftMws = (leftRoute?.middlewares || []) as MiddlewareDefinition[]; const rightMws = (rightRoute?.middlewares || []) as MiddlewareDefinition[]; expect(leftMws[0].handler).toBe(common.handler); expect(leftMws[1].handler).toBe(leftMw.handler); expect(rightMws[0].handler).toBe(common.handler); expect(rightMws[1].handler).toBe(rightMw.handler); expect(leftMws.some((m) => m.handler === rightMw.handler)).toBe(false); expect(rightMws.some((m) => m.handler === leftMw.handler)).toBe(false); }); it("should ensure middleware defined in router A is NOT applied in router B", async () => { const mwA = middleware((_ctx, next) => next()); const mwB = middleware((_ctx, next) => next()); const routerA = router() .use(mwA) .get("/a", ({ res }) => res.send("a")); const routerB = router() .use(mwB) .get("/b", ({ res }) => res.send("b")); const app = server(); app.mount("/group", routerA); app.mount("/group", routerB); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; const routeA = routes.find((r) => r.path === "/group/a"); const routeB = routes.find((r) => r.path === "/group/b"); expect(routeA?.middlewares).toHaveLength(1); expect(routeB?.middlewares).toHaveLength(1); expect( routeA?.middlewares.some( (m) => (m as MiddlewareDefinition).handler === mwB.handler, ), ).toBe(false); expect( routeB?.middlewares.some( (m) => (m as MiddlewareDefinition).handler === mwA.handler, ), ).toBe(false); }); }); it("should handle mixed route mounting: direct routes and sub-routers", async () => { const subMw = middleware((_ctx, next) => next()); const sub = router(); sub.use(subMw); sub.get("/sub-route", ({ res }) => res.send("ok")); const app = server(); app.get("/direct", ({ res }) => res.send("ok")); app.mount("/mounted", sub); // biome-ignore lint/complexity/useLiteralKeys: ... const appRoutes = app["routes"]; const directRoute = appRoutes.find((r) => r.path === "/direct"); const mountedRoute = appRoutes.find( (r) => r.path === "/mounted/sub-route", ); expect(directRoute?.middlewares).toHaveLength(0); expect(mountedRoute?.middlewares).toHaveLength(1); const firstMw = mountedRoute?.middlewares[0] as MiddlewareDefinition; if (firstMw) { expect(firstMw.handler).toBe(subMw.handler); } }); it("should handle mounting at root path '/'", () => { const sub = router(); sub.get("/test", (ctx) => ctx.res.send("ok")); const app = server(); app.mount("/", sub); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; expect(routes.find((r) => r.path === "/test")).toBeDefined(); }); it("should handle mounting with trailing slashes", () => { const sub = router(); sub.get("/test/", (ctx) => ctx.res.send("ok")); const app = server(); app.mount("/api/", sub); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; expect(routes.find((r) => r.path === "/api/test")).toBeDefined(); }); it("should handle redundant slashes in mounting", () => { const sub = router(); sub.get("//test", (ctx) => ctx.res.send("ok")); const app = server(); app.mount("//api//", sub); // biome-ignore lint/complexity/useLiteralKeys: ... const routes = app["routes"]; expect(routes.find((r) => r.path.includes("test"))).toBeDefined(); }); it("should handle mounting an empty router", () => { const sub = router(); const app = server(); expect(() => app.mount("/api", sub)).not.toThrow(); // biome-ignore lint/complexity/useLiteralKeys: ... expect(app["routes"]).toHaveLength(0); }); }); describe("Server Integration", () => { it("should correctly propagate and execute sub-router middlewares", async () => { const app = server(); const mwCalled = vi.fn(); const mw = middleware((_ctx, next) => { mwCalled(); return next(); }); const sub = router(); sub.get("/test", [mw], (ctx) => ctx.res.send("ok")); // biome-ignore lint/complexity/useLiteralKeys: ... const addRouteSpy = vi.spyOn(app["coreServer"], "addRoute"); app.mount("/api", sub); expect(addRouteSpy).toHaveBeenCalled(); // Find the registered handler for the mounted route const call = addRouteSpy.mock.calls.find( (c) => (c[0] as Route).path === "/api/test", ); expect(call).toBeDefined(); const registeredHandler = (call as [Route])[0].handler; // Mock context to simulate server execution const mockCtx = { req: { method: "GET", url: "/api/test", headers: {}, params: {}, query: {}, body: {}, }, res: { status: vi.fn().mockReturnThis(), send: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(), header: vi.fn().mockReturnThis(), cookie: vi.fn().mockReturnThis(), }, }; try { await registeredHandler(mockCtx); } catch (_e) { // Ignore potential channel/response builder errors in mock environment } // Verification: The middleware on the sub-router MUST have been called expect(mwCalled).toHaveBeenCalled(); }); it("should register routes with correctly fused middlewares in core server", async () => { const mwCalled = vi.fn(); const mw = middleware((_ctx, next) => { mwCalled(); return next(); }); const sub = router(); sub.use(mw); sub.get("/test", (ctx) => ctx.res.send("ok")); const app = server(); // biome-ignore lint/complexity/useLiteralKeys: ... const addRouteSpy = vi.spyOn(app["coreServer"], "addRoute"); app.mount("/api", sub); expect(addRouteSpy).toHaveBeenCalled(); const call = addRouteSpy.mock.calls.find( (c) => (c[0] as Route).path === "/api/test", ); expect(call).toBeDefined(); const registeredHandler = (call?.[0] as Route).handler; const mockCtx = { req: { method: "GET", url: "/api/test", headers: {}, params: {}, query: {}, body: {}, }, res: { status: vi.fn().mockReturnThis(), send: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(), header: vi.fn().mockReturnThis(), cookie: vi.fn().mockReturnThis(), }, }; try { await registeredHandler(mockCtx); } catch (_e) { // Ignore crashes } expect(mwCalled).toHaveBeenCalled(); }); }); }); ================================================ FILE: packages/kitojs/tests/schema-helper.test.ts ================================================ import { describe, expect, it } from "vitest"; import { schema, t } from "../src"; describe("Schema Helper", () => { it("should create schema definition with params", () => { const userSchema = schema({ params: t.object({ id: t.str().uuid(), }), }); expect(userSchema).toHaveProperty("params"); expect(userSchema.params).toBeDefined(); }); it("should create schema definition with query", () => { const listSchema = schema({ query: t.object({ page: t.num().min(1).default(1), limit: t.num().min(1).max(100).default(10), }), }); expect(listSchema).toHaveProperty("query"); expect(listSchema.query).toBeDefined(); }); it("should create schema definition with body", () => { const createUserSchema = schema({ body: t.object({ name: t.str().min(1), email: t.str().email(), age: t.num().min(0).optional(), }), }); expect(createUserSchema).toHaveProperty("body"); expect(createUserSchema.body).toBeDefined(); }); it("should create schema definition with headers", () => { const authSchema = schema({ headers: t.object({ authorization: t.str().min(1), }), }); expect(authSchema).toHaveProperty("headers"); expect(authSchema.headers).toBeDefined(); }); it("should create complete schema definition", () => { const completeSchema = schema({ params: t.object({ id: t.str() }), query: t.object({ include: t.str().optional() }), body: t.object({ data: t.str() }), headers: t.object({ "content-type": t.str() }), }); expect(completeSchema.params).toBeDefined(); expect(completeSchema.query).toBeDefined(); expect(completeSchema.body).toBeDefined(); expect(completeSchema.headers).toBeDefined(); }); it("should pass through schema object unchanged", () => { const original = { params: t.object({ id: t.str() }), query: t.object({ page: t.num() }), }; const result = schema(original); expect(result).toBe(original); }); }); ================================================ FILE: packages/kitojs/tests/server.test.ts ================================================ // biome-ignore assist/source/organizeImports: ... import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { server, middleware, schema, t } from "../src"; describe("Server", () => { let app: ReturnType; beforeEach(() => { app = server(); }); afterEach(() => { if (app) { app.close(); } }); describe("Creation", () => { it("should create server instance", () => { expect(app).toBeDefined(); expect(typeof app.get).toBe("function"); expect(typeof app.post).toBe("function"); expect(typeof app.listen).toBe("function"); }); it("should create server with options", () => { const appWithOptions = server({ port: 4000, host: "127.0.0.1" }); expect(appWithOptions).toBeDefined(); appWithOptions.close(); }); }); describe("Route Registration", () => { it("should register GET route", () => { expect(() => { app.get("/", (ctx) => { ctx.res.send("hello"); }); }).not.toThrow(); }); it("should register POST route", () => { expect(() => { app.post("/users", (ctx) => { ctx.res.json({ success: true }); }); }).not.toThrow(); }); it("should register PUT route", () => { expect(() => { app.put("/users/:id", (ctx) => { ctx.res.send("updated"); }); }).not.toThrow(); }); it("should register DELETE route", () => { expect(() => { app.delete("/users/:id", (ctx) => { ctx.res.sendStatus(204); }); }).not.toThrow(); }); it("should register PATCH route", () => { expect(() => { app.patch("/users/:id", (ctx) => { ctx.res.send("patched"); }); }).not.toThrow(); }); it("should register route with schema", () => { const userSchema = schema({ params: t.object({ id: t.str().uuid() }), }); expect(() => { app.get("/users/:id", [userSchema], (ctx) => { ctx.res.json({ id: ctx.req.params.id }); }); }).not.toThrow(); }); it("should register route with middleware", () => { const auth = middleware((_, next) => { next(); }); expect(() => { app.get("/protected", [auth], (ctx) => { ctx.res.send("secret"); }); }).not.toThrow(); }); it("should register route with multiple middlewares", () => { const auth = middleware((_, next) => next()); const logger = middleware((_, next) => next()); expect(() => { app.get("/admin", [auth, logger], (ctx) => { ctx.res.send("admin"); }); }).not.toThrow(); }); }); describe("Global Middleware", () => { it("should register global middleware", () => { const globalMw = middleware((_, next) => { next(); }); expect(() => { app.use(globalMw); }).not.toThrow(); }); it("should register global middleware as function", () => { expect(() => { app.use((_, next) => { next(); }); }).not.toThrow(); }); }); describe("Extension", () => { it("should extend context", () => { const extendedApp = app.extend<{ db: { query: () => string } }>((ctx) => { ctx.db = { query: () => "result" }; }); expect(extendedApp).toBeDefined(); extendedApp.close(); }); it("should extend context with return value", () => { const extendedApp = app.extend<{ user: { id: string } }>((_) => { return { user: { id: "123" } }; }); expect(extendedApp).toBeDefined(); extendedApp.close(); }); it("should chain multiple extensions", () => { const extended = app // biome-ignore lint/suspicious/noExplicitAny: ... .extend<{ db: any }>((_) => ({ db: {} })) // biome-ignore lint/suspicious/noExplicitAny: ... .extend<{ cache: any }>((_) => ({ cache: {} })); expect(extended).toBeDefined(); extended.close(); }); }); describe("Listen", () => { it("should have listen method", () => { expect(typeof app.listen).toBe("function"); }); it("should have close method", () => { expect(typeof app.close).toBe("function"); }); }); }); ================================================ FILE: packages/kitojs/tests/types.test.ts ================================================ // biome-ignore assist/source/organizeImports: ... import { describe, expectTypeOf, it } from "vitest"; import { server, schema, t, type Context } from "../src"; describe("Type Safety", () => { it("should infer schema types correctly", () => { const userSchema = schema({ params: t.object({ id: t.str().uuid(), }), query: t.object({ limit: t.num().default(10), }), body: t.object({ name: t.str(), email: t.str().email(), age: t.num().optional(), }), }); type UserContext = Context; expectTypeOf().toMatchTypeOf<{ id: string; }>(); expectTypeOf().toMatchTypeOf<{ limit: number; }>(); expectTypeOf().toMatchTypeOf<{ name: string; email: string; age?: number; }>(); }); it("should infer optional fields", () => { const optionalSchema = schema({ query: t.object({ search: t.str().optional(), page: t.num().default(1), }), }); type OptionalContext = Context; expectTypeOf().toMatchTypeOf<{ search?: string; page: number; }>(); }); it("should infer array types", () => { const arraySchema = schema({ body: t.object({ tags: t.array(t.str()), scores: t.array(t.num()), }), }); type ArrayContext = Context; expectTypeOf().toMatchTypeOf<{ tags: string[]; scores: number[]; }>(); }); it("should infer nested object types", () => { const nestedSchema = schema({ body: t.object({ user: t.object({ name: t.str(), profile: t.object({ bio: t.str(), avatar: t.str().url().optional(), }), }), }), }); type NestedContext = Context; expectTypeOf().toMatchTypeOf<{ user: { name: string; profile: { bio: string; avatar?: string; }; }; }>(); }); it("should infer literal types", () => { const literalSchema = schema({ body: t.object({ role: t.union( t.literal("admin"), t.literal("user"), t.literal("guest"), ), status: t.literal("active"), }), }); type LiteralContext = Context; expectTypeOf().toMatchTypeOf<{ role: "admin" | "user" | "guest"; status: "active"; }>(); }); it("should support extended context types", () => { interface Database { // biome-ignore lint/suspicious/noExplicitAny: ... query: (sql: string) => any; } const app = server().extend<{ db: Database }>((ctx) => { ctx.db = { query: () => {} }; }); app.get("/", (ctx) => { expectTypeOf(ctx).toHaveProperty("db"); expectTypeOf(ctx.db).toHaveProperty("query"); }); }); it("should chain extension types", () => { interface DB { query: () => void; } interface Cache { get: () => void; } const app = server() .extend<{ db: DB }>(() => ({ db: { query: () => {} } })) .extend<{ cache: Cache }>(() => ({ cache: { get: () => {} } })); app.get("/", (ctx) => { expectTypeOf(ctx).toHaveProperty("db"); expectTypeOf(ctx).toHaveProperty("cache"); }); }); }); ================================================ FILE: packages/kitojs/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist" }, "include": ["src"], "references": [{ "path": "../core" }, { "path": "../types" }] } ================================================ FILE: packages/kitojs/tsdown.config.ts ================================================ import { defineConfig } from "tsdown"; export default defineConfig({ entry: "src/index.ts", outDir: "dist", format: "esm", minify: true, dts: true, tsconfig: "tsconfig.json", }); ================================================ FILE: packages/kitojs/typedoc.json ================================================ { "$schema": "https://typedoc.org/schema.json", "entryPoints": ["src/index.ts"], "out": "docs", "plugin": [], "theme": "default", "name": "Kito Framework", "includeVersion": true, "readme": "../../readme.md", "categorizeByGroup": true, "categoryOrder": ["Server", "Schemas", "Middleware", "Types", "*"], "exclude": [ "**/node_modules/**", "**/dist/**", "**/tests/**", "**/*.test.ts", "**/server/analyzer.ts", "**/server/request.ts", "**/server/response.ts" ], "excludePrivate": true, "excludeProtected": true, "excludeInternal": true, "cleanOutputDir": true, "navigation": { "includeCategories": true, "includeGroups": true }, "searchInComments": true, "sort": ["source-order", "required-first", "alphabetical"], "treatWarningsAsErrors": false, "validation": { "notExported": true, "invalidLink": true, "notDocumented": false } } ================================================ FILE: packages/kitojs/vitest.config.ts ================================================ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, environment: "node", include: ["tests/**/*.test.ts"], coverage: { provider: "v8", reporter: ["text", "json", "html"], exclude: [ "node_modules/", "dist/", "tests/", "**/*.d.ts", "**/*.config.ts", ], }, }, }); ================================================ FILE: packages/readme.md ================================================ # Kito - `packages` This folder contains all the **modular packages** that make up the Kito framework. It is part of the **monorepo** managed with [`pnpm workspaces`](https://pnpm.io/workspaces). Each package is **independent**, versioned, and published under the `@kitojs/*` namespace (except `packages/kitojs`). --- ## 📂 Project structure ``` 📂 packages/ ├── 📂 core │ ├── 📄 Cargo.toml │ ├── 📄 package.json │ ├── 📄 build.rs │ ├── 📄 .gitignore │ ├── 📂 src │ │ ├── 📄 lib.rs │ │ ├── 📄 server.rs │ │ ├── 📄 http.rs │ │ ├── 📂 http │ │ ├── 📂 server │ │ └── 📂 validation │ └── 📄 tsconfig.json ├── 📂 kitojs │ ├── 📄 package.json │ ├── 📄 tsconfig.json │ ├── 📄 tsdown.config.ts │ ├── 📂 src │ │ ├── 📄 index.ts │ │ ├── 📂 server │ │ ├── 📂 helpers │ │ └── 📂 schemas │ └── 📄 .gitignore ├── 📂 types │ ├── 📄 package.json │ ├── 📄 tsconfig.json │ ├── 📄 tsdown.config.ts │ ├── 📂 src │ │ ├── 📄 index.d.ts │ │ ├── 📄 context.d.ts │ │ ├── 📄 routes.d.ts │ │ ├── 📄 handlers.d.ts │ │ ├── 📂 schema │ │ └── 📂 http │ └── 📄 .gitignore └── 📄 readme.md ``` --- ## 📦 Packages ### `@kitojs/kito-core` - The **Rust core** of the framework, exposing high-performance HTTP server functionality via [N-API](https://github.com/napi-rs/napi-rs). - Responsibilities: - Handling HTTP requests/responses efficiently. - Route and middleware execution. - Validation of request schemas and automatic error handling. - Integration with JS/TS through N-API bindings. - This package is **fully written in Rust**, compiled to a native module, and serves as the runtime for all server logic. ### `kitojs` (TypeScript library) - Main **TypeScript wrapper** for Kito, exposing the framework API to developers. - Responsibilities: - Create and configure servers (`app.get`, `app.post`, `app.use`, etc.). - Define route schemas, middleware, and static responses. - Utilities for schema building, validation, and server context. - Analyze route handlers for static/dynamic optimization. - This package **depends on `kito-core`** for the runtime, but provides a developer-friendly API. ### `@kitojs/types` - Standalone package containing **TypeScript type definitions** for Kito. - Features: - Type definitions for request, response, and server context. - Route, schema, and handler typings. - Shared types between core, CLI, and the TS library. - Designed to enable **full type safety** in TypeScript projects using Kito. --- ## 🛠️ Development workflow Inside the monorepo, you can work on packages in isolation or all together: ```bash # Build all packages pnpm build # Build only a package pnpm --filter @kitojs/pkg build ```` Each package is published independently but linked locally via the workspace. --- ================================================ FILE: packages/types/.gitignore ================================================ dist/ ================================================ FILE: packages/types/package.json ================================================ { "name": "@kitojs/types", "version": "1.0.0-alpha.8", "description": "TypeScript types for the Kito framework.", "scripts": { "build": "tsdown" }, "dependencies": {}, "devDependencies": { "tsdown": "^0.15.0" }, "repository": { "type": "git", "url": "https://github.com/kitojs/kito.git" }, "homepage": "https://kito.pages.dev", "bugs": { "url": "https://github.com/kitojs/kito/issues" }, "license": "MIT", "author": { "name": "Nehuén", "url": "https://github.com/nehu3n" }, "type": "module", "types": "dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.d.ts" } }, "files": [ "dist", "readme.md", "package.json" ] } ================================================ FILE: packages/types/src/context.d.ts ================================================ // biome-ignore assist/source/organizeImports: ... import type { KitoRequest } from "./http/request"; import type { KitoResponse } from "./http/response"; import type { SchemaDefinition, InferSchemaRequest } from "./schema/base"; // biome-ignore lint/complexity/noBannedTypes: ... export interface KitoContext { req: KitoRequest & InferSchemaRequest; res: KitoResponse; } // biome-ignore lint/complexity/noBannedTypes: ... export type Context = KitoContext; ================================================ FILE: packages/types/src/handlers.d.ts ================================================ import type { KitoContext } from "./context"; export type NextFunction = () => void | Promise; export type MiddlewareHandler = ( ctx: KitoContext & TExtensions, next: NextFunction, ) => void | Promise; export type RouteHandler = ( ctx: KitoContext & TExtensions, ) => void | Promise | unknown | Promise; ================================================ FILE: packages/types/src/http/request.d.ts ================================================ export type CommonHeaderNames = | "accept" | "accept-encoding" | "accept-language" | "authorization" | "cache-control" | "content-type" | "content-length" | "cookie" | "host" | "origin" | "referer" | "user-agent" | "x-forwarded-for" | "x-forwarded-host" | "x-forwarded-proto" | "x-requested-with"; export interface RequestHeaders extends Record { accept?: string; "accept-encoding"?: string; "accept-language"?: string; authorization?: string; "cache-control"?: string; "content-type"?: string; "content-length"?: string; cookie?: string; host?: string; origin?: string; referer?: string; "user-agent"?: string; "x-forwarded-for"?: string; "x-forwarded-host"?: string; "x-forwarded-proto"?: string; "x-requested-with"?: string; } export interface ParsedUrl { pathname: string; search: string | null; query: Record; } export interface KitoRequest { get method(): string; get url(): string; get headers(): RequestHeaders; get body(): unknown; get params(): Record; get query(): Record; get cookies(): Record; get pathname(): string; get search(): string | null; get protocol(): string; get hostname(): string; get ip(): string; get ips(): string[]; get secure(): boolean; get xhr(): boolean; get originalUrl(): string; header(name: CommonHeaderNames): string | undefined; header(name: string): string | undefined; queryParam(name: string): string | string[] | undefined; param(name: string): string | undefined; cookie(name: string): string | undefined; json(): T; text(): string; get raw(): { body: Buffer; headers: RequestHeaders; url: string; method: string; }; } ================================================ FILE: packages/types/src/http/response.d.ts ================================================ import type { InferType, SchemaType } from "../schema/base"; export type CommonResponseHeaderNames = | "content-type" | "content-length" | "cache-control" | "etag" | "expires" | "last-modified" | "location" | "set-cookie" | "access-control-allow-origin" | "access-control-allow-methods" | "access-control-allow-headers" | "access-control-allow-credentials" | "vary" | "x-powered-by" | "x-frame-options" | "x-content-type-options" | "strict-transport-security"; export interface StreamWriter { write(data: string | Buffer): void; end(): void; end(data: string | Buffer): void; } export interface SSEWriter { send(data: unknown, event?: string, id?: string, retry?: number): void; comment(text: string): void; close(): void; } export interface KitoResponse { status(code: number): KitoResponse; sendStatus(code: number): void; header( name: CommonResponseHeaderNames, value: string, ): KitoResponse; header(name: string, value: string): KitoResponse; headers( headers: Record, ): KitoResponse; headers(headers: Record): KitoResponse; append( field: CommonResponseHeaderNames, value: string, ): KitoResponse; append(field: string, value: string): KitoResponse; set( field: CommonResponseHeaderNames, value: string, ): KitoResponse; set(field: string, value: string): KitoResponse; get(field: CommonResponseHeaderNames): string | undefined; get(field: string): string | undefined; type(contentType: string): KitoResponse; contentType(contentType: string): KitoResponse; cookie( name: string, value: string, options?: CookieOptions, ): KitoResponse; clearCookie( name: string, options?: CookieOptions, ): KitoResponse; end(): void; send( data: TResponseSchema extends SchemaType ? InferType : unknown, ): void; json( data: TResponseSchema extends SchemaType ? InferType : unknown, ): void; text(data: string): void; html(data: string): void; redirect(url: string, code?: number): void; location(url: string): KitoResponse; attachment(filename?: string): KitoResponse; download(path: string, filename?: string, options?: SendFileOptions): void; sendFile(path: string, options?: SendFileOptions): void; vary(field: string): KitoResponse; links(links: Record): KitoResponse; format(obj: Record void>): KitoResponse; stream(): StreamWriter; sse(): SSEWriter; } export interface CookieOptions { domain?: string; expires?: Date; httpOnly?: boolean; maxAge?: number; path?: string; secure?: boolean; signed?: boolean; sameSite?: boolean | "lax" | "strict" | "none"; } export interface SendFileOptions { maxAge?: number; root?: string; lastModified?: boolean; headers?: Record; dotfiles?: "allow" | "deny" | "ignore"; acceptRanges?: boolean; cacheControl?: boolean; immutable?: boolean; etag?: boolean; } ================================================ FILE: packages/types/src/index.d.ts ================================================ // biome-ignore assist/source/organizeImports: ... export * from "./http/request"; export * from "./http/response"; export * from "./context"; export * from "./handlers"; export * from "./routes"; export * from "./router"; export * from "./server"; export * from "./schema/base"; export * from "./schema/string"; export * from "./schema/number"; export * from "./schema/boolean"; export * from "./schema/array"; export * from "./schema/object"; export * from "./schema/literal"; export * from "./schema/union"; export * from "./schema/jsonSchema"; ================================================ FILE: packages/types/src/router.d.ts ================================================ import type { MiddlewareHandler, RouteHandler } from "./handlers"; import type { MiddlewareDefinition, RouteChain } from "./routes"; import type { SchemaDefinition } from "./schema/base"; // biome-ignore lint/complexity/noBannedTypes: ... export interface KitoRouterInstance { use( middleware: MiddlewareDefinition | MiddlewareHandler, ): KitoRouterInstance; mount( path: string, router: KitoRouterInstance, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... get( path: string, handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... get( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... get( path: string, handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... get( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... post( path: string, handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... post( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... post( path: string, handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... post( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... put( path: string, handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... put( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... put( path: string, handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... put( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... delete( path: string, handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... delete( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... delete( path: string, handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... delete( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... patch( path: string, handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... patch( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... patch( path: string, handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... patch( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... head( path: string, handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... head( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... head( path: string, handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... head( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... options( path: string, handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... options( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... options( path: string, handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; // biome-ignore lint/complexity/noBannedTypes: ... options( path: string, middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, schema: TSchema, ): KitoRouterInstance; route(path: string): RouteChain; route( path: string, middlewares: MiddlewareDefinition[] | MiddlewareDefinition, ): RouteChain; } ================================================ FILE: packages/types/src/routes.d.ts ================================================ import type { MiddlewareHandler, RouteHandler } from "./handlers"; import type { SchemaDefinition } from "./schema/base"; import type { KitoRouterInstance } from "./router"; export type HttpMethod = | "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS" | "TRACE"; export interface RouteDefinition { method: HttpMethod; path: string; middlewares: (MiddlewareDefinition | SchemaDefinition)[]; handler: RouteHandler; } export interface MiddlewareDefinition { type: "function" | "schema"; handler?: MiddlewareHandler; schema?: SchemaDefinition; global: boolean; } // biome-ignore lint/complexity/noBannedTypes: ... export type RouteChain = { // biome-ignore lint/complexity/noBannedTypes: ... get( handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... get( middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... get( handler: RouteHandler, schema: TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... get( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... post( handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... post( middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... post( handler: RouteHandler, schema: TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... post( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... put( handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... put( middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... put( handler: RouteHandler, schema: TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... put( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... delete( handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... delete( middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... delete( handler: RouteHandler, schema: TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... delete( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... patch( handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... patch( middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... patch( handler: RouteHandler, schema: TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... patch( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... head( handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... head( middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... head( handler: RouteHandler, schema: TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... head( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... options( handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... options( middlewares: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema), handler: RouteHandler, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... options( handler: RouteHandler, schema: TSchema, ): RouteChain; // biome-ignore lint/complexity/noBannedTypes: ... options( middlewaresOrHandler: | (MiddlewareDefinition | TSchema)[] | (MiddlewareDefinition | TSchema) | RouteHandler, handlerOrSchema?: RouteHandler | TSchema, ): RouteChain; end(): KitoRouterInstance; }; ================================================ FILE: packages/types/src/schema/array.d.ts ================================================ import type { InferType, SchemaType } from "./base"; export interface ArraySchema extends SchemaType { _type: InferType[]; min(length: number): ArraySchema; max(length: number): ArraySchema; length(length: number): ArraySchema; optional(): OptionalArraySchema; default(value: InferType[]): DefaultArraySchema; } export interface OptionalArraySchema extends Omit, "optional" | "default"> { _optional: true; default(value: InferType[]): DefaultArraySchema; } export interface DefaultArraySchema extends Omit, "optional" | "default"> { _optional: true; _default: InferType[]; } ================================================ FILE: packages/types/src/schema/base.d.ts ================================================ import type { RequestHeaders } from "../http/request"; export interface SchemaDefinition { params?: SchemaType; query?: SchemaType; body?: SchemaType; headers?: SchemaType; response?: ResponseSchemaDefinition; } export interface ResponseSchemaDefinition { [statusCode: number]: SchemaType; } export type InferSchemaRequest = T extends { __jsonSchemaInfer: infer J; } ? J : { params: T["params"] extends SchemaType ? InferType : Record; query: T["query"] extends SchemaType ? InferType : Record; body: T["body"] extends SchemaType ? InferType : unknown; headers: T["headers"] extends SchemaType ? InferType : RequestHeaders; }; export interface SchemaType { _type: unknown; _optional: boolean; _default?: unknown; // biome-ignore lint/suspicious/noExplicitAny: ... _serialize?(): any; } export type InferType = T extends { _default: infer D } ? D : T extends { _optional: true } ? T["_type"] | undefined : T["_type"]; ================================================ FILE: packages/types/src/schema/boolean.d.ts ================================================ import type { SchemaType } from "./base"; export interface BooleanSchema extends SchemaType { _type: boolean; optional(): OptionalBooleanSchema; default(value: boolean): DefaultBooleanSchema; } export interface OptionalBooleanSchema extends Omit { _optional: true; default(value: boolean): DefaultBooleanSchema; } export interface DefaultBooleanSchema extends Omit { _optional: true; _default: boolean; } ================================================ FILE: packages/types/src/schema/jsonSchema.d.ts ================================================ type StringFormat = "email" | "uuid" | "uri" | "date-time"; interface BaseJSONSchema { description?: string; default?: unknown; } export interface JSONSchemaString extends BaseJSONSchema { type: "string"; minLength?: number; maxLength?: number; pattern?: string; format?: StringFormat; enum?: readonly string[]; const?: string; } export interface JSONSchemaNumber extends BaseJSONSchema { type: "number" | "integer"; minimum?: number; maximum?: number; enum?: readonly number[]; const?: number; } export interface JSONSchemaBoolean extends BaseJSONSchema { type: "boolean"; const?: boolean; } export interface JSONSchemaArray extends BaseJSONSchema { type: "array"; items: JSONSchema; minItems?: number; maxItems?: number; } export interface JSONSchemaObject extends BaseJSONSchema { type: "object"; properties: Record; required?: readonly string[]; } export type JSONSchema = | JSONSchemaString | JSONSchemaNumber | JSONSchemaBoolean | JSONSchemaArray | JSONSchemaObject; type InferString = T extends { const: infer C } ? C : T extends { enum: infer E } ? E extends readonly (infer U)[] ? U : never : string; type InferNumber = T extends { const: infer C } ? C : T extends { enum: infer E } ? E extends readonly (infer U)[] ? U : never : number; type InferBoolean = T extends { const: infer C } ? C : boolean; type InferArray = T extends { items: infer I } ? I extends JSONSchema ? InferJSONSchemaType[] : never : never; type InferObject = T extends { properties: infer P; required?: infer R; } ? P extends Record ? R extends readonly string[] ? { [K in keyof P as K extends R[number] ? K : never]: P[K] extends JSONSchema ? InferJSONSchemaType : never; } & { [K in keyof P as K extends R[number] ? never : K]?: P[K] extends JSONSchema ? InferJSONSchemaType : never; } : { [K in keyof P]?: P[K] extends JSONSchema ? InferJSONSchemaType : never; } : never : never; export type InferJSONSchemaType = T extends { default: infer D } ? D : T extends JSONSchemaString ? InferString : T extends JSONSchemaNumber ? InferNumber : T extends JSONSchemaBoolean ? InferBoolean : T extends JSONSchemaArray ? InferArray : T extends JSONSchemaObject ? InferObject : never; export interface JSONSchemaDefinition { params?: JSONSchemaObject; query?: JSONSchemaObject; body?: JSONSchema; headers?: JSONSchemaObject; response?: { [statusCode: number]: JSONSchema; }; } export type InferJSONSchemaRequest = { params: T["params"] extends JSONSchemaObject ? InferJSONSchemaType : Record; query: T["query"] extends JSONSchemaObject ? InferJSONSchemaType : Record; body: T["body"] extends JSONSchema ? InferJSONSchemaType : unknown; headers: T["headers"] extends JSONSchemaObject ? InferJSONSchemaType : Record; }; ================================================ FILE: packages/types/src/schema/literal.d.ts ================================================ import type { SchemaType } from "./base"; export interface LiteralSchema extends SchemaType { _type: T; optional(): OptionalLiteralSchema; default(value: T): DefaultLiteralSchema; } export interface OptionalLiteralSchema extends Omit, "optional" | "default"> { _optional: true; default(value: T): DefaultLiteralSchema; } export interface DefaultLiteralSchema extends Omit, "optional" | "default"> { _optional: true; _default: T; } ================================================ FILE: packages/types/src/schema/number.d.ts ================================================ import type { SchemaType } from "./base"; export interface NumberSchema extends SchemaType { _type: number; min(value: number): NumberSchema; max(value: number): NumberSchema; int(): NumberSchema; positive(): NumberSchema; negative(): NumberSchema; optional(): OptionalNumberSchema; default(value: number): DefaultNumberSchema; } export interface OptionalNumberSchema extends Omit { _optional: true; default(value: number): DefaultNumberSchema; } export interface DefaultNumberSchema extends Omit { _optional: true; _default: number; } ================================================ FILE: packages/types/src/schema/object.d.ts ================================================ import type { InferType, SchemaType } from "./base"; export interface ObjectSchema> extends SchemaType { _type: { [K in keyof T]: InferType }; shape: T; optional(): OptionalObjectSchema; default(value: { [K in keyof T]: InferType }): DefaultObjectSchema; } export interface OptionalObjectSchema> extends Omit, "optional" | "default"> { _optional: true; default(value: { [K in keyof T]: InferType }): DefaultObjectSchema; } export interface DefaultObjectSchema> extends Omit, "optional" | "default"> { _optional: true; _default: { [K in keyof T]: InferType }; } ================================================ FILE: packages/types/src/schema/string.d.ts ================================================ import type { SchemaType } from "./base"; export interface StringSchema extends SchemaType { _type: string; min(length: number): StringSchema; max(length: number): StringSchema; length(length: number): StringSchema; email(): StringSchema; url(): StringSchema; uuid(): StringSchema; regex(pattern: RegExp): StringSchema; optional(): OptionalStringSchema; default(value: string): DefaultStringSchema; } export interface OptionalStringSchema extends Omit { _optional: true; default(value: string): DefaultStringSchema; } export interface DefaultStringSchema extends Omit { _optional: true; _default: string; } ================================================ FILE: packages/types/src/schema/union.d.ts ================================================ import type { InferType, SchemaType } from "./base"; export interface UnionSchema extends SchemaType { _type: InferType; optional(): OptionalUnionSchema; default(value: InferType): DefaultUnionSchema; } export interface OptionalUnionSchema extends Omit, "optional" | "default"> { _optional: true; default(value: InferType): DefaultUnionSchema; } export interface DefaultUnionSchema extends Omit, "optional" | "default"> { _optional: true; _default: InferType; } ================================================ FILE: packages/types/src/server.d.ts ================================================ import type { MiddlewareHandler, RouteHandler } from "./handlers"; import type { MiddlewareDefinition, RouteChain } from "./routes"; import type { SchemaDefinition } from "./schema/base"; import type { KitoRouterInstance } from "./router"; export interface ServerOptions { port?: number; host?: string; unixSocket?: string; trustProxy?: boolean; maxRequestSize?: number; timeout?: number; reusePort?: boolean; } // biome-ignore lint/complexity/noBannedTypes: ... export interface KitoServerInstance extends KitoRouterInstance { listen(callback?: () => void): Promise; listen(port?: number, callback?: () => void): Promise; listen( port?: number, host?: string, callback?: () => void, ): Promise; listen(options: ServerOptions, callback?: () => void): Promise; close(): void; } ================================================ FILE: packages/types/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", "emitDeclarationOnly": true }, "include": ["src"] } ================================================ FILE: packages/types/tsdown.config.ts ================================================ import { defineConfig } from "tsdown"; export default defineConfig({ entry: "src/index.d.ts", outDir: "dist", format: "esm", minify: true, dts: true, tsconfig: "tsconfig.json", }); ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - packages/* - examples - bench onlyBuiltDependencies: - skia-canvas ================================================ FILE: readme.md ================================================


High-performance, fully type-safe, and modern web framework for TypeScript. Powered by Rust for extreme speed and low memory usage.

--- - **Extreme performance** – Rust core optimized for extreme speed & efficiency. **See the [benchmarks](https://github.com/kitojs/kito/tree/main/bench).** - **Type-safe** – full TypeScript support with end-to-end safety and exceptional DX. - **Schema validation** – built-in validation with zero bloat. - **Middleware system** – composable and flexible like you expect. - **Cross-platform** – runs on Node.js, Bun, and Deno. --- ## 🚀 Quick Start You can add **Kito** to an existing project: ```bash pnpm add kitojs # Or: npm/yarn/bun add kitojs # Or: deno add npm:kitojs ``` Or create a new project instantly with the [official starter](https://github.com/kitojs/create-kitojs): ```bash pnpm create kitojs # Or: npm/yarn/bun create kitojs # Or: deno init --npm kitojs ``` ### Minimal Example ```ts import { server } from "kitojs"; const app = server(); app.get("/", ({ res }) => { res.send("hello world!"); }); app.listen(3000); ```
Fluent style Kito also supports fluent style. You can chain all methods. **See the examples [here](https://github.com/kitojs/kito/tree/main/examples/fluent).** ```ts import { server } from "kitojs"; server() .get("/", ({ res }) => res.send("hello world!")) .listen(3000); ```
--- ## 📚 Documentation Full docs available at the [**official website**](https://kito.pages.dev). You can also explore ready-to-run [examples](https://github.com/kitojs/kito/tree/main/examples). --- ## 🤝 Contributing We welcome contributions! Check the [**contributing guide**](https://github.com/kitojs/kito/blob/main/contributing.md) to learn how to set up your environment and submit pull requests. --- ## 📄 License Licensed under the [MIT License](https://github.com/kitojs/kito/blob/main/license). --- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/kitojs/kito) ================================================ FILE: rust-toolchain.toml ================================================ [toolchain] channel = "stable" components = ["clippy", "rustfmt", "rust-analyzer"] ================================================ FILE: rustfmt.toml ================================================ edition = "2024" max_width = 100 hard_tabs = false tab_spaces = 4 newline_style = "Unix" use_small_heuristics = "Max" reorder_imports = true reorder_modules = true merge_derives = true ================================================ FILE: tsconfig.base.json ================================================ { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Node", "declaration": true, "declarationMap": true, "outDir": "dist", "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "composite": true, "resolveJsonModule": true, "baseUrl": ".", "paths": { "@kitojs/kito-core": ["packages/core/src"], "@kitojs/types": ["packages/types/src"], "kitojs": ["packages/kitojs/src"] } } }