65 Commits

Author SHA1 Message Date
cdeeebea90 chore(changelog): update changelog for v0.2.1
Some checks failed
Sync Release to GitHub / build-and-publish (release) Failing after 9s
2025-11-12 13:25:33 +00:00
bf4aa3f393 chore(version): bump version to 0.2.1
Some checks failed
Auto Changelog & (Release) / release (push) Successful in 9s
CI / build (push) Failing after 23s
2025-11-12 14:25:21 +01:00
8850e5e478 chore(changelog): simplify release workflow using composite action
Some checks failed
Auto Changelog & (Release) / release (push) Failing after 6s
CI / build (push) Failing after 21s
2025-11-12 14:25:01 +01:00
6d7127a52f feat: export errors, interfaces, types, and utils from main module 2025-11-12 14:21:59 +01:00
3a4a056760 chore(changelog): update unreleased changelog 2025-06-14 15:42:45 +00:00
f177746760 chore(workflows): update release workflow for consistency
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / changelog-only (push) Successful in 13s
CI / build (push) Successful in 23s
Auto Changelog & Release / release (push) Has been skipped
- Adjust branch pattern to use consistent quoting style
- Add `--locked` flag to `cargo install` for reproducible builds
- Ensure consistent formatting and remove unnecessary blank lines
- Set author and committer dates for better tag creation
2025-06-14 17:42:24 +02:00
e98244325d chore(changelog): update unreleased changelog 2025-05-27 13:10:20 +00:00
5686940fe2 fix(workflows): remove redundant tag fallback in sync job
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 7s
CI / build (push) Successful in 16s
2025-05-27 15:10:07 +02:00
4a06bc67ea chore(changelog): update changelog for v0.2.0
Some checks failed
Sync Release to GitHub / build-and-publish (release) Failing after 7s
2025-05-27 13:08:31 +00:00
68cec85380 chore(version): bump version to 0.2.0
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / changelog-only (push) Has been skipped
CI / build (push) Successful in 15s
Auto Changelog & Release / release (push) Successful in 7s
2025-05-27 15:08:14 +02:00
86895f41f1 chore(changelog): update unreleased changelog 2025-05-27 13:06:24 +00:00
de6d3ee389 feat(workflows): add GitHub release sync workflow
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 2s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
CI / build (push) Successful in 17s
- Add a workflow to synchronize Gitea releases with GitHub
- Remove deprecated upload-assets workflow example
2025-05-27 15:06:11 +02:00
54cfa1888e feat(ci): enhance CI workflow with granular steps and error handling
- Add separate steps for formatting, linting, testing, and benchmarking
- Introduce failure detection to halt workflow on step failure
- Improve maintainability and visibility of CI process outcomes
2025-05-27 15:06:11 +02:00
c207dc7392 chore(config): add CI task for local checks
- Introduces a new "ci" task to streamline local CI workflows
- Combines formatting, linting, testing, and benchmarking for convenience
2025-05-27 15:06:11 +02:00
b14e9acc5f test(routebuilder): add validation tests for handler and middleware
- Add tests to validate middleware and handler signatures
- Ensure TypeError is thrown for invalid signatures
2025-05-27 15:06:11 +02:00
731bba22d8 test(httpkernel): enforce compile-time validation for signatures
- Replace runtime 500 errors with compile-time validation for invalid
  middleware and handler signatures using `assertThrows`
- Improve test clarity by explicitly checking for expected exceptions
  and error messages
2025-05-27 15:06:11 +02:00
aea3fb45e7 refactor(kernel): simplify middleware and handler execution
- Replace middleware pipeline logic with route-specific execution
- Remove redundant type checks and streamline error handling
- Improve maintainability by delegating response decoration to routes
2025-05-27 15:06:11 +02:00
35d83c073e feat(route-builder): add middleware chain compilation
- Introduces a `compile` method to compose middleware and handler
  into a single executable function.
- Enhances runtime safety by ensuring `next()` is called only once
  per middleware and validating middleware/handler signatures.
- Improves code structure and maintainability for route execution.
2025-05-27 15:06:11 +02:00
67ebb4307a feat(interfaces): add runRoute method to IInternalRoute
- Introduces a statically compiled `runRoute` method to encapsulate
  the middleware chain and final handler execution for routes.
- Ensures consistent pipeline execution with safeguards like single
  `next()` invocation and a guaranteed `Response` return type.
2025-05-27 15:06:11 +02:00
3da34e2684 test(bench): add parallel benchmarks for HTTP kernel
- Introduce parallel benchmarks for simple and complex HTTP requests
- Improve performance testing with higher concurrency (10,000 requests)
- Comment out single-request benchmarks for future reference
2025-05-27 15:06:11 +02:00
26198ccdcd chore(changelog): update unreleased changelog 2025-05-27 13:04:23 +00:00
1b447f5190 chore(gitignore): add .local directory to ignored files
Some checks failed
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
CI / build (push) Failing after 7s
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Updates .gitignore to include the .local directory
- Prevents accidental commits of local configuration files
2025-05-27 15:04:09 +02:00
38c00b035b chore(ci): update deno tasks in CI workflow
- Replace individual deno commands with `deno task` equivalents
- Add benchmark task to the CI workflow for performance testing
2025-05-27 15:04:09 +02:00
6e6e61693f chore(tasks): add benchmark, format, and lint commands
- Introduces a benchmark task for running performance tests
- Adds format and lint tasks to enforce code style and quality
2025-05-27 15:04:09 +02:00
d8283ea83a chore(changelog): update unreleased changelog 2025-05-27 11:25:56 +00:00
ec1697df94 ci(workflows): consolidate and update CI configuration
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
CI / build (push) Successful in 7s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Replace `test.yml` with `ci.yml` for streamlined workflows
- Add support for `deno fmt` and `deno lint` in CI checks
- Ensure tests run on `main` branch pushes and PR events
2025-05-27 13:25:44 +02:00
e416af9754 chore(changelog): update unreleased changelog 2025-05-27 11:18:44 +00:00
b83aa330b3 refactor(imports): use explicit type-only imports across codebase
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Replace standard imports with type-only imports to improve clarity
  and align with TypeScript best practices.
