39 Commits

Author SHA1 Message Date
5d1b0517a5 chore(changelog): update unreleased changelog 2025-05-10 15:51:08 +00:00
6ce73c14fa 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 8s
Test http-kernel / Run Tests (pull_request) Successful in 14s
- 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:05 +02:00
0aac2337a0 chore(changelog): update unreleased changelog 2025-05-10 15:42:59 +00:00
2ab6f1b8db feat(workflows): conditionally generate changelog
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Test http-kernel / Run Tests (pull_request) Successful in 12s
Auto Changelog & Release / changelog-only (push) Successful in 13s
- 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:42:19 +02:00
b69a51247d chore(changelog): update unreleased changelog 2025-05-10 15:28:39 +00:00
2ab74b9859 chore(changelog): update unreleased changelog
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Test http-kernel / Run Tests (pull_request) Successful in 14s
Auto Changelog & Release / changelog-only (push) Successful in 9s
2025-05-10 17:28:21 +02:00
d04dfcd63e chore(git): ignore merge conflicts for CHANGELOG.md 2025-05-10 17:27:55 +02:00
32f3fe5f52 chore(changelog): update unreleased changelog 2025-05-10 15:01:16 +00:00
abd2d6e840 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
Test http-kernel / Run Tests (pull_request) Successful in 13s
Auto Changelog & Release / changelog-only (push) Successful in 10s
- 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 16:59:53 +02:00
927a9081d4 feat(interfaces): add pipeline executor interface
All checks were successful
Auto Changelog & Release / detect-version-change (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Has been skipped
Auto Changelog & Release / release (push) Has been skipped
Test http-kernel / Run Tests (pull_request) Successful in 13s
- Introduce `IPipelineExecutor` to define pipeline execution
- Add `PipelineExecutorFactory` for instantiating pipeline executors
- Export new types in the interfaces module for external use
2025-05-10 16:50:41 +02:00
8f94cc915c chore(workflows): refine branch handling in release process
All checks were successful
Test http-kernel / Run Tests (pull_request) Successful in 16s
Auto Changelog & Release / detect-version-change (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Has been skipped
Auto Changelog & Release / release (push) Has been skipped
- 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:34:19 +02:00
3f114bb68d feat(pipeline): add configuration and hooks for pipeline execution
- Introduce `IPipelineExecutorConfig` to enable customizable pipeline behavior
- Add `IPipelineHooks` interface for tracing and monitoring lifecycle events
- Define callback types for pipeline start, step execution, and completion
- Export new types and interfaces for broader integration within the system
2025-05-10 16:21:16 +02:00
0846dbb758 docs(pipeline): add design plan for PipelineExecutor class
All checks were successful
Test http-kernel / Run Tests (pull_request) Successful in 10s
- Introduce a detailed plan for the PipelineExecutor class
- Describe its purpose, interface, hooks, and internal workflow
- Highlight advantages such as decoupling, testability, and extensibility
- Will be removed before merge
2025-05-08 22:05:30 +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
30 changed files with 1225 additions and 70 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"

View File

@@ -0,0 +1,222 @@
name: Auto Changelog & Release
on:
push:
branches:
- main
- '**'
jobs:
detect-version-change:
runs-on: ubuntu-latest
outputs:
version_changed: ${{ steps.set.outputs.version_changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if VERSION file changed
if: github.ref == 'refs/heads/main'
run: |
echo "🔍 Vergleich mit github.event.before:"
echo "Before: ${{ github.event.before }}"
echo "After: ${{ github.sha }}"
echo "📄 Changed files between before and after:"
git diff --name-only ${{ github.event.before }} ${{ github.sha }} || echo "(diff failed)"
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q '^VERSION$'; then
echo "✅ VERSION file was changed"
echo "VERSION_CHANGED=true" >> $GITHUB_ENV
else
echo "ℹ️ VERSION file not changed"
echo "VERSION_CHANGED=false" >> $GITHUB_ENV
fi
- name: Set output (always)
id: set
run: |
echo "version_changed=${VERSION_CHANGED:-false}" >> $GITHUB_OUTPUT
changelog-only:
needs: detect-version-change
if: github.ref != 'refs/heads/main' || needs.detect-version-change.outputs.version_changed == 'false'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set Git Author
run: |
git config user.name "$CI_COMMIT_AUTHOR_NAME"
git config user.email "$CI_COMMIT_AUTHOR_EMAIL"
- name: Read CLIFF_VERSION from cliff.toml
id: cliff_version
run: |
echo "version=$(awk -F '=' '/^# CLIFF_VERSION=/ { gsub(/[" ]/, "", $2); print $2 }' cliff.toml)" >> $GITHUB_OUTPUT
- name: Restore git-cliff cache
id: restore-cliff
uses: https://git.0xmax42.io/actions/cache@v1
with:
key: cargo-cliff-${{ steps.cliff_version.outputs.version }}
paths: |
/root/.cargo/bin
- name: Install git-cliff
if: steps.restore-cliff.outputs.cache-hit != 'true'
run: |
cargo install git-cliff --version "${{ steps.cliff_version.outputs.version }}" --features gitea
- name: Generate unreleased changelog (if file exists or on main)
run: |
if [[ -f CHANGELOG.md || "${GITHUB_REF##refs/heads/}" == "main" ]]; then
echo "Generating CHANGELOG.md..."
git-cliff -c cliff.toml -o CHANGELOG.md
else
echo "CHANGELOG.md does not exist and this is not 'main'. Skipping generation."
fi
- name: Commit updated CHANGELOG
run: |
git add CHANGELOG.md
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore(changelog): update unreleased changelog"
git push origin "${GITHUB_REF##refs/heads/}"
fi
release:
needs: detect-version-change
if: needs.detect-version-change.outputs.version_changed == 'true' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set Git Author
run: |
git config user.name "$CI_COMMIT_AUTHOR_NAME"
git config user.email "$CI_COMMIT_AUTHOR_EMAIL"
- name: Read VERSION
id: version
run: echo "value=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Read CLIFF_VERSION from cliff.toml
id: cliff_version
run: |
echo "version=$(awk -F '=' '/^# CLIFF_VERSION=/ { gsub(/[" ]/, "", $2); print $2 }' cliff.toml)" >> $GITHUB_OUTPUT
- name: Restore git-cliff cache
id: restore-cliff
uses: https://git.0xmax42.io/actions/cache@v1
with:
key: cargo-cliff-${{ steps.cliff_version.outputs.version }}
paths: |
/root/.cargo/bin
- name: Install git-cliff
if: steps.restore-cliff.outputs.cache-hit != 'true'
run: |
cargo install git-cliff --version "${{ steps.cliff_version.outputs.version }}" --features gitea
- name: Generate changelog for release and tag
id: generate-changelog
run: |
VERSION=${{ steps.version.outputs.value }}
git-cliff -c cliff.toml -t "v$VERSION" -o CHANGELOG.md
BODY=$(mktemp)
ESCAPED_VERSION=$(echo "$VERSION" | sed 's/\./\\./g')
awk -v ver="$ESCAPED_VERSION" '
$0 ~ "^## \\[" ver "\\]" {
print_flag=1
line = $0
sub(/^## /, "", line)
sub(/\\s*\\(.*\\)/, "", line) # entfernt z. B. "(...)" oder "(*)"
print line
next
}
$0 ~ "^## \\[" && $0 !~ "^## \\[" ver "\\]" {
print_flag=0
}
print_flag
' CHANGELOG.md > "$BODY"
echo "changelog_body_path=$BODY" >> $GITHUB_OUTPUT
- name: Commit updated CHANGELOG
run: |
git add CHANGELOG.md
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore(changelog): update changelog for v${{ steps.version.outputs.value }}"
git push origin main
fi
- name: Create Git tag (if not exists)
run: |
VERSION=${{ steps.version.outputs.value }}
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "Tag v$VERSION already exists, skipping tag creation."
else
git tag -a "v$VERSION" -F "${{ steps.generate-changelog.outputs.changelog_body_path }}" --cleanup=verbatim
git push origin "v$VERSION"
fi
- name: Create Gitea release
env:
RELEASE_PUBLISH_TOKEN: ${{ secrets.RELEASE_PUBLISH_TOKEN }}
run: |
VERSION=${{ steps.version.outputs.value }}
BODY_FILE="${{ steps.generate-changelog.outputs.changelog_body_path }}"
OWNER=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f1)
REPO=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)
# Token-Auswahl
TOKEN="${RELEASE_PUBLISH_TOKEN:-$ACTIONS_RUNTIME_TOKEN}"
if [[ -z "${RELEASE_PUBLISH_TOKEN:-}" ]]; then
echo "::warning title=Limited Release Propagation::"
echo "RELEASE_PUBLISH_TOKEN is not set. Using ACTIONS_RUNTIME_TOKEN instead."
echo "⚠️ Release events may not trigger other workflows if created with the runtime token."
echo
fi
# Prüfe, ob der Release schon existiert
if curl -sf "$GITHUB_API_URL/repos/$OWNER/$REPO/releases/tags/v$VERSION" \
-H "Authorization: token $TOKEN" > /dev/null; then
echo "🔁 Release for tag v$VERSION already exists, skipping."
exit 0
fi
echo "🚀 Creating Gitea release for v$VERSION"
# Release-Beschreibung vorbereiten
RELEASE_BODY=$(tail -n +2 "$BODY_FILE" | jq -Rs .)
curl -X POST "$GITHUB_API_URL/repos/$OWNER/$REPO/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"tag_name": "v$VERSION",
"target_commitish": "main",
"name": "Release v$VERSION",
"body": $RELEASE_BODY,
"draft": false,
"prerelease": false
}
EOF
echo "✅ Release for tag $VERSION created successfully."

