Compare commits
39 Commits
b0c6901d7d
...
feature/pi
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d1b0517a5 | |||
|
6ce73c14fa
|
|||
| 0aac2337a0 | |||
|
2ab6f1b8db
|
|||
| b69a51247d | |||
|
2ab74b9859
|
|||
|
d04dfcd63e
|
|||
| 32f3fe5f52 | |||
|
abd2d6e840
|
|||
|
927a9081d4
|
|||
|
8f94cc915c
|
|||
|
3f114bb68d
|
|||
|
0846dbb758
|
|||
| b624415320 | |||
|
a88b4d112f
|
|||
|
4f2b65049f
|
|||
| c9de4669c7 | |||
|
a1ce30627c
|
|||
|
5118a19aea
|
|||
| 0a09c8c324 | |||
| b9d25f23fc | |||
| 92f09e6e60 | |||
|
03115464e0
|
|||
| 11a2273240 | |||
|
1233a0b720
|
|||
| 195619ca99 | |||
|
9d5db4f414
|
|||
|
16c0053964
|
|||
|
04029f87a3
|
|||
| 594eccbc8b | |||
|
3afe6bcbc3
|
|||
|
bbf78cff17
|
|||
|
8235680904
|
|||
|
56633cd95b
|
|||
|
5c03cdfb03
|
|||
|
661f83d1fd
|
|||
|
7b6eb2b574
|
|||
|
f0838567b4
|
|||
|
b009b5763d
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
CHANGELOG.md merge=ours
|
||||
198
.gitea/HOWTO_RELEASE.md
Normal file
198
.gitea/HOWTO_RELEASE.md
Normal 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.
|
||||
21
.gitea/scripts/get-release-id.sh
Executable file
21
.gitea/scripts/get-release-id.sh
Executable 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
40
.gitea/scripts/upload-asset.sh
Executable 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"
|
||||
222
.gitea/workflows/release.yml
Normal file
222
.gitea/workflows/release.yml
Normal 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
22
.gitea/workflows/test.yml
Normal 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
|
||||
47
.gitea/workflows/upload-assets.yml.example
Normal file
47
.gitea/workflows/upload-assets.yml.example
Normal 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
1
.gitignore
vendored
@@ -6,3 +6,4 @@ cache/
|
||||
out.py
|
||||
output.txt
|
||||
git_log_diff.txt
|
||||
.gitea/COMMIT_GPT.md
|
||||
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -18,4 +18,13 @@
|
||||
],
|
||||
"exportall.config.barrelName": "mod.ts",
|
||||
"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
78
CHANGELOG.md
Normal 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
18
LICENSE
Normal 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
135
README.md
Normal 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)
|
||||
104
cliff.toml
Normal file
104
cliff.toml
Normal 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"
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "@0xmax42/http-kernel",
|
||||
"description": "A simple HTTP kernel for Deno",
|
||||
"tasks": {
|
||||
// "start": "deno run --allow-net --allow-env --unstable-kv --allow-read --allow-write --env-file src/main.ts -- --verbose",
|
||||
// "watch": "deno run --watch --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"
|
||||
"test": "deno test --allow-net --allow-env --unstable-kv --allow-read --allow-write --coverage **/__tests__/*.test.ts",
|
||||
"test:watch": "deno test --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__tests__/*.test.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
@@ -24,6 +25,6 @@
|
||||
"src/",
|
||||
"main.ts"
|
||||
]
|
||||
},
|
||||
}
|
||||
//"importMap": "./import_map.json"
|
||||
}
|
||||
72
docs/pipeline_executor_plan.md
Normal file
72
docs/pipeline_executor_plan.md
Normal 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`
|
||||
@@ -1,20 +1,20 @@
|
||||
import {
|
||||
IContext,
|
||||
IHandler,
|
||||
IHttpKernel,
|
||||
IHttpKernelConfig,
|
||||
IInternalRoute,
|
||||
IMiddleware,
|
||||
IRouteBuilder,
|
||||
IRouteDefinition,
|
||||
isHandler,
|
||||
isMiddleware,
|
||||
} from './Interfaces/mod.ts';
|
||||
import {
|
||||
DeepPartial,
|
||||
Handler,
|
||||
HTTP_404_NOT_FOUND,
|
||||
HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
HttpStatusTextMap,
|
||||
isHandler,
|
||||
isMiddleware,
|
||||
Middleware,
|
||||
} from './Types/mod.ts';
|
||||
import { RouteBuilder } from './RouteBuilder.ts';
|
||||
import { createEmptyContext, normalizeError } from './Utils/mod.ts';
|
||||
@@ -151,8 +151,8 @@ export class HttpKernel<TContext extends IContext = IContext>
|
||||
*/
|
||||
private async executePipeline(
|
||||
ctx: TContext,
|
||||
middleware: IMiddleware<TContext>[],
|
||||
handler: IHandler<TContext>,
|
||||
middleware: Middleware<TContext>[],
|
||||
handler: Handler<TContext>,
|
||||
): Promise<Response> {
|
||||
const handleInternalError = (ctx: TContext, err?: unknown) =>
|
||||
this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { HttpMethod } from '../Types/mod.ts';
|
||||
import { IHandler } from './IHandler.ts';
|
||||
import { IMiddleware } from './IMiddleware.ts';
|
||||
import { Handler, HttpMethod, Middleware } from '../Types/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.
|
||||
*/
|
||||
middlewares: IMiddleware<TContext>[];
|
||||
middlewares: Middleware<TContext>[];
|
||||
|
||||
/**
|
||||
* The final handler that generates the HTTP response after all middleware has run.
|
||||
*/
|
||||
handler: IHandler<TContext>;
|
||||
handler: Handler<TContext>;
|
||||
}
|
||||
|
||||
50
src/Interfaces/IPipelineExecutor.ts
Normal file
50
src/Interfaces/IPipelineExecutor.ts
Normal 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>;
|
||||
}
|
||||
33
src/Interfaces/IPipelineExecutorConfig.ts
Normal file
33
src/Interfaces/IPipelineExecutorConfig.ts
Normal 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>;
|
||||
}
|
||||
36
src/Interfaces/IPipelineHooks.ts
Normal file
36
src/Interfaces/IPipelineHooks.ts
Normal 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>;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IHandler } from './IHandler.ts';
|
||||
import { Handler, Middleware } from '../Types/mod.ts';
|
||||
import { IInternalRoute } from './IInternalRoute.ts';
|
||||
import { IMiddleware } from './IMiddleware.ts';
|
||||
import { IRouteDefinition } from './IRouteDefinition.ts';
|
||||
import { IContext } from './mod.ts';
|
||||
|
||||
@@ -8,7 +7,7 @@ export interface IRouteBuilderFactory<TContext extends IContext = IContext> {
|
||||
new (
|
||||
registerRoute: (route: IInternalRoute<TContext>) => void,
|
||||
def: IRouteDefinition,
|
||||
mws?: IMiddleware<TContext>[],
|
||||
mws?: Middleware<TContext>[],
|
||||
): IRouteBuilder<TContext>;
|
||||
}
|
||||
|
||||
@@ -25,7 +24,7 @@ export interface IRouteBuilder<TContext extends IContext = IContext> {
|
||||
* @returns The route builder for further chaining.
|
||||
*/
|
||||
middleware(
|
||||
mw: IMiddleware<TContext>,
|
||||
mw: Middleware<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.
|
||||
*/
|
||||
handle(
|
||||
handler: IHandler<TContext>,
|
||||
handler: Handler<TContext>,
|
||||
): void;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// deno-coverage-ignore-file
|
||||
|
||||
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 { IHttpKernel } from './IHttpKernel.ts';
|
||||
export type { IHttpKernelConfig } from './IHttpKernelConfig.ts';
|
||||
export type { IInternalRoute } from './IInternalRoute.ts';
|
||||
export { isMiddleware } from './IMiddleware.ts';
|
||||
export type { IMiddleware } from './IMiddleware.ts';
|
||||
export type {
|
||||
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 {
|
||||
isDynamicRouteDefinition,
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts';
|
||||
import {
|
||||
IContext,
|
||||
IHandler,
|
||||
IMiddleware,
|
||||
IRouteBuilder,
|
||||
IRouteDefinition,
|
||||
} from './Interfaces/mod.ts';
|
||||
import { RegisterRoute } from './Types/mod.ts';
|
||||
import { IContext, IRouteBuilder, IRouteDefinition } from './Interfaces/mod.ts';
|
||||
import { Handler, Middleware, RegisterRoute } from './Types/mod.ts';
|
||||
import { createRouteMatcher } from './Utils/createRouteMatcher.ts';
|
||||
|
||||
/**
|
||||
@@ -27,7 +21,7 @@ export class RouteBuilder<TContext extends IContext = IContext>
|
||||
constructor(
|
||||
private readonly registerRoute: RegisterRoute<TContext>,
|
||||
private readonly def: IRouteDefinition,
|
||||
private readonly mws: IMiddleware<TContext>[] = [],
|
||||
private readonly mws: Middleware<TContext>[] = [],
|
||||
private readonly matcherFactory: IRouteMatcherFactory =
|
||||
createRouteMatcher,
|
||||
) {}
|
||||
@@ -42,7 +36,7 @@ export class RouteBuilder<TContext extends IContext = IContext>
|
||||
* @returns A new `RouteBuilder` instance for continued chaining.
|
||||
*/
|
||||
middleware(
|
||||
mw: IMiddleware<TContext>,
|
||||
mw: Middleware<TContext>,
|
||||
): IRouteBuilder<TContext> {
|
||||
return new RouteBuilder<TContext>(
|
||||
this.registerRoute,
|
||||
@@ -60,7 +54,7 @@ export class RouteBuilder<TContext extends IContext = IContext>
|
||||
* @param handler - The final request handler for this route.
|
||||
*/
|
||||
handle(
|
||||
handler: IHandler<TContext>,
|
||||
handler: Handler<TContext>,
|
||||
): void {
|
||||
const matcher = this.matcherFactory(this.def);
|
||||
this.registerRoute({
|
||||
|
||||
@@ -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.
|
||||
@@ -11,16 +11,23 @@ import { IContext } from './IContext.ts';
|
||||
*
|
||||
* @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,
|
||||
* and mutable state populated during the middleware phase.
|
||||
* @returns A `Promise` resolving to an HTTP `Response` to be sent to the client.
|
||||
* This is useful for debugging, logging, or when you need to reference
|
||||
* the handler by name in your application.
|
||||
*
|
||||
* @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.
|
||||
@@ -42,7 +49,7 @@ export interface IHandler<TContext extends IContext = IContext> {
|
||||
*/
|
||||
export function isHandler<TContext extends IContext = IContext>(
|
||||
value: unknown,
|
||||
): value is IHandler<TContext> {
|
||||
): value is Handler<TContext> {
|
||||
return (
|
||||
typeof value === 'function' &&
|
||||
value.length === 1 // ctx
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IContext } from './IContext.ts';
|
||||
import { IContext } from '../Interfaces/IContext.ts';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
* @param next - A continuation function that executes the next middleware or handler in the pipeline.
|
||||
* @returns A `Promise` resolving to an HTTP `Response`, either from this middleware or downstream.
|
||||
* This is useful for debugging, logging, or when you need to reference
|
||||
* the middleware by name in your application.
|
||||
*
|
||||
* @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.
|
||||
@@ -35,7 +43,7 @@ export interface IMiddleware<TContext extends IContext = IContext> {
|
||||
*/
|
||||
export function isMiddleware<TContext extends IContext = IContext>(
|
||||
value: unknown,
|
||||
): value is IMiddleware<TContext> {
|
||||
): value is Middleware<TContext> {
|
||||
return (
|
||||
typeof value === 'function' &&
|
||||
value.length === 2 // ctx, next
|
||||
49
src/Types/PipelineHooks.ts
Normal file
49
src/Types/PipelineHooks.ts
Normal 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;
|
||||
@@ -1,6 +1,8 @@
|
||||
// deno-coverage-ignore-file
|
||||
|
||||
export type { DeepPartial } from './DeepPartial.ts';
|
||||
export { isHandler } from './Handler.ts';
|
||||
export type { Handler } from './Handler.ts';
|
||||
export type { HttpErrorHandler } from './HttpErrorHandler.ts';
|
||||
export { isHttpMethod, validHttpMethods } from './HttpMethod.ts';
|
||||
export type { HttpMethod } from './HttpMethod.ts';
|
||||
@@ -34,7 +36,15 @@ export {
|
||||
validHttpStatusCodes,
|
||||
} 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 {
|
||||
OnPipelineEnd,
|
||||
OnPipelineStart,
|
||||
OnStepEnd,
|
||||
OnStepStart,
|
||||
} from './PipelineHooks.ts';
|
||||
export type { Query } from './Query.ts';
|
||||
export type { RegisterRoute } from './RegisterRoute.ts';
|
||||
export type { ResponseDecorator } from './ResponseDecorator.ts';
|
||||
|
||||
@@ -35,7 +35,9 @@ export function createRouteMatcher(
|
||||
// 3b. Extract route params
|
||||
const params: Params = {};
|
||||
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
|
||||
|
||||
@@ -4,17 +4,14 @@ import {
|
||||
assertNotEquals,
|
||||
assertThrows,
|
||||
} from 'https://deno.land/std@0.204.0/assert/mod.ts';
|
||||
import {
|
||||
IHandler,
|
||||
IInternalRoute,
|
||||
IMiddleware,
|
||||
IRouteDefinition,
|
||||
} from '../Interfaces/mod.ts';
|
||||
import { IInternalRoute, IRouteDefinition } from '../Interfaces/mod.ts';
|
||||
import { RouteBuilder } from '../mod.ts';
|
||||
import { Handler, Middleware } from '../Types/mod.ts';
|
||||
|
||||
// Dummy objects
|
||||
const dummyHandler: IHandler = async () => new Response('ok');
|
||||
const dummyMiddleware: IMiddleware = async (_, next) => await next();
|
||||
// deno-lint-ignore require-await
|
||||
const dummyHandler: Handler = async () => new Response('ok');
|
||||
const dummyMiddleware: Middleware = async (_, next) => await next();
|
||||
const dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' };
|
||||
const dummyMatcher = () => ({ params: {} });
|
||||
|
||||
@@ -39,8 +36,8 @@ Deno.test('middleware: middleware is chained immutably', () => {
|
||||
});
|
||||
|
||||
Deno.test('middleware: preserves order of middleware', () => {
|
||||
const mw1: IMiddleware = async (_, next) => await next();
|
||||
const mw2: IMiddleware = async (_, next) => await next();
|
||||
const mw1: Middleware = async (_, next) => await next();
|
||||
const mw2: Middleware = async (_, next) => await next();
|
||||
|
||||
let result: IInternalRoute | null = null as IInternalRoute | null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user