- Ensure consistency across modules by modifying all relevant files.
2025-05-27 13:18:31 +02:00
c28eb7f28d chore(config): add exports field to module metadata
- Introduces an `exports` field to map module entry points
2025-05-27 13:18:18 +02:00
d7460c4b1d chore(changelog): update unreleased changelog 2025-05-10 15:51:02 +00:00
6a0f1c774b docs(release): update guidelines for handling changelog
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 7s
- Add instructions to avoid merge conflicts in `CHANGELOG.md`
- Provide steps for using `.gitattributes` to automate conflict resolution
- Clarify recommended workflows for feature branches and merging
2025-05-10 17:50:44 +02:00
7327ed5c1b chore(changelog): update unreleased changelog 2025-05-10 15:44:02 +00:00
b44bb2ddaf feat(workflows): conditionally generate changelog
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 11s
- Add logic to generate changelog only if the file exists or on the main branch
- Prevent unnecessary changelog generation in other contexts
2025-05-10 17:43:39 +02:00
dd60c2cdc7 chore(changelog): update unreleased changelog 2025-05-10 15:26:06 +00:00
6399113e12 chore(git): ignore merge conflicts for CHANGELOG.md
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
2025-05-10 17:25:51 +02:00
813c734ae6 chore(changelog): update unreleased changelog 2025-05-10 15:00:52 +00:00
3707242d27 fix(workflows): ensure version detection output is always set
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
- Move VERSION file change detection behind a branch guard (main only)
- Use a fallback in the output step to ensure 'version_changed' is always defined
- Prevent job skipping and output access errors in feature branches
2025-05-10 17:00:27 +02:00
adc1a4276e chore(changelog): update unreleased changelog 2025-05-10 14:36:02 +00:00
71ea4247b3 chore(workflows): refine branch handling in release process
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
- Adjusts workflow to support pushes from all branches
- Ensures main branch-specific conditions for version detection
- Modifies changelog and release steps for non-main branch handling
2025-05-10 16:35:39 +02:00
b624415320 chore(changelog): update unreleased changelog 2025-05-08 18:19:58 +00:00
a88b4d112f chore(workflows): update changelog file extension to .md and revert b9d25f23fc
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 5s
2025-05-08 20:19:43 +02:00
4f2b65049f chore: rename changelog and readme files to use .md extension 2025-05-08 20:19:43 +02:00
c9de4669c7 chore(changelog): update unreleased changelog 2025-05-08 18:16:14 +00:00
a1ce30627c docs: add README for HttpKernel project
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / changelog-only (push) Successful in 7s
Auto Changelog & Release / release (push) Has been skipped
- Introduce HttpKernel with an overview of its purpose and features
- Provide a quick start guide and API reference for developers
- Include testing instructions, configuration options, and roadmap
- Add license information for clarity and compliance
2025-05-08 20:15:58 +02:00
5118a19aea docs: add MIT license file 2025-05-08 20:15:48 +02:00
0a09c8c324 chore(changelog): update unreleased changelog 2025-05-08 18:09:16 +00:00
b9d25f23fc refactor(workflows): rename changelog file for consistency
All checks were successful
Test http-kernel / Run Tests (pull_request) Successful in 10s
Auto Changelog & Release / detect-version-change (push) Successful in 7s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 12s
- Rename `CHANGELOG.md` to `CHANGELOG` in workflows
- Update related commands and references for consistency
2025-05-08 19:49:24 +02:00
92f09e6e60 Merge pull request 'chore(workflows): update test workflow for http-kernel project' (#1) from fix/test-workflow into main
Some checks failed
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Failing after 10s
Reviewed-on: #1
2025-05-08 19:46:00 +02:00
03115464e0 chore(workflows): update test workflow for http-kernel project
All checks were successful
Test http-kernel / Run Tests (pull_request) Successful in 10s
- Rename workflow and job to align with the http-kernel project
- Improve clarity and maintain consistency in workflow naming
2025-05-08 19:38:49 +02:00
11a2273240 chore(changelog): update unreleased changelog 2025-05-08 17:35:37 +00:00
1233a0b720 chore(workflows): update Deno setup action to v2
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 6s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 10s
- Updates the Deno setup action to the latest major version (v2)
- Ensures compatibility and benefits from improvements in the new version
2025-05-08 19:35:11 +02:00
195619ca99 chore(changelog): update unreleased changelog 2025-05-08 17:33:23 +00:00
9d5db4f414 feat(workflows): add CI for Deno project tests
Some checks are pending
Auto Changelog & Release / release (push) Blocked by required conditions
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / changelog-only (push) Successful in 18s
- Introduce a GitHub Actions workflow to run tests for the Deno project
- Configure workflow to trigger on pull requests across all branches
- Include steps for repository checkout, Deno setup, and test execution
2025-05-08 19:32:51 +02:00
16c0053964 chore(workflows): remove unused workflow_dispatch trigger
- Eliminates the workflow_dispatch trigger from the release workflow
  as it is no longer required, simplifying the configuration.
2025-05-08 19:32:42 +02:00
04029f87a3 chore(tasks): remove commented-out start and watch scripts
- Cleans up unused task definitions for better maintainability
- Reduces clutter in the task configuration
2025-05-08 19:32:34 +02:00
594eccbc8b chore(changelog): update changelog for v0.1.0 2025-05-08 17:07:19 +00:00
3afe6bcbc3 chore(version): add initial version file
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 10s
Auto Changelog & Release / changelog-only (push) Has been skipped
Auto Changelog & Release / release (push) Successful in 2m21s
- Introduces a version file to track the project's version
- Sets the initial version to 0.1.0
2025-05-08 19:03:42 +02:00
bbf78cff17 feat(workflows): add automated changelog and release workflow
- Introduces a GitHub Actions workflow for automated changelog generation
  and release creation triggered by version changes or manual dispatch.
- Includes steps for detecting version file changes, generating changelogs
  using git-cliff, and publishing releases to Gitea.
2025-05-08 19:03:30 +02:00
8235680904 refactor(types): unify handler and middleware definitions
- Consolidates `Handler` and `Middleware` types under `Types` module
- Replaces `IHandler` and `IMiddleware` interfaces with typed functions
- Simplifies imports and improves code organization
- Enhances debugging with named handlers and middlewares
2025-05-08 19:03:18 +02:00
56633cd95b feat(vscode): customize activity bar and peacock colors
- Add custom colors for the activity bar to enhance UI aesthetics
- Set Peacock extension color for consistent workspace theming
2025-05-08 19:02:24 +02:00
5c03cdfb03 docs(gitea): add release automation guide and scripts
- Document automated release process with versioning and changelog generation
- Add scripts for retrieving release IDs and uploading assets to releases
- Provide best practices and debugging tips for maintaining consistency
2025-05-08 19:02:14 +02:00
661f83d1fd chore(config): add default git-cliff configuration
- Introduces a default configuration file for git-cliff
- Enables changelog generation with templates and commit parsing
- Configures commit grouping, tag patterns, and remote repository details
2025-05-08 19:01:55 +02:00
7b6eb2b574 feat(workflows): add upload assets template for releases
- Introduces a workflow to automate uploading release assets
- Triggers on published releases and uses custom scripts
- Enhances release management by linking artifacts to releases
2025-05-08 19:01:37 +02:00
f0838567b4 chore(gitignore): add .gitea/COMMIT_GPT.md to ignored files
- Adds .gitea/COMMIT_GPT.md to the .gitignore file to exclude it from version control.
2025-05-08 19:01:27 +02:00
b009b5763d feat(config): add project metadata and test watch task
- Introduce name and description fields for project metadata
- Add a test:watch task for running tests in watch mode
- Improve configuration clarity by adjusting formatting
2025-05-08 19:01:03 +02:00
43 changed files with 1115 additions and 196 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
CHANGELOG.md merge=ours

198
.gitea/HOWTO_RELEASE.md Normal file
View File

@@ -0,0 +1,198 @@
# 📦 HOWTO: Release erstellen mit Auto-Changelog-Workflow
Dieses Repository nutzt einen automatisierten CI/CD-Workflow zur **Versionsverwaltung, Changelog-Generierung und Release-Erstellung**.
Der gesamte Prozess ist deklarativ und läuft automatisch – ausgelöst durch Änderungen an einer Datei: `VERSION`.
---
## 🧭 Was passiert automatisch?
Sobald Änderungen in `main` landen, prüft der Workflow:
- 🔍 **Hat sich die Datei `VERSION` geändert?**
-**Nein** → es wird nur das `CHANGELOG.md` aktualisiert (unreleased Abschnitt)
-**Ja** → es wird:
- ein vollständiger Changelog für diese Version erzeugt
- ein Git-Tag `vX.Y.Z` erstellt
- ein Release in Gitea veröffentlicht (inkl. Beschreibung aus dem Changelog)
---
## ✅ Wie erzeuge ich ein Release?
### 1. Erhöhe die Version in der Datei `VERSION`
Beispiel:
```txt
1.2.3
```
> Diese Datei muss **als eigene Commit-Änderung** erfolgen – idealerweise als letzter Commit in einem PR.
> Die Commit-Nachricht sollte mit `chore(version)` beginnen, damit dieser nicht im Changelog auftaucht.
---
### 2. Mergen in `main`
Sobald `main` den Commit mit neuer `VERSION` enthält, wird automatisch:
- das `CHANGELOG.md` regeneriert und committed
- der neue Git-Tag erstellt (`v1.2.3`)
- ein Gitea Release mit genau diesem Changelog erzeugt
---
## 🛡️ Hinweis zu Tokens & Webhooks
Damit das Release auch korrekt weitere Workflows auslösen kann (z. B. über `on: release`), ist **ein Personal Access Token notwendig**.
### 🔐 Secret: `RELEASE_PUBLISH_TOKEN`
> Lege ein Repository-Secret mit diesem Namen an.
> Es sollte ein **Gitea Personal Access Token** mit folgenden Berechtigungen sein:
- `write:repo`
- `write:release`
- idealerweise: keine Ablaufzeit
Wird dieser Token **nicht** gesetzt, fällt der Workflow auf `ACTIONS_RUNTIME_TOKEN` zurück, aber:
- Release wird trotzdem erstellt
- **⚠️ andere Workflows, die auf `release.published` reagieren, könnten nicht getriggert werden**
---
## 🧪 Debugging-Tipps
- Stelle sicher, dass `VERSION` exakt **eine gültige neue semver-Version** enthält
- Achte auf den Commit-Log: Changelog-Commits sind mit `chore(changelog):` gekennzeichnet
- Verwende nur `main` als Trigger-Zweig
---
## 🧩 Erweiterung
In `upload-assets.yml` kannst du beliebige Build-Artefakte automatisch an das Release anhängen, sobald es veröffentlicht ist.
Dafür:
- liegt das Script `.gitea/scripts/get-release-id.sh`
- sowie `.gitea/scripts/upload-asset.sh` bereit
Mehr dazu in der Datei: `.gitea/workflows/upload-assets.yml`
---
## 🧘 Best Practice
- Changelog-Generierung nie manuell ausführen
- Nur `VERSION` ändern, um ein neues Release auszulösen
- Auf `CHANGELOG.md` nie direkt committen
- Release-Daten niemals per Hand in Gitea pflegen
📎 Alles wird versioniert, automatisiert und reproduzierbar erzeugt.
---
## 🧠 Commit-Gruppierung & Changelog-Erzeugung
Der Changelog wird auf Basis definierter **Commit-Gruppen** erzeugt.
Diese Regeln sind in `cliff.toml` unter `commit_parsers` konfiguriert.
| Prefix / Muster | Gruppe | Beschreibung |
|-------------------------------|---------------------------|--------------------------------------------------|
| `feat:` | 🚀 Features | Neue Funktionalität |
| `fix:` | 🐛 Bug Fixes | Fehlerbehebungen |
| `doc:` | 📚 Documentation | Änderungen an Doku, Readmes etc. |
| `perf:` | ⚡ Performance | Leistungsverbesserungen |
| `refactor:` | 🚜 Refactor | Reorganisation ohne Verhaltensänderung |
| `style:` | 🎨 Styling | Formatierung, Whitespaces, Code-Style |
| `test:` | 🧪 Testing | Neue oder angepasste Tests |
| `ci:` oder `chore:` (ohne Spezifizierung) | ⚙️ Miscellaneous Tasks | CI-Änderungen, Aufgaben, Wartung etc. |
| `chore(changelog)`, `chore(version)`, `chore(release): prepare for`, `chore(deps...)`, `chore(pr)`, `chore(pull)` | *(ignoriert)* | Diese Commits werden im Changelog **ausgelassen** |
| Commit-Body enthält `security` | 🛡️ Security | Sicherheitsrelevante Änderungen |
| `revert:` | ◀️ Revert | Rückgängig gemachte Commits |
| alles andere | 💼 Other | Fallback für nicht erkannte Formate |
### ✍️ Beispiel:
```bash
git commit -m "feat: add login endpoint"
git commit -m "fix: prevent crash on null input"
git commit -m "chore(version): bump to 1.2.3"
```
> Nur die ersten beiden erscheinen im Changelog – der dritte wird **automatisch übersprungen**.
---
## 🧾 Umgang mit `CHANGELOG.md` beim Mergen und Releasen
Wenn du automatisiert einen Changelog mit `git-cliff` erzeugst, ist `CHANGELOG.md` ein **generiertes Artefakt** – und kein handgepflegter Quelltext.
Beim Mergen von Feature-Branches in `main` kann es deshalb zu **unnötigen Konflikten** in dieser Datei kommen, obwohl der Inhalt später sowieso neu erzeugt wird.
---
## 🧼 Umgang mit `CHANGELOG.md` in Feature-Branches
Wenn du mit **Feature-Branches** arbeitest, wird `CHANGELOG.md` dort oft automatisch erzeugt.
Das kann beim späteren Merge in `main` zu **unnötigen Merge-Konflikten** führen.
### ✅ Empfohlene Vorgehensweise
**Bevor du den Branch mit `main` zusammenführst** (Merge oder Cherry-Pick):
```bash
git rm CHANGELOG.md
git commit -m "chore(changelog): remove generated CHANGELOG.md before merge"
git push
```
Dadurch:
* verhinderst du Merge-Konflikte mit `CHANGELOG.md`
* wird die Datei bei Feature-Branches nicht mehr automatisch erzeugt
* bleibt deine Historie sauber und konfliktfrei
> 💡 Der Workflow erzeugt `CHANGELOG.md` automatisch **nur**, wenn:
>
> * die Datei schon vorhanden ist **oder**
> * der Branch `main` heißt
---
## 🧩 Merge-Konflikte verhindern mit `.gitattributes`
Damit Git bei Konflikten in `CHANGELOG.md` **automatisch deine Version bevorzugt**, kannst du folgende Zeile in die Datei `.gitattributes` aufnehmen:
```gitattributes
CHANGELOG.md merge=ours
```
Das bedeutet:
* Beim Merge wird die Version aus dem aktuellen Branch (`ours`) behalten
* Änderungen aus dem Ziel-Branch (`theirs`) werden verworfen
### ✅ So verwendest du es richtig:
1. **Füge die Regel in `main` hinzu**:
```bash
echo "CHANGELOG.md merge=ours" >> .gitattributes
git add .gitattributes
git commit -m "chore(git): prevent merge conflicts in CHANGELOG.md"
git push origin main
```
2. **Hole sie in deinen Feature-Branch**:
```bash
git checkout feature/xyz
git rebase origin/main
```
3. **Ab sofort werden Konflikte in `CHANGELOG.md` automatisch aufgelöst** – lokal.
> ⚠️ Hinweis: Plattformen wie **Gitea, GitHub oder GitLab ignorieren `.gitattributes` beim Merge über die Web-Oberfläche**.
> Führe Merges daher **lokal** durch, wenn du Konflikte verhindern willst.

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
# Eingaben
TAG="$1"
TOKEN="${ACTIONS_RUNTIME_TOKEN:-<fallback_token>}"
REPO="${GITHUB_REPOSITORY:-owner/example}"
API="${GITHUB_API_URL:-https://gitea.example.tld/api/v1}"
OWNER=$(echo "$REPO" | cut -d/ -f1)
NAME=$(echo "$REPO" | cut -d/ -f2)
RESPONSE=$(curl -sf \
-H "Authorization: token $TOKEN" \
"$API/repos/$OWNER/$NAME/releases/tags/$TAG")
RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id')
echo "Release-ID für $TAG ist: $RELEASE_ID"
# Für GitHub Actions als Umgebungsvariable
echo "GT_RELEASE_ID=$RELEASE_ID" >> "$GITHUB_ENV"

40
.gitea/scripts/upload-asset.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
# Eingabeparameter
FILE_PATH="$1" # z. B. ./dist/build.zip
CUSTOM_NAME="${2:-}" # optional: anderer Name unter dem das Asset gespeichert werden soll
RELEASE_ID="${GT_RELEASE_ID:-}" # aus Umgebung
# Validierung
if [[ -z "$RELEASE_ID" ]]; then
echo "❌ RELEASE_ID ist nicht gesetzt. Abbruch."
exit 1
fi
if [[ ! -f "$FILE_PATH" ]]; then
echo "❌ Datei '$FILE_PATH' existiert nicht. Abbruch."
exit 1
fi
# Default-Konfiguration
TOKEN="${ACTIONS_RUNTIME_TOKEN:-<fallback_token>}"
REPO="${GITHUB_REPOSITORY:-owner/example}"
API="${GITHUB_API_URL:-https://gitea.example.tld/api/v1}"
# Owner/Repo auflösen
OWNER=$(echo "$REPO" | cut -d/ -f1)
NAME=$(echo "$REPO" | cut -d/ -f2)
# Dateiname setzen
FILENAME="${CUSTOM_NAME:-$(basename "$FILE_PATH")}"
echo "🔼 Uploading '$FILE_PATH' as '$FILENAME' to release ID $RELEASE_ID"
# Upload
curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-F "attachment=@$FILE_PATH" \
"$API/repos/$OWNER/$NAME/releases/$RELEASE_ID/assets?name=$FILENAME"
echo "✅ Upload abgeschlossen: $FILENAME"

52
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,52 @@
name: CI
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
- reopened
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Format
id: format
continue-on-error: true
run: deno task fmt
- name: Lint
id: lint
continue-on-error: true
run: deno task lint
- name: Test
id: test
continue-on-error: true
run: deno task test
- name: Benchmark
id: benchmark
continue-on-error: true
run: deno task benchmark
- name: Fail if any step failed
if: |
steps.format.outcome != 'success' ||
steps.lint.outcome != 'success' ||
steps.test.outcome != 'success' ||
steps.benchmark.outcome != 'success'
run: |
echo "::error::One or more steps failed"
exit 1

View File

@@ -0,0 +1,18 @@
name: Auto Changelog & (Release)
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Release
uses: https://git.0xmax42.io/actions/auto-changelog-release-action@v1
with:
token: ${{ secrets.RELEASE_PUBLISH_TOKEN }}

View File

@@ -0,0 +1,27 @@
name: Sync Release to GitHub
on:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: Run Releases Sync Action
uses: https://git.0xmax42.io/actions/releases-sync@main
with:
gitea_token: $ACTIONS_RUNTIME_TOKEN
gitea_url: https://git.0xmax42.io
gitea_owner: maxp
gitea_repo: http-kernel
tag_name: ${{ github.event.release.tag_name }}
github_token: ${{ secrets.SYNC_GITHUB_TOKEN }}
github_owner: 0xMax42
github_repo: http-kernel

2
.gitignore vendored
View File

@@ -2,7 +2,9 @@
coverage/ coverage/
logs/ logs/
.locale/ .locale/
.local/
cache/ cache/
out.py out.py
output.txt output.txt
git_log_diff.txt git_log_diff.txt
.gitea/COMMIT_GPT.md

View File

@@ -18,4 +18,13 @@
], ],
"exportall.config.barrelName": "mod.ts", "exportall.config.barrelName": "mod.ts",
"exportall.config.message": "deno-coverage-ignore-file", "exportall.config.message": "deno-coverage-ignore-file",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#9ed8bc",
"activityBar.background": "#9ed8bc",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#a177c8",
"activityBarBadge.foreground": "#15202b"
},
"peacock.color": "#7ac9a3",
} }