22
.gitea/workflows/test.yml Normal file
View File

@@ -0,0 +1,22 @@
name: Test http-kernel
on:
workflow_dispatch:
pull_request:
branches:
- '**'
jobs:
run-test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Deno
uses: denoland/setup-deno@v2
- name: Run Tests
run: deno task test

View File

@@ -0,0 +1,47 @@
# ========================
# 📦 Upload Assets Template
# ========================
# Dieser Workflow wird automatisch ausgelöst, wenn ein Release
# in Gitea veröffentlicht wurde (event: release.published).
#
# Er dient dem Zweck, Release-Artefakte (wie z. B. Binary-Dateien,
# Changelogs oder Build-Zips) nachträglich mit dem Release zu verknüpfen.
#
# Voraussetzung: Zwei Shell-Skripte liegen im Projekt:
# - .gitea/scripts/get-release-id.sh → ermittelt Release-ID per Tag
# - .gitea/scripts/upload-asset.sh → lädt Datei als Release-Asset hoch
#
# --------------------------------------
name: Upload Assets
on:
release:
types: [published] # Nur bei Veröffentlichung eines Releases (nicht bei Entwürfen)
jobs:
upload-assets:
runs-on: ubuntu-latest
steps:
# 📥 Checke den Stand des Repos aus, exakt auf dem veröffentlichten Tag
# So ist garantiert, dass die Artefakte dem Zustand des Releases entsprechen.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }} # z. B. "v1.2.3"
fetch-depth: 0 # vollständige Git-Historie (für z. B. git-cliff, logs etc.)
# 🆔 Hole die Release-ID basierend auf dem Tag
# Die ID wird als Umgebungsvariable RELEASE_ID über $GITHUB_ENV verfügbar gemacht.
- name: Get Release ID from tag
run: .gitea/scripts/get-release-id.sh "${{ github.event.release.tag_name }}"
# 🔼 Upload eines Release-Assets
# Beispiel: Lade CHANGELOG.md als Datei mit abweichendem Namen "RELEASE-NOTES.md" hoch
#
# Du kannst beliebig viele Upload-Schritte hinzufügen oder in einer Schleife iterieren.
#
# Hinweis: RELEASE_ID wird automatisch verwendet, da get-release-id.sh sie exportiert.
#
# - name: Upload CHANGELOG.md as RELEASE-NOTES.md
# run: .gitea/scripts/upload-asset.sh ./CHANGELOG.md RELEASE-NOTES.md

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ 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",
} }