107
CHANGELOG.md Normal file
View File

@@ -0,0 +1,107 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.2.1](https://git.0xmax42.io/maxp/http-kernel/compare/v0.2.0..v0.2.1) - 2025-11-12
### 🚀 Features
- Export errors, interfaces, types, and utils from main module - ([6d7127a](https://git.0xmax42.io/maxp/http-kernel/commit/6d7127a52f4aecfd178523c8a873ab0b558550f1))
### 🐛 Bug Fixes
- *(workflows)* Remove redundant tag fallback in sync job - ([5686940](https://git.0xmax42.io/maxp/http-kernel/commit/5686940fe26b699bffa62af7fb0efc42cc85a6b3))
### ⚙️ Miscellaneous Tasks
- *(workflows)* Update release workflow for consistency - ([f177746](https://git.0xmax42.io/maxp/http-kernel/commit/f1777467607874f6bc83e1d7e37433298e25607c))
## [0.2.0](https://git.0xmax42.io/maxp/http-kernel/compare/v0.1.0..v0.2.0) - 2025-05-27
### 🚀 Features
- *(workflows)* Add GitHub release sync workflow - ([de6d3ee](https://git.0xmax42.io/maxp/http-kernel/commit/de6d3ee389b0d92c5056e47be85da1d0c41f62af))
- *(ci)* Enhance CI workflow with granular steps and error handling - ([54cfa18](https://git.0xmax42.io/maxp/http-kernel/commit/54cfa1888e13d0872b5411e83d3d45925f2687ee))
- *(route-builder)* Add middleware chain compilation - ([35d83c0](https://git.0xmax42.io/maxp/http-kernel/commit/35d83c073ef8644d657195c332b463d18e856e18))
- *(interfaces)* Add runRoute method to IInternalRoute - ([67ebb43](https://git.0xmax42.io/maxp/http-kernel/commit/67ebb4307a2a1c588b78f8f0c498d1a4276ad09b))
- *(workflows)* Conditionally generate changelog - ([b44bb2d](https://git.0xmax42.io/maxp/http-kernel/commit/b44bb2ddafe99c85b25229d2c4a0dfeacf750052))
- *(workflows)* Add CI for Deno project tests - ([9d5db4f](https://git.0xmax42.io/maxp/http-kernel/commit/9d5db4f414cf961248f2b879f2b132b81a32cb92))
### 🐛 Bug Fixes
- *(workflows)* Ensure version detection output is always set - ([3707242](https://git.0xmax42.io/maxp/http-kernel/commit/3707242d278e15c55a41056bb64810f6824d24b3))
### 🚜 Refactor
- *(kernel)* Simplify middleware and handler execution - ([aea3fb4](https://git.0xmax42.io/maxp/http-kernel/commit/aea3fb45e7c099a38440c85783747e80fca54ba6))
- *(imports)* Use explicit type-only imports across codebase - ([b83aa33](https://git.0xmax42.io/maxp/http-kernel/commit/b83aa330b34523e5102ab98ee61dedbbd62d4656))
- *(workflows)* Rename changelog file for consistency - ([b9d25f2](https://git.0xmax42.io/maxp/http-kernel/commit/b9d25f23fc6ad7696deee319024aa5b1af4d98c0))
### 📚 Documentation
- *(release)* Update guidelines for handling changelog - ([6a0f1c7](https://git.0xmax42.io/maxp/http-kernel/commit/6a0f1c774bc01ab976090612bbc361576feb3942))
- Add README for HttpKernel project - ([a1ce306](https://git.0xmax42.io/maxp/http-kernel/commit/a1ce30627c68a3f869eb6a104308322af8596dc1))
- Add MIT license file - ([5118a19](https://git.0xmax42.io/maxp/http-kernel/commit/5118a19aeaa1102591aa7fe093fdec1aa19dc7f5))
### 🧪 Testing
- *(routebuilder)* Add validation tests for handler and middleware - ([b14e9ac](https://git.0xmax42.io/maxp/http-kernel/commit/b14e9acc5f9617a01886e7734b2ae717b86de03e))
- *(httpkernel)* Enforce compile-time validation for signatures - ([731bba2](https://git.0xmax42.io/maxp/http-kernel/commit/731bba22d88df077b0a39293ddd1a3eec3bf96e8))
- *(bench)* Add parallel benchmarks for HTTP kernel - ([3da34e2](https://git.0xmax42.io/maxp/http-kernel/commit/3da34e268426b92510c7f9b730a2fa297dca6fbf))
### ⚙️ Miscellaneous Tasks
- *(config)* Add CI task for local checks - ([c207dc7](https://git.0xmax42.io/maxp/http-kernel/commit/c207dc7392d9f40e7b7c736eadf6c9c7bbf9b7d4))
- *(gitignore)* Add .local directory to ignored files - ([1b447f5](https://git.0xmax42.io/maxp/http-kernel/commit/1b447f51900b3a1a7f1be9d5192fd5aba37bdbc4))
- *(ci)* Update deno tasks in CI workflow - ([38c00b0](https://git.0xmax42.io/maxp/http-kernel/commit/38c00b035bfd05c83d5898c97c9423a653db0840))
- *(tasks)* Add benchmark, format, and lint commands - ([6e6e616](https://git.0xmax42.io/maxp/http-kernel/commit/6e6e61693fef3b11a81ce260d80bc93edae1e718))
- *(workflows)* Consolidate and update CI configuration - ([ec1697d](https://git.0xmax42.io/maxp/http-kernel/commit/ec1697df94b5378f1766663e278a41d403a64336))
- *(config)* Add exports field to module metadata - ([c28eb7f](https://git.0xmax42.io/maxp/http-kernel/commit/c28eb7f28dfaa8d3fdc540c4bcc306a3a8b9d6f8))
- *(git)* Ignore merge conflicts for CHANGELOG.md - ([6399113](https://git.0xmax42.io/maxp/http-kernel/commit/6399113e122e1207ebf4113aebd250358e31f461))
- *(workflows)* Refine branch handling in release process - ([71ea424](https://git.0xmax42.io/maxp/http-kernel/commit/71ea4247b35dc4afe5090d3c6502bfa936b5a947))
- *(workflows)* Update changelog file extension to .md and revert b9d25f23fc - ([a88b4d1](https://git.0xmax42.io/maxp/http-kernel/commit/a88b4d112f5c07664d41f6e9d03246307551f25d))
- Rename changelog and readme files to use .md extension - ([4f2b650](https://git.0xmax42.io/maxp/http-kernel/commit/4f2b65049f461ef377e7231905fd066cbc3c7fe0))
- *(workflows)* Update test workflow for http-kernel project - ([0311546](https://git.0xmax42.io/maxp/http-kernel/commit/03115464e0fb01b8ca00a2fdabde013d004ae8a2))
- *(workflows)* Update Deno setup action to v2 - ([1233a0b](https://git.0xmax42.io/maxp/http-kernel/commit/1233a0b7204d12a60f4b7bd1199242a4cb7c4579))
- *(workflows)* Remove unused workflow_dispatch trigger - ([16c0053](https://git.0xmax42.io/maxp/http-kernel/commit/16c0053964c72d01e5f555ec8f33c9eead160e69))
- *(tasks)* Remove commented-out start and watch scripts - ([04029f8](https://git.0xmax42.io/maxp/http-kernel/commit/04029f87a3b9dd24e8792b852ead9097e18d23c7))
## [0.1.0] - 2025-05-08
### 🚀 Features
- *(workflows)* Add automated changelog and release workflow - ([bbf78cf](https://git.0xmax42.io/maxp/http-kernel/commit/bbf78cff17be0cae651b8abf3e239103b26354bf))
- *(vscode)* Customize activity bar and peacock colors - ([56633cd](https://git.0xmax42.io/maxp/http-kernel/commit/56633cd95b37a8b2cfd8eb95982d07cd1f9b5126))
- *(workflows)* Add upload assets template for releases - ([7b6eb2b](https://git.0xmax42.io/maxp/http-kernel/commit/7b6eb2b57470198684a1dfa8b668351b8b9a91ae))
- *(config)* Add project metadata and test watch task - ([b009b57](https://git.0xmax42.io/maxp/http-kernel/commit/b009b5763d1824fc94fdc1e3d919fe2597158f84))
- *(http)* Add error handling for invalid HTTP methods - ([ba7aa79](https://git.0xmax42.io/maxp/http-kernel/commit/ba7aa79f56772213bf73b62bc6bf8810f3871127))
- *(http)* Enhance type safety and extend route context - ([a236fa7](https://git.0xmax42.io/maxp/http-kernel/commit/a236fa7c97ae49e6baf560d4ca92c6e83702b3ec))
### 🐛 Bug Fixes
- *(params)* Enforce non-undefined route parameter values - ([b0c6901](https://git.0xmax42.io/maxp/http-kernel/commit/b0c6901d7d272ec98b3d00ef2dd2848482892a25))
### 🚜 Refactor
- *(types)* Unify handler and middleware definitions - ([8235680](https://git.0xmax42.io/maxp/http-kernel/commit/8235680904c7f30f25b98b835d48376431108e91))
- *(core)* [**breaking**] Enhance HttpKernel pipeline and matcher system with full context and error handling - ([b7410b4](https://git.0xmax42.io/maxp/http-kernel/commit/b7410b44dd8720e46ee2871aa1727ce5039ebad4))
- *(httpkernel)* Introduce configuration object for flexibility - ([9059bdd](https://git.0xmax42.io/maxp/http-kernel/commit/9059bdda62081c8e775087cabe4c3406e42065a5))
### 📚 Documentation
- *(gitea)* Add release automation guide and scripts - ([5c03cdf](https://git.0xmax42.io/maxp/http-kernel/commit/5c03cdfb031adeb6ee5d0de0889477d6d1efafef))
- *(httpkernel)* Enhance class and interface documentation - ([6c4420d](https://git.0xmax42.io/maxp/http-kernel/commit/6c4420d32f8e7fe317f7c1b0b45de2dcf8565ef5))
### 🧪 Testing
- *(utils)* Rename and update import paths in test file - ([82a6877](https://git.0xmax42.io/maxp/http-kernel/commit/82a687748558f15c2023861a0cc3a33095c86731))
- *(utils)* Add unit tests for parseQuery function - ([94525fc](https://git.0xmax42.io/maxp/http-kernel/commit/94525fce5299f3417801f0152a475892e1edac30))
### ⚙️ Miscellaneous Tasks
- *(config)* Add default git-cliff configuration - ([661f83d](https://git.0xmax42.io/maxp/http-kernel/commit/661f83d1fd0101aa0d5d06b60f6eeb68efac6ceb))
- *(gitignore)* Add .gitea/COMMIT_GPT.md to ignored files - ([f083856](https://git.0xmax42.io/maxp/http-kernel/commit/f0838567b46822327fe739d8de099722e405dfa3))
- *(settings)* Add exportall configuration for barrel name and message - ([0990cac](https://git.0xmax42.io/maxp/http-kernel/commit/0990cacb225e1cbbbbb2a288501df7de9641294f))
- *(.gitignore)* Add git_log_diff.txt to ignore list - ([fd1c7f4](https://git.0xmax42.io/maxp/http-kernel/commit/fd1c7f4170ffffd55ab276090f8b90ee82b853fc))

18
LICENSE Normal file
View File

@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2025 0xMax42
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.

135
README.md Normal file
View File

@@ -0,0 +1,135 @@
# HttpKernel – A Type-Safe Router & Middleware Kernel for Deno
> Fluent routing • Zero-dependency core • 100 % TypeScript
HttpKernel is a small but powerful dispatching engine that turns an ordinary
`Deno.serve()` loop into a structured, middleware-driven HTTP server.
It focuses on **type safety**, **immutability**, and an **expressive builder API**
while staying framework-agnostic and dependency‑free.
---
## ✨ Key Features
* **Fluent Route Builder** – chain middleware and handlers without side effects
* **Static *and* Dynamic Matching** – use URL patterns *or* custom matcher functions
* **First-Class Generics** – strongly‑typed `ctx.params`, `ctx.query`, and `ctx.state`
* **Pluggable Error Handling** – override 404/500 (and any other status) per kernel
* **Response Decorators** – inject CORS headers, security headers, logging, … in one place
* **100 % Test Coverage** – built‑in unit tests ensure every edge case is covered
---
## 🚀 Quick Start
```ts
// Import directly from your repo or deno.land/x
import { HttpKernel } from "https://deno.land/x/httpkernel/mod.ts";
// 1) Create a kernel (optionally pass overrides)
const kernel = new HttpKernel();
// 2) Register a route with fluent chaining
kernel
.route({ method: "GET", path: "/hello/:name" })
.middleware(async (ctx, next) => {
console.log("Incoming request for", ctx.params.name);
return await next(); // continue pipeline
})
.handle(async (ctx) =>
new Response(`Hello ${ctx.params.name}!`, { status: 200 })
);
// 3) Let Deno serve the kernel
Deno.serve(kernel.handle);
```
Run it:
```bash
deno run --allow-net main.ts
# → GET http://localhost:8000/hello/Isaac
```
---
## 🧩 API Overview
| Method / Type | Purpose | Hints |
| --------------------- | ---------------------------------------------- | ------------------------------------------------------------- |
| `kernel.route(def)` | Begin defining a new route. Returns `RouteBuilder`. | `def` can be `{ method, path }` **or** `{ method, matcher }`. |
| `.middleware(fn)` | Add a middleware to the current builder. | Each call returns a *new* builder (immutability). |
| `.handle(fn)` | Finalise the route and register the handler. | Must be called exactly once per route. |
| `kernel.handle(req)` | Kernel entry point you pass to `Deno.serve()`. | Resolves to a `Response`. |
### Context Shape
```ts
interface Context<S = Record<string, unknown>> {
req: Request; // original request
params: Record<string>; // route params e.g. { id: "42" }
query: Record<string | string[]>; // parsed query string
state: S; // per‑request mutable storage
}
```
Generics let you supply your own param / query / state types for full IntelliSense.
---
## 🛠️ Configuration
```ts
new HttpKernel({
decorateResponse: (res, ctx) => {
// add CORS header globally
const headers = new Headers(res.headers);
headers.set("Access-Control-Allow-Origin", "*");
return new Response(res.body, { ...res, headers });
},
httpErrorHandlers: {
404: () => new Response("Nothing here ☹️", { status: 404 }),
500: (_ctx, err) => {
console.error(err);
return new Response("Custom 500", { status: 500 });
},
},
});
```
Everything is optional – omit what you do not override.
---
## 🧪 Testing
All logic is covered by unit tests using `std@0.204.0/testing`.
Run them with:
```bash
deno test -A
```
The CI suite checks:
* Route guards (`isStaticRouteDefinition`, `isDynamicRouteDefinition`)
* Builder immutability & middleware order
* 404 / 500 fall-backs and error propagation
* Middleware mis-use (double `next()`, wrong signatures, …)
---
## 📦 Roadmap
* 🔌 Adapter helpers for Oak / Fresh / any framework that can delegate to `kernel.handle`
* 🔍 Built‑in logger & timing middleware
* 🔒 CSRF & auth middleware presets
* 📝 OpenAPI route generator
Contributions & ideas are welcome – feel free to open an issue or PR.
---
## 📄 License
[MIT](LICENSE)

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.2.1

104
cliff.toml Normal file
View File

@@ -0,0 +1,104 @@
# CLIFF_VERSION=2.8.0
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[remote.gitea]
owner = "maxp"
repo = "http-kernel"
[changelog]
# postprocessors
postprocessors = [
{ pattern = '<GITEA_URL>', replace = "https://git.0xmax42.io" }, # replace gitea url
]
# template for the changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{%- macro remote_url() -%}
<GITEA_URL>/{{ remote.gitea.owner }}/{{ remote.gitea.repo }}
{%- endmacro -%}
{% if version %}\
{% if previous.version %}\
## [{{ version | trim_start_matches(pat="v") }}]\
({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}\
{% 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 }} - \
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% endfor %}
{% endfor %}\n
"""
# template for the changelog footer
footer = """
"""
# remove the leading and trailing s
trim = true
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# Replace issue numbers
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(changelog\\)", skip = true },
{ message = "^chore\\(version\\)", skip = true },
{ 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 = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
{ message = ".*", group = "<!-- 10 -->💼 Other" },
]
# Regex to select git tags that represent releases.
tag_pattern = "v[0-9]+\\.[0-9]+\\.[0-9]+"
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

View File

@@ -1,8 +1,16 @@
{ {
"name": "@0xmax42/http-kernel",
"description": "A simple HTTP kernel for Deno",
"exports": {
"./mod.ts": "./src/mod.ts"
},
"tasks": { "tasks": {
// "start": "deno run --allow-net --allow-env --unstable-kv --allow-read --allow-write --env-file src/main.ts -- --verbose", "test": "deno test --allow-net --allow-env --unstable-kv --allow-read --allow-write --coverage **/__tests__/*.test.ts",
// "watch": "deno run --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write --env-file src/main.ts -- --verbose", "test:watch": "deno test --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__tests__/*.test.ts",
"test": "deno test --allow-net --allow-env --unstable-kv --allow-read --allow-write --coverage **/__tests__/*.test.ts" "benchmark": "deno bench --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__bench__/*.bench.ts",
"fmt": "deno fmt --check",
"lint": "deno lint",
"ci": "deno task fmt && deno task lint && deno task test && deno task benchmark" // For local CI checks
}, },
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": [
@@ -24,6 +32,5 @@
"src/", "src/",
"main.ts" "main.ts"
] ]
}, }
//"importMap": "./import_map.json"
} }

View File

@@ -1,17 +1,13 @@
import { import type {
IContext, IContext,
IHandler,
IHttpKernel, IHttpKernel,
IHttpKernelConfig, IHttpKernelConfig,
IInternalRoute, IInternalRoute,
IMiddleware,
IRouteBuilder, IRouteBuilder,
IRouteDefinition, IRouteDefinition,
isHandler,
isMiddleware,
} from './Interfaces/mod.ts'; } from './Interfaces/mod.ts';
import { import {
DeepPartial, type DeepPartial,
HTTP_404_NOT_FOUND, HTTP_404_NOT_FOUND,
HTTP_500_INTERNAL_SERVER_ERROR, HTTP_500_INTERNAL_SERVER_ERROR,
HttpStatusTextMap, HttpStatusTextMap,
@@ -108,11 +104,12 @@ export class HttpKernel<TContext extends IContext = IContext>
query: match.query, query: match.query,
state: {}, state: {},
} as TContext; } as TContext;
return await this.executePipeline( try {
ctx, const response = await route.runRoute(ctx);
route.middlewares, return this.cfg.decorateResponse(response, ctx);
route.handler, } catch (e) {
); return await this.handleInternalError(ctx, e);
}
} }
} }
@@ -135,65 +132,13 @@ export class HttpKernel<TContext extends IContext = IContext>
this.routes.push(route as unknown as IInternalRoute<TContext>); this.routes.push(route as unknown as IInternalRoute<TContext>);
} }
/** private handleInternalError = (
* Executes the middleware and handler pipeline for a matched route.
*
* This function:
* - Enforces linear middleware execution with `next()` tracking
* - Validates middleware and handler types at runtime
* - Applies the optional response decorator post-processing
* - Handles all runtime errors via the configured 500 handler
*
* @param ctx - The active request context passed to middleware and handler.
* @param middleware - Ordered middleware functions for this route.
* @param handler - The final handler responsible for generating a response.
* @returns The final HTTP `Response`, possibly decorated.
*/
private async executePipeline(
ctx: TContext, ctx: TContext,
middleware: IMiddleware<TContext>[], err?: unknown,
handler: IHandler<TContext>, ): Response | Promise<Response> => {
): Promise<Response> { return this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
const handleInternalError = (ctx: TContext, err?: unknown) =>
this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
ctx, ctx,
normalizeError(err), normalizeError(err),
); );
let lastIndex = -1;
const dispatch = async (currentIndex: number): Promise<Response> => {
if (currentIndex <= lastIndex) {
throw new Error('Middleware called `next()` multiple times');
}
lastIndex = currentIndex;
const isWithinMiddleware = currentIndex < middleware.length;
const fn = isWithinMiddleware ? middleware[currentIndex] : handler;
if (isWithinMiddleware) {
if (!isMiddleware(fn)) {
throw new Error(
'Expected middleware function, but received invalid value',
);
}
return await fn(ctx, () => dispatch(currentIndex + 1));
}
if (!isHandler(fn)) {
throw new Error(
'Expected request handler, but received invalid value',
);
}
return await fn(ctx);
}; };
try {
const response = await dispatch(0);
return this.cfg.decorateResponse(response, ctx);
} catch (e) {
return handleInternalError(ctx, e);
}
}
} }

View File

@@ -1,4 +1,4 @@
import { Params, Query, State } from '../Types/mod.ts'; import type { Params, Query, State } from '../Types/mod.ts';
/** /**
* Represents the complete context for a single HTTP request, * Represents the complete context for a single HTTP request,

View File

@@ -1,5 +1,5 @@
import { IContext } from '../Interfaces/mod.ts'; import type { IContext } from '../Interfaces/mod.ts';
import { HttpErrorHandler, validHttpErrorCodes } from '../Types/mod.ts'; import type { HttpErrorHandler, validHttpErrorCodes } from '../Types/mod.ts';
/** /**
* A mapping of HTTP status codes to their corresponding error handlers. * A mapping of HTTP status codes to their corresponding error handlers.

View File

@@ -1,6 +1,6 @@
import { IContext } from './IContext.ts'; import type { IContext } from './IContext.ts';
import { IRouteBuilder } from './IRouteBuilder.ts'; import type { IRouteBuilder } from './IRouteBuilder.ts';
import { IRouteDefinition } from './IRouteDefinition.ts'; import type { IRouteDefinition } from './IRouteDefinition.ts';
/** /**
* The `IHttpKernel` interface defines the public API for a type-safe, middleware-driven HTTP dispatching system. * The `IHttpKernel` interface defines the public API for a type-safe, middleware-driven HTTP dispatching system.

View File

@@ -1,7 +1,7 @@
import { ResponseDecorator } from '../Types/mod.ts'; import type { ResponseDecorator } from '../Types/mod.ts';
import { IContext } from './IContext.ts'; import type { IContext } from './IContext.ts';
import { IHttpErrorHandlers } from './IHttpErrorHandlers.ts'; import type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
import { IRouteBuilderFactory } from './IRouteBuilder.ts'; import type { IRouteBuilderFactory } from './IRouteBuilder.ts';
export interface IHttpKernelConfig<TContext extends IContext = IContext> { export interface IHttpKernelConfig<TContext extends IContext = IContext> {
decorateResponse: ResponseDecorator<TContext>; decorateResponse: ResponseDecorator<TContext>;

View File

@@ -1,7 +1,5 @@
import { HttpMethod } from '../Types/mod.ts'; import type { Handler, HttpMethod, Middleware } from '../Types/mod.ts';
import { IHandler } from './IHandler.ts'; import type { IContext, IRouteMatcher } from './mod.ts';
import { IMiddleware } from './IMiddleware.ts';
import { IContext, IRouteMatcher } from './mod.ts';
/** /**
* Represents an internally registered route within the HttpKernel. * Represents an internally registered route within the HttpKernel.
@@ -32,10 +30,35 @@ export interface IInternalRoute<TContext extends IContext = IContext> {
/** /**
* An ordered list of middleware functions to be executed before the handler. * An ordered list of middleware functions to be executed before the handler.
*/ */
middlewares: IMiddleware<TContext>[]; middlewares: Middleware<TContext>[];
/** /**
* The final handler that generates the HTTP response after all middleware has run. * The final handler that generates the HTTP response after all middleware has run.
*/ */
handler: IHandler<TContext>; handler: Handler<TContext>;
/**
* The fully compiled execution pipeline for this route.
*
* This function is generated at route registration time and encapsulates the
* entire middleware chain as well as the final handler. It is called by the
* HttpKernel during request dispatch when a route has been matched.
*
* Internally, `runRoute` ensures that each middleware is invoked in the correct order
* and receives a `next()` callback to pass control downstream. The final handler is
* invoked once all middleware has completed or short-circuited the pipeline.
*
* It is guaranteed that:
* - The function is statically compiled and does not perform dynamic dispatching.
* - Each middleware can only call `next()` once; repeated invocations will throw.
* - The return value is either a `Response` or a Promise resolving to one.
*
* @param ctx - The context object carrying route, request, response and other scoped data.
* @returns A `Response` object or a Promise resolving to a `Response`.
*
* @throws {Error} If a middleware calls `next()` more than once.
*/
runRoute: (
ctx: TContext,
) => Promise<Response> | Response;
} }

View File

@@ -1,14 +1,13 @@
import { IHandler } from './IHandler.ts'; import type { Handler, Middleware } from '../Types/mod.ts';
import { IInternalRoute } from './IInternalRoute.ts'; import type { IInternalRoute } from './IInternalRoute.ts';
import { IMiddleware } from './IMiddleware.ts'; import type { IRouteDefinition } from './IRouteDefinition.ts';
import { IRouteDefinition } from './IRouteDefinition.ts'; import type { IContext } from './mod.ts';
import { IContext } from './mod.ts';
export interface IRouteBuilderFactory<TContext extends IContext = IContext> { export interface IRouteBuilderFactory<TContext extends IContext = IContext> {
new ( new (
registerRoute: (route: IInternalRoute<TContext>) => void, registerRoute: (route: IInternalRoute<TContext>) => void,
def: IRouteDefinition, def: IRouteDefinition,
mws?: IMiddleware<TContext>[], mws?: Middleware<TContext>[],
): IRouteBuilder<TContext>; ): IRouteBuilder<TContext>;
} }
@@ -25,7 +24,7 @@ export interface IRouteBuilder<TContext extends IContext = IContext> {
* @returns The route builder for further chaining. * @returns The route builder for further chaining.
*/ */
middleware( middleware(
mw: IMiddleware<TContext>, mw: Middleware<TContext>,
): IRouteBuilder<TContext>; ): IRouteBuilder<TContext>;
/** /**
@@ -35,6 +34,6 @@ export interface IRouteBuilder<TContext extends IContext = IContext> {
* @param handler - The function to execute when this route is matched. * @param handler - The function to execute when this route is matched.
*/ */
handle( handle(
handler: IHandler<TContext>, handler: Handler<TContext>,
): void; ): void;
} }

View File

@@ -1,5 +1,5 @@
import { HttpMethod, isHttpMethod } from '../Types/mod.ts'; import { type HttpMethod, isHttpMethod } from '../Types/mod.ts';
import { IRouteMatcher } from './IRouteMatcher.ts'; import type { IRouteMatcher } from './IRouteMatcher.ts';
/** /**
* Defines a static route using a path pattern with optional parameters. * Defines a static route using a path pattern with optional parameters.

View File

@@ -1,4 +1,4 @@
import { Params, Query } from '../Types/mod.ts'; import type { Params, Query } from '../Types/mod.ts';
export interface IRouteMatch { export interface IRouteMatch {
params?: Params; params?: Params;

View File

@@ -1,6 +1,5 @@
import { Params } from '../Types/mod.ts'; import type { IRouteDefinition } from './IRouteDefinition.ts';
import { IRouteDefinition } from './IRouteDefinition.ts'; import type { IRouteMatch } from './IRouteMatch.ts';
import { IRouteMatch } from './IRouteMatch.ts';
/** /**
* Defines a route matcher function that evaluates whether a route applies to a given request. * Defines a route matcher function that evaluates whether a route applies to a given request.

View File

@@ -1,6 +1,6 @@
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts'; import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { import {
IRouteDefinition, type IRouteDefinition,
isDynamicRouteDefinition, isDynamicRouteDefinition,
isStaticRouteDefinition, isStaticRouteDefinition,
} from '../IRouteDefinition.ts'; } from '../IRouteDefinition.ts';

View File

@@ -1,14 +1,10 @@
// deno-coverage-ignore-file // deno-coverage-ignore-file
export type { IContext } from './IContext.ts'; export type { IContext } from './IContext.ts';
export { isHandler } from './IHandler.ts';
export type { IHandler } from './IHandler.ts';
export type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts'; export type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
export type { IHttpKernel } from './IHttpKernel.ts'; export type { IHttpKernel } from './IHttpKernel.ts';
export type { IHttpKernelConfig } from './IHttpKernelConfig.ts'; export type { IHttpKernelConfig } from './IHttpKernelConfig.ts';
export type { IInternalRoute } from './IInternalRoute.ts'; export type { IInternalRoute } from './IInternalRoute.ts';
export { isMiddleware } from './IMiddleware.ts';
export type { IMiddleware } from './IMiddleware.ts';
export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts'; export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts';
export { export {
isDynamicRouteDefinition, isDynamicRouteDefinition,

View File

@@ -1,12 +1,17 @@
import { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts'; import type { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts';
import { import type {
IContext, IContext,
IHandler, IInternalRoute,
IMiddleware,
IRouteBuilder, IRouteBuilder,
IRouteDefinition, IRouteDefinition,
} from './Interfaces/mod.ts'; } from './Interfaces/mod.ts';
import { RegisterRoute } from './Types/mod.ts'; import { isHandler } from './Types/Handler.ts';
import {
type Handler,
isMiddleware,
type Middleware,
type RegisterRoute,
} from './Types/mod.ts';
import { createRouteMatcher } from './Utils/createRouteMatcher.ts'; import { createRouteMatcher } from './Utils/createRouteMatcher.ts';
/** /**
@@ -27,7 +32,7 @@ export class RouteBuilder<TContext extends IContext = IContext>
constructor( constructor(
private readonly registerRoute: RegisterRoute<TContext>, private readonly registerRoute: RegisterRoute<TContext>,
private readonly def: IRouteDefinition, private readonly def: IRouteDefinition,
private readonly mws: IMiddleware<TContext>[] = [], private readonly mws: Middleware<TContext>[] = [],
private readonly matcherFactory: IRouteMatcherFactory = private readonly matcherFactory: IRouteMatcherFactory =
createRouteMatcher, createRouteMatcher,
) {} ) {}
@@ -42,7 +47,7 @@ export class RouteBuilder<TContext extends IContext = IContext>
* @returns A new `RouteBuilder` instance for continued chaining. * @returns A new `RouteBuilder` instance for continued chaining.
*/ */
middleware( middleware(
mw: IMiddleware<TContext>, mw: Middleware<TContext>,
): IRouteBuilder<TContext> { ): IRouteBuilder<TContext> {
return new RouteBuilder<TContext>( return new RouteBuilder<TContext>(
this.registerRoute, this.registerRoute,
@@ -60,7 +65,7 @@ export class RouteBuilder<TContext extends IContext = IContext>
* @param handler - The final request handler for this route. * @param handler - The final request handler for this route.
*/ */
handle( handle(
handler: IHandler<TContext>, handler: Handler<TContext>,
): void { ): void {
const matcher = this.matcherFactory(this.def); const matcher = this.matcherFactory(this.def);
this.registerRoute({ this.registerRoute({
@@ -68,6 +73,76 @@ export class RouteBuilder<TContext extends IContext = IContext>
matcher, matcher,
middlewares: this.mws, middlewares: this.mws,
handler: handler, handler: handler,
runRoute: this.compile({
middlewares: this.mws,
handler: handler,
}),
}); });
} }
/**
* Compiles the middleware chain and handler into a single executable function.
*
* This method constructs a statically linked function chain by reducing all middleware
* and the final handler into one composed `runRoute` function. Each middleware receives
* a `next()` callback that invokes the next function in the chain.
*
* Additionally, the returned function ensures that `next()` can only be called once
* per middleware. If `next()` is invoked multiple times within the same middleware,
* a runtime `Error` is thrown, preventing unintended double-processing.
*
* Type safety is enforced at compile time:
* - If the final handler does not match the expected signature, a `TypeError` is thrown.
* - If any middleware does not conform to the middleware interface, a `TypeError` is thrown.
*
* @param route - A partial route object containing middleware and handler,
* excluding `matcher`, `method`, and `runRoute`.
* @returns A composed route execution function that takes a context object
* and returns a `Promise<Response>`.
*
* @throws {TypeError} If the handler or any middleware function is invalid.
* @throws {Error} If a middleware calls `next()` more than once during execution.
*/
private compile(
route: Omit<
IInternalRoute<TContext>,
'runRoute' | 'matcher' | 'method'
>,
): (
ctx: TContext,
) => Promise<Response> {
if (!isHandler<TContext>(route.handler)) {
throw new TypeError(
'Route handler must be a function returning a Promise<Response>.',
);
}
let composed = route.handler;
for (let i = route.middlewares.length - 1; i >= 0; i--) {
if (!isMiddleware<TContext>(route.middlewares[i])) {
throw new TypeError(
`Middleware at index ${i} is not a valid function.`,
);
}
const current = route.middlewares[i];
const next = composed;
composed = async (ctx: TContext): Promise<Response> => {
let called = false;
return await current(ctx, async () => {
if (called) {
throw new Error(
`next() called multiple times in middleware at index ${i}`,
);
}
called = true;
return await next(ctx);
});
};
}
return composed;
}
} }

View File

@@ -1,4 +1,4 @@
import { IContext } from './IContext.ts'; import type { IContext } from '../Interfaces/mod.ts';
/** /**
* Represents a final request handler responsible for producing an HTTP response. * Represents a final request handler responsible for producing an HTTP response.
@@ -11,16 +11,23 @@ import { IContext } from './IContext.ts';
* *
* @template TContext The specific context type for this handler, including typed `state`, `params`, and `query`. * @template TContext The specific context type for this handler, including typed `state`, `params`, and `query`.
*/ */
export interface IHandler<TContext extends IContext = IContext> { type Handler<TContext extends IContext = IContext> = (
ctx: TContext,
) => Promise<Response>;
/** /**
* Handles the request and generates a response. * Represents a handler function with an associated name.
* *
* @param ctx - The complete request context, including request metadata, route and query parameters, * This is useful for debugging, logging, or when you need to reference
* and mutable state populated during the middleware phase. * the handler by name in your application.
* @returns A `Promise` resolving to an HTTP `Response` to be sent to the client. *
* @template TContext The specific context type for this handler, including typed `state`, `params`, and `query`.
*/ */
(ctx: TContext): Promise<Response>; type NamedHandler<TContext extends IContext = IContext> =
} & Handler<TContext>
& { name?: string };
export type { NamedHandler as Handler };
/** /**
* Type guard to determine whether a given value is a valid `IHandler` function. * Type guard to determine whether a given value is a valid `IHandler` function.
@@ -42,7 +49,7 @@ export interface IHandler<TContext extends IContext = IContext> {
*/ */
export function isHandler<TContext extends IContext = IContext>( export function isHandler<TContext extends IContext = IContext>(
value: unknown, value: unknown,
): value is IHandler<TContext> { ): value is Handler<TContext> {
return ( return (
typeof value === 'function' && typeof value === 'function' &&
value.length === 1 // ctx value.length === 1 // ctx

View File

@@ -1,4 +1,4 @@
import { IContext } from '../Interfaces/mod.ts'; import type { IContext } from '../Interfaces/mod.ts';
/** /**
* Defines a handler function for errors that occur during the execution * Defines a handler function for errors that occur during the execution

View File

@@ -1,4 +1,4 @@
import { IContext } from './IContext.ts'; import type { IContext } from '../Interfaces/IContext.ts';
/** /**
* Represents a middleware function in the HTTP request pipeline. * Represents a middleware function in the HTTP request pipeline.
@@ -13,16 +13,24 @@ import { IContext } from './IContext.ts';
* *
* @template TContext The specific context type for this middleware, including state, params, and query information. * @template TContext The specific context type for this middleware, including state, params, and query information.
*/ */
export interface IMiddleware<TContext extends IContext = IContext> { type Middleware<TContext extends IContext = IContext> = (
ctx: TContext,
next: () => Promise<Response>,
) => Promise<Response>;
/** /**
* Handles the request processing at this middleware stage. * Represents a middleware function with an associated name.
* *
* @param ctx - The full request context, containing request, params, query, and typed state. * This is useful for debugging, logging, or when you need to reference
* @param next - A continuation function that executes the next middleware or handler in the pipeline. * the middleware by name in your application.
* @returns A `Promise` resolving to an HTTP `Response`, either from this middleware or downstream. *
* @template TContext The specific context type for this middleware, including state, params, and query information.
*/ */
(ctx: TContext, next: () => Promise<Response>): Promise<Response>; type NamedMiddleware<TContext extends IContext = IContext> =
} & Middleware<TContext>
& { name?: string };
export type { NamedMiddleware as Middleware };
/** /**
* Type guard to verify whether a given value is a valid `IMiddleware` function. * Type guard to verify whether a given value is a valid `IMiddleware` function.
@@ -35,7 +43,7 @@ export interface IMiddleware<TContext extends IContext = IContext> {
*/ */
export function isMiddleware<TContext extends IContext = IContext>( export function isMiddleware<TContext extends IContext = IContext>(
value: unknown, value: unknown,
): value is IMiddleware<TContext> { ): value is Middleware<TContext> {
return ( return (
typeof value === 'function' && typeof value === 'function' &&
value.length === 2 // ctx, next value.length === 2 // ctx, next

View File

@@ -1,5 +1,5 @@
import { IContext } from '../Interfaces/IContext.ts'; import type { IContext } from '../Interfaces/IContext.ts';
import { IInternalRoute } from '../Interfaces/mod.ts'; import type { IInternalRoute } from '../Interfaces/mod.ts';
/** /**
* A type alias for the internal route registration function used by the `HttpKernel`. * A type alias for the internal route registration function used by the `HttpKernel`.

View File

@@ -1,4 +1,4 @@
import { IContext } from '../Interfaces/mod.ts'; import type { IContext } from '../Interfaces/mod.ts';
/** /**
* A function that modifies or enriches an outgoing HTTP response before it is returned to the client. * A function that modifies or enriches an outgoing HTTP response before it is returned to the client.

View File

@@ -1,6 +1,8 @@
// deno-coverage-ignore-file // deno-coverage-ignore-file
export type { DeepPartial } from './DeepPartial.ts'; export type { DeepPartial } from './DeepPartial.ts';
export { isHandler } from './Handler.ts';
export type { Handler } from './Handler.ts';
export type { HttpErrorHandler } from './HttpErrorHandler.ts'; export type { HttpErrorHandler } from './HttpErrorHandler.ts';
export { isHttpMethod, validHttpMethods } from './HttpMethod.ts'; export { isHttpMethod, validHttpMethods } from './HttpMethod.ts';
export type { HttpMethod } from './HttpMethod.ts'; export type { HttpMethod } from './HttpMethod.ts';
@@ -34,6 +36,8 @@ export {
validHttpStatusCodes, validHttpStatusCodes,
} from './HttpStatusCode.ts'; } from './HttpStatusCode.ts';
export type { HttpStatusCode } from './HttpStatusCode.ts'; export type { HttpStatusCode } from './HttpStatusCode.ts';
export { isMiddleware } from './Middleware.ts';
export type { Middleware } from './Middleware.ts';
export type { Params } from './Params.ts'; export type { Params } from './Params.ts';
export type { Query } from './Query.ts'; export type { Query } from './Query.ts';
export type { RegisterRoute } from './RegisterRoute.ts'; export type { RegisterRoute } from './RegisterRoute.ts';

View File

@@ -1,6 +1,6 @@
import { assertEquals } from 'https://deno.land/std/assert/mod.ts'; import { assertEquals } from 'https://deno.land/std/assert/mod.ts';
import { createEmptyContext } from '../createEmptyContext.ts'; import { createEmptyContext } from '../createEmptyContext.ts';
import { IContext } from '../../Interfaces/mod.ts'; import type { IContext } from '../../Interfaces/mod.ts';
Deno.test('createEmptyContext: returns default-initialized context', () => { Deno.test('createEmptyContext: returns default-initialized context', () => {
const request = new Request('http://localhost'); const request = new Request('http://localhost');

View File

@@ -3,7 +3,7 @@ import {
assertEquals, assertEquals,
assertStrictEquals, assertStrictEquals,
} from 'https://deno.land/std/assert/mod.ts'; } from 'https://deno.land/std/assert/mod.ts';
import { IRouteDefinition } from '../../Interfaces/mod.ts'; import type { IRouteDefinition } from '../../Interfaces/mod.ts';
import { createRouteMatcher } from '../../mod.ts'; import { createRouteMatcher } from '../../mod.ts';
// Dummy request // Dummy request

View File

@@ -1,5 +1,5 @@
import { IContext } from '../Interfaces/mod.ts'; import type { IContext } from '../Interfaces/mod.ts';
import { Params, Query, State } from '../Types/mod.ts'; import type { Params, Query, State } from '../Types/mod.ts';
/** /**
* Creates an empty request context suitable for fallback handlers (e.g., 404 or 500 errors). * Creates an empty request context suitable for fallback handlers (e.g., 404 or 500 errors).

View File

@@ -1,12 +1,12 @@
// createRouteMatcher.ts // createRouteMatcher.ts
import { import {
IRouteDefinition, type IRouteDefinition,
IRouteMatch, type IRouteMatch,
IRouteMatcher, type IRouteMatcher,
isDynamicRouteDefinition, isDynamicRouteDefinition,
} from '../Interfaces/mod.ts'; } from '../Interfaces/mod.ts';
import { Params, Query } from '../Types/mod.ts'; import type { Params, Query } from '../Types/mod.ts';
/** /**
* Transforms a route definition into a matcher using Deno's URLPattern API. * Transforms a route definition into a matcher using Deno's URLPattern API.
@@ -35,7 +35,9 @@ export function createRouteMatcher(
// 3b. Extract route params // 3b. Extract route params
const params: Params = {}; const params: Params = {};
for (const [key, value] of Object.entries(result.pathname.groups)) { for (const [key, value] of Object.entries(result.pathname.groups)) {
params[key] = value ?? ''; // null → empty string if (value) {
params[key] = value;
}
} }
// 3c. Extract query parameters – keep duplicates as arrays // 3c. Extract query parameters – keep duplicates as arrays

View File

@@ -22,9 +22,7 @@
* ``` * ```
*/ */
export function normalizeError(unknownError: unknown): Error { export function normalizeError(unknownError: unknown): Error {
return unknownError instanceof Error return unknownError instanceof Error ? unknownError : new Error(
? unknownError
: new Error(
typeof unknownError === 'string' typeof unknownError === 'string'
? unknownError ? unknownError
: JSON.stringify(unknownError), : JSON.stringify(unknownError),

View File

@@ -0,0 +1,87 @@
import type { IRouteDefinition } from '../Interfaces/mod.ts';
import { HttpKernel } from '../mod.ts';
const CONCURRENT_REQUESTS = 10000;
// Deno.bench('Simple request', async (b) => {
// const kernel = new HttpKernel();
// const def: IRouteDefinition = { method: 'GET', path: '/hello' };
// kernel.route(def).handle((_ctx) => {
// return Promise.resolve(new Response('OK', { status: 200 }));
// });
// b.start();
// await kernel.handle(
// new Request('http://localhost/hello', { method: 'GET' }),
// );
// b.end();
// });
Deno.bench('Simple request (parallel)', async (b) => {
const kernel = new HttpKernel();
const def: IRouteDefinition = { method: 'GET', path: '/hello' };
kernel.route(def).handle((_ctx) => {
return Promise.resolve(new Response('OK', { status: 200 }));
});
const requests = Array.from(
{ length: CONCURRENT_REQUESTS },
() =>
kernel.handle(
new Request('http://localhost/hello', { method: 'GET' }),
),
);
b.start();
await Promise.all(requests);
b.end();
});
// Deno.bench('Complex request', async (b) => {
// const kernel = new HttpKernel();
// kernel.route({ method: 'GET', path: '/test' })
// .middleware(async (_ctx, next) => {
// return await next();
// })
// .middleware(async (_ctx, next) => {
// return await next();
// })
// .handle((_ctx) => {
// return Promise.resolve(new Response('done'));
// });
// b.start();
// await kernel.handle(
// new Request('http://localhost/test', { method: 'GET' }),
// );
// b.end();
// });
Deno.bench('Complex request (parallel)', async (b) => {
const kernel = new HttpKernel();
kernel.route({ method: 'GET', path: '/test' })
.middleware(async (_ctx, next) => {
return await next();
})
.middleware(async (_ctx, next) => {
return await next();
})
.handle((_ctx) => {
return Promise.resolve(new Response('done'));
});
const requests = Array.from(
{ length: CONCURRENT_REQUESTS },
() =>
kernel.handle(
new Request('http://localhost/test', { method: 'GET' }),
),
);
b.start();
await Promise.all(requests);
b.end();
});

View File

@@ -1,6 +1,9 @@
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts'; import {
assertEquals,
assertThrows,
} from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { HttpKernel } from '../HttpKernel.ts'; import { HttpKernel } from '../HttpKernel.ts';
import { IRouteDefinition } from '../Interfaces/mod.ts'; import type { IRouteDefinition } from '../Interfaces/mod.ts';
Deno.test('HttpKernel: matches static route and executes handler', async () => { Deno.test('HttpKernel: matches static route and executes handler', async () => {
const kernel = new HttpKernel(); const kernel = new HttpKernel();
@@ -88,30 +91,32 @@ Deno.test('HttpKernel: middleware short-circuits pipeline', async () => {
assertEquals(calls, ['mw1']); assertEquals(calls, ['mw1']);
}); });
Deno.test('HttpKernel: invalid middleware or handler signature triggers 500', async () => { Deno.test('HttpKernel: invalid middleware or handler signature throws at compile time', () => {
const kernel = new HttpKernel(); const kernel = new HttpKernel();
// Middleware with wrong signature (missing ctx, next) // Middleware with wrong signature (missing ctx, next)
assertThrows(
() => {
kernel.route({ method: 'GET', path: '/bad-mw' }) kernel.route({ method: 'GET', path: '/bad-mw' })
// @ts-expect-error invalid middleware // @ts-expect-error invalid middleware
.middleware(() => new Response('invalid')) .middleware(() => new Response('invalid'))
.handle((_ctx) => Promise.resolve(new Response('ok'))); .handle((_ctx) => Promise.resolve(new Response('ok')));
},
const res1 = await kernel.handle(new Request('http://localhost/bad-mw')); TypeError,
assertEquals(res1.status, 500); 'Middleware at index 0 is not a valid function.',
assertEquals(await res1.text(), 'Internal Server Error'); );
// Handler with wrong signature (no ctx) // Handler with wrong signature (no ctx)
assertThrows(
() => {
kernel.route({ method: 'GET', path: '/bad-handler' }) kernel.route({ method: 'GET', path: '/bad-handler' })
.middleware(async (_ctx, next) => await next()) .middleware(async (_ctx, next) => await next())
// @ts-expect-error invalid handler // @ts-expect-error invalid handler
.handle(() => new Response('invalid')); .handle(() => new Response('invalid'));
},
const res2 = await kernel.handle( TypeError,
new Request('http://localhost/bad-handler'), 'Route handler must be a function returning a Promise<Response>.',
); );
assertEquals(res2.status, 500);
assertEquals(await res2.text(), 'Internal Server Error');
}); });
Deno.test('HttpKernel: 404 for unmatched route', async () => { Deno.test('HttpKernel: 404 for unmatched route', async () => {
@@ -124,7 +129,7 @@ Deno.test('HttpKernel: skips route with wrong method', async () => {
const kernel = new HttpKernel(); const kernel = new HttpKernel();
kernel.route({ method: 'POST', path: '/only-post' }) kernel.route({ method: 'POST', path: '/only-post' })
.handle(() => Promise.resolve(new Response('nope'))); .handle((_ctx) => Promise.resolve(new Response('nope')));
const res = await kernel.handle( const res = await kernel.handle(
new Request('http://localhost/only-post', { method: 'GET' }), new Request('http://localhost/only-post', { method: 'GET' }),
@@ -152,7 +157,7 @@ Deno.test('HttpKernel: handler throws → error propagates', async () => {
const kernel = new HttpKernel(); const kernel = new HttpKernel();
kernel.route({ method: 'GET', path: '/throw' }) kernel.route({ method: 'GET', path: '/throw' })
.handle(() => { .handle((_ctx) => {
throw new Error('fail!'); throw new Error('fail!');
}); });

View File

@@ -4,20 +4,30 @@ import {
assertNotEquals, assertNotEquals,
assertThrows, assertThrows,
} from 'https://deno.land/std@0.204.0/assert/mod.ts'; } from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { import type { IInternalRoute, IRouteDefinition } from '../Interfaces/mod.ts';
IHandler,
IInternalRoute,
IMiddleware,
IRouteDefinition,
} from '../Interfaces/mod.ts';
import { RouteBuilder } from '../mod.ts'; import { RouteBuilder } from '../mod.ts';
import type { Handler, Middleware } from '../Types/mod.ts';
// Dummy objects // Dummy objects
const dummyHandler: IHandler = async () => new Response('ok'); // deno-lint-ignore require-await
const dummyMiddleware: IMiddleware = async (_, next) => await next(); const dummyHandler: Handler = async (_) => new Response('ok');
// deno-lint-ignore require-await
const wrongHandler: Handler = async () => new Response('ok'); // Wrong signature, no ctx
const dummyMiddleware: Middleware = async (_, next) => await next();
// deno-lint-ignore require-await
const wrongMiddleware: Middleware = async () => new Response('ok'); // Wrong signature, no ctx, next
const dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' }; const dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' };
const dummyMatcher = () => ({ params: {} }); const dummyMatcher = () => ({ params: {} });
Deno.test('middleware: throws if middleware signature is wrong', () => {
const builder = new RouteBuilder(() => {}, dummyDef);
assertThrows(
() => builder.middleware(wrongMiddleware).handle(dummyHandler),
TypeError,
'Middleware at index 0 is not a valid function.',
);
});
Deno.test('middleware: single middleware is registered correctly', () => { Deno.test('middleware: single middleware is registered correctly', () => {
let registered: IInternalRoute | null = null as IInternalRoute | null; let registered: IInternalRoute | null = null as IInternalRoute | null;
@@ -39,8 +49,8 @@ Deno.test('middleware: middleware is chained immutably', () => {
}); });
Deno.test('middleware: preserves order of middleware', () => { Deno.test('middleware: preserves order of middleware', () => {
const mw1: IMiddleware = async (_, next) => await next(); const mw1: Middleware = async (_, next) => await next();
const mw2: IMiddleware = async (_, next) => await next(); const mw2: Middleware = async (_, next) => await next();
let result: IInternalRoute | null = null as IInternalRoute | null; let result: IInternalRoute | null = null as IInternalRoute | null;
@@ -54,6 +64,15 @@ Deno.test('middleware: preserves order of middleware', () => {
assertEquals(result!.middlewares, [mw1, mw2]); assertEquals(result!.middlewares, [mw1, mw2]);
}); });
Deno.test('handle: throws if handler signature is wrong', () => {
const builder = new RouteBuilder(() => {}, dummyDef);
assertThrows(
() => builder.handle(wrongHandler),
TypeError,
'Route handler must be a function returning a Promise<Response>.',
);
});
Deno.test('handle: uppercases method', () => { Deno.test('handle: uppercases method', () => {
let result: IInternalRoute | null = null as IInternalRoute | null; let result: IInternalRoute | null = null as IInternalRoute | null;
@@ -94,7 +113,7 @@ Deno.test('handle: works with no middleware', async () => {
Deno.test('handle: uses custom matcher factory', () => { Deno.test('handle: uses custom matcher factory', () => {
let called = false; let called = false;
const factory = (def: IRouteDefinition) => { const factory = (_def: IRouteDefinition) => {
called = true; called = true;
return dummyMatcher; return dummyMatcher;
}; };

View File

@@ -2,3 +2,15 @@
export { HttpKernel } from './HttpKernel.ts'; export { HttpKernel } from './HttpKernel.ts';
export { RouteBuilder } from './RouteBuilder.ts'; export { RouteBuilder } from './RouteBuilder.ts';
export { createRouteMatcher } from './Utils/createRouteMatcher.ts'; export { createRouteMatcher } from './Utils/createRouteMatcher.ts';
// Errors
export * from './Errors/mod.ts';
// Interfaces
export * from './Interfaces/mod.ts';
// Types
export * from './Types/mod.ts';
// Utils
export * from './Utils/mod.ts';