78
CHANGELOG.md Normal file
View File

@@ -0,0 +1,78 @@
# Changelog
All notable changes to this project will be documented in this file.
## [unreleased]
### 🚀 Features
- *(workflows)* Conditionally generate changelog - ([2ab6f1b](https://git.0xmax42.io/maxp/http-kernel/commit/2ab6f1b8db2d7bd31ca30248d0de183f17a5738c))
- *(interfaces)* Add pipeline executor interface - ([927a908](https://git.0xmax42.io/maxp/http-kernel/commit/927a9081d4f363202520d017eb424c7c097ced94))
- *(pipeline)* Add configuration and hooks for pipeline execution - ([3f114bb](https://git.0xmax42.io/maxp/http-kernel/commit/3f114bb68d94c48a53514752d57cb4f01adeaae3))
- *(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 - ([abd2d6e](https://git.0xmax42.io/maxp/http-kernel/commit/abd2d6e8402662f863d9974aaa0bc228a4777724))
### 🚜 Refactor
- *(workflows)* Rename changelog file for consistency - ([b9d25f2](https://git.0xmax42.io/maxp/http-kernel/commit/b9d25f23fc6ad7696deee319024aa5b1af4d98c0))
### 📚 Documentation
- *(release)* Update guidelines for handling changelog - ([6ce73c1](https://git.0xmax42.io/maxp/http-kernel/commit/6ce73c14fa6736b622e646feb61522e6ec1f4c5a))
- *(pipeline)* Add design plan for PipelineExecutor class - ([0846dbb](https://git.0xmax42.io/maxp/http-kernel/commit/0846dbb758ba788f969a381c56498920ee0f9562))
- 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))
### ⚙️ Miscellaneous Tasks
- *(git)* Ignore merge conflicts for CHANGELOG.md - ([d04dfcd](https://git.0xmax42.io/maxp/http-kernel/commit/d04dfcd63ea2478ffdff2e966d310194dafd8d7d))
- *(workflows)* Refine branch handling in release process - ([8f94cc9](https://git.0xmax42.io/maxp/http-kernel/commit/8f94cc915c75a11efa1a8e3bdc51ffea9c2f19b5))
- *(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.1.0

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,9 @@
{ {
"name": "@0xmax42/http-kernel",
"description": "A simple HTTP kernel for Deno",
"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"
}, },
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": [
@@ -24,6 +25,6 @@
"src/", "src/",
"main.ts" "main.ts"
] ]
}, }
//"importMap": "./import_map.json" //"importMap": "./import_map.json"
} }

View File

@@ -0,0 +1,72 @@
# 🧩 Plan: `PipelineExecutor<TContext>`
## 🎯 Ziel
Eine eigenständige, testbare Klasse zur Ausführung einer Middleware- und Handler-Pipeline, die:
- Linear und sauber das `next()`-Verhalten abbildet
- Typvalidierung durchführt (`isMiddleware`, `isHandler`)
- Fehler behandelt und an konfigurierbare Handler weiterleitet
- Optionale Hooks zur Tracing-Integration bietet (z. B. für Zeitmessung, Logging)
- Am Ende eine dekorierte `Response` zurückliefert
---
## 🧩 Schnittstelle (API)
```ts
class PipelineExecutor<TContext extends IContext> {
constructor(cfg: IHttpKernelConfig<TContext>);
run(
ctx: TContext,
middleware: Middleware<TContext>[],
handler: Handler<TContext>,
hooks?: IPipelineHooks<TContext>, // optional
): Promise<Response>;
}
```
---
## 🪝 Hook-Schnittstelle (`IPipelineHooks`)
```ts
interface IPipelineHooks<TContext> {
onPipelineStart?(ctx: TContext): void;
onStepStart?(name: string | undefined, ctx: TContext): void;
onStepEnd?(name: string | undefined, ctx: TContext, duration: number): void;
onPipelineEnd?(ctx: TContext, totalDuration: number): void;
}
```
- `name` ist `undefined`, wenn keine `.name` am Handler/Middleware gesetzt ist
- Diese Hooks ermöglichen später Logging, Zeitmessung, Statistiken etc.
- Der `TraceManager` wird dieses Interface implementieren
---
## 🛠️ Interne Aufgaben / Ablauf
1. `run(...)` beginnt mit Aufruf `onPipelineStart(ctx)`
2. Zeitmessung (`performance.now()`)
3. Dispatcher-Funktion führt jede Middleware mit `next()`-Kette aus
4. Vor jedem Aufruf: `onStepStart(name, ctx)`
5. Nach jedem Aufruf: `onStepEnd(name, ctx, duration)`
6. Nach letztem Handler: `onPipelineEnd(ctx, totalDuration)`
7. Ergebnis wird durch `cfg.decorateResponse(res, ctx)` geschickt
8. Im Fehlerfall: `cfg.httpErrorHandlers[500](ctx, error)`
---
## ✅ Vorteile
- `HttpKernel` ist von Ausführungsdetails entkoppelt
- Tracing-/Logging-System kann ohne Invasivität angeschlossen werden
- Sehr gut testbar (z. B. Middleware-Mock + Hook-Aufrufe prüfen)
- Erweiterbar für Timeout, Async-Context, Abbruchlogik etc.
---
## 📦 Dateiname-Vorschlag
- `src/Core/PipelineExecutor.ts` oder
- `src/HttpKernel/PipelineExecutor.ts`

View File

@@ -1,20 +1,20 @@
import { import {
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, DeepPartial,
Handler,
HTTP_404_NOT_FOUND, HTTP_404_NOT_FOUND,
HTTP_500_INTERNAL_SERVER_ERROR, HTTP_500_INTERNAL_SERVER_ERROR,
HttpStatusTextMap, HttpStatusTextMap,
isHandler,
isMiddleware,
Middleware,
} from './Types/mod.ts'; } from './Types/mod.ts';
import { RouteBuilder } from './RouteBuilder.ts'; import { RouteBuilder } from './RouteBuilder.ts';
import { createEmptyContext, normalizeError } from './Utils/mod.ts'; import { createEmptyContext, normalizeError } from './Utils/mod.ts';
@@ -151,8 +151,8 @@ export class HttpKernel<TContext extends IContext = IContext>
*/ */
private async executePipeline( private async executePipeline(
ctx: TContext, ctx: TContext,
middleware: IMiddleware<TContext>[], middleware: Middleware<TContext>[],
handler: IHandler<TContext>, handler: Handler<TContext>,
): Promise<Response> { ): Promise<Response> {
const handleInternalError = (ctx: TContext, err?: unknown) => const handleInternalError = (ctx: TContext, err?: unknown) =>
this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR]( this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](

View File

@@ -1,6 +1,4 @@
import { HttpMethod } from '../Types/mod.ts'; import { Handler, HttpMethod, Middleware } from '../Types/mod.ts';
import { IHandler } from './IHandler.ts';
import { IMiddleware } from './IMiddleware.ts';
import { IContext, IRouteMatcher } from './mod.ts'; import { IContext, IRouteMatcher } from './mod.ts';
/** /**
@@ -32,10 +30,10 @@ 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>;
} }

View File

@@ -0,0 +1,50 @@
import { Handler, Middleware } from '../Types/mod.ts';
import { IContext } from './IContext.ts';
import { IPipelineExecutorConfig } from './IPipelineExecutorConfig.ts';
/**
* Constructor type for a class implementing the IPipelineExecutor interface.
*
* This can be used for dependency injection, factory-based initialization,
* or dynamic instantiation of pipeline executors.
*
* @template TContext - The extended context type passed through the pipeline.
*/
export interface PipelineExecutorFactory<TContext extends IContext = IContext> {
/**
* Creates a new instance of a pipeline executor.
*
* @param config - Configuration used to control error handling,
* response decoration and lifecycle hooks.
*/
new (
config: IPipelineExecutorConfig<TContext>,
): IPipelineExecutor<TContext>;
}
/**
* Defines the contract for executing a middleware and handler pipeline.
*
* The pipeline is responsible for:
* - Executing middleware in order with `next()` chaining
* - Invoking the final handler
* - Applying optional lifecycle hooks
* - Producing and decorating a Response
*
* @template TContext - The context type flowing through the pipeline.
*/
export interface IPipelineExecutor<TContext extends IContext = IContext> {
/**
* Executes the middleware pipeline and returns the final Response.
*
* @param ctx - The context object representing the current HTTP request state.
* @param middleware - An ordered array of middleware functions to be executed.
* @param handler - The final route handler to be called after all middleware.
* @returns A Promise resolving to the final HTTP Response.
*/
run(
ctx: TContext,
middleware: Middleware<TContext>[],
handler: Handler<TContext>,
): Promise<Response>;
}

View File

@@ -0,0 +1,33 @@
import { ResponseDecorator } from '../Types/ResponseDecorator.ts';
import { IContext } from './IContext.ts';
import { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
import { IPipelineHooks } from './IPipelineHooks.ts';
/**
* Configuration object for the PipelineExecutor, defining how to handle
* errors, responses, and tracing hooks.
*
* This allows the execution logic to remain decoupled from kernel-level behavior
* while still supporting custom behavior injection.
*
* @template TContext - The context type propagated during pipeline execution.
*/
export interface IPipelineExecutorConfig<TContext extends IContext = IContext> {
/**
* Optional function used to transform or decorate the final Response object
* before it is returned to the client.
*/
decorateResponse?: ResponseDecorator<TContext>;
/**
* Optional map of error handlers, keyed by HTTP status codes (e.g., 404, 500).
* These handlers are invoked if an error occurs during middleware or handler execution.
*/
errorHandlers?: IHttpErrorHandlers<TContext>;
/**
* Optional hooks that allow tracing and lifecycle monitoring during pipeline execution.
* Each hook is called at a specific phase of the middleware/handler lifecycle.
*/
pipelineHooks?: IPipelineHooks<TContext>;
}

View File

@@ -0,0 +1,36 @@
import {
OnPipelineEnd,
OnPipelineStart,
OnStepEnd,
OnStepStart,
} from '../Types/mod.ts';
import { IContext } from './IContext.ts';
/**
* A set of optional hook functions that can be triggered during pipeline execution.
* These hooks allow tracing, performance measurement, and logging to be integrated
* without altering middleware or handler logic.
*
* @template TContext - The custom context type used within the application.
*/
export interface IPipelineHooks<TContext extends IContext = IContext> {
/**
* Triggered once before any middleware or handler is executed.
*/
onPipelineStart?: OnPipelineStart<TContext>;
/**
* Triggered immediately before each middleware or handler runs.
*/
onStepStart?: OnStepStart<TContext>;
/**
* Triggered immediately after each middleware or handler has finished executing.
*/
onStepEnd?: OnStepEnd<TContext>;
/**
* Triggered after the entire pipeline completes execution.
*/
onPipelineEnd?: OnPipelineEnd<TContext>;
}

View File

@@ -1,6 +1,5 @@
import { IHandler } from './IHandler.ts'; import { Handler, Middleware } from '../Types/mod.ts';
import { IInternalRoute } from './IInternalRoute.ts'; import { IInternalRoute } from './IInternalRoute.ts';
import { IMiddleware } from './IMiddleware.ts';
import { IRouteDefinition } from './IRouteDefinition.ts'; import { IRouteDefinition } from './IRouteDefinition.ts';
import { IContext } from './mod.ts'; import { IContext } from './mod.ts';
@@ -8,7 +7,7 @@ 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,14 +1,16 @@
// 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 {
export type { IMiddleware } from './IMiddleware.ts'; IPipelineExecutor,
PipelineExecutorFactory,
} from './IPipelineExecutor.ts';
export type { IPipelineExecutorConfig } from './IPipelineExecutorConfig.ts';
export type { IPipelineHooks } from './IPipelineHooks.ts';
export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts'; export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts';
export { export {
isDynamicRouteDefinition, isDynamicRouteDefinition,

View File

@@ -1,12 +1,6 @@
import { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts'; import { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts';
import { import { IContext, IRouteBuilder, IRouteDefinition } from './Interfaces/mod.ts';
IContext, import { Handler, Middleware, RegisterRoute } from './Types/mod.ts';
IHandler,
IMiddleware,
IRouteBuilder,
IRouteDefinition,
} from './Interfaces/mod.ts';
import { RegisterRoute } from './Types/mod.ts';
import { createRouteMatcher } from './Utils/createRouteMatcher.ts'; import { createRouteMatcher } from './Utils/createRouteMatcher.ts';
/** /**
@@ -27,7 +21,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 +36,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 +54,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({

View File

@@ -1,4 +1,4 @@
import { IContext } from './IContext.ts'; import { 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,
* Handles the request and generates a response. ) => Promise<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 './IContext.ts'; import { 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,
* Handles the request processing at this middleware stage. next: () => Promise<Response>,
) => Promise<Response>;
/**
* 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

@@ -0,0 +1,49 @@
import { IContext } from '../Interfaces/mod.ts';
/**
* A callback invoked when the middleware pipeline starts.
*
* @template TContext - The context type passed throughout the pipeline.
* @param ctx - The context object for the current request.
*/
export type OnPipelineStart<TContext extends IContext> = (
ctx: TContext,
) => void;
/**
* A callback invoked immediately before a middleware or handler is executed.
*
* @template TContext - The context type passed throughout the pipeline.
* @param name - Optional name of the current middleware or handler, if defined.
* @param ctx - The context object for the current request.
*/
export type OnStepStart<TContext extends IContext> = (
name: string | undefined,
ctx: TContext,
) => void;
/**
* A callback invoked immediately after a middleware or handler has completed.
*
* @template TContext - The context type passed throughout the pipeline.
* @param name - Optional name of the current middleware or handler, if defined.
* @param ctx - The context object for the current request.
* @param duration - Execution time in milliseconds.
*/
export type OnStepEnd<TContext extends IContext> = (
name: string | undefined,
ctx: TContext,
duration: number,
) => void;
/**
* A callback invoked after the entire pipeline has completed execution.
*
* @template TContext - The context type passed throughout the pipeline.
* @param ctx - The context object for the current request.
* @param totalDuration - Total execution time of the pipeline in milliseconds.
*/
export type OnPipelineEnd<TContext extends IContext> = (
ctx: TContext,
totalDuration: number,
) => void;

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,7 +36,15 @@ 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 {
OnPipelineEnd,
OnPipelineStart,
OnStepEnd,
OnStepStart,
} from './PipelineHooks.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';
export type { ResponseDecorator } from './ResponseDecorator.ts'; export type { ResponseDecorator } from './ResponseDecorator.ts';

View File

@@ -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

@@ -4,17 +4,14 @@ 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 { 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 { 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');
const dummyMiddleware: Middleware = async (_, next) => await next();
const dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' }; const dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' };
const dummyMatcher = () => ({ params: {} }); const dummyMatcher = () => ({ params: {} });
@@ -39,8 +36,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;