Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .githooks/_common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# .githooks/_common.sh
# Shared colour variables and print helpers.
# Source this file after setting HOOK_NAME, e.g.:
# HOOK_NAME=pre-commit
# . "$(dirname "$0")/_common.sh"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

_TAG="${HOOK_NAME:-hook}"
print_info() { printf "${BLUE}[%s]${NC} %s\n" "$_TAG" "$1"; }
print_success() { printf "${GREEN}[%s]${NC} %s\n" "$_TAG" "$1"; }
print_warning() { printf "${YELLOW}[%s]${NC} %s\n" "$_TAG" "$1"; }
print_error() { printf "${RED}[%s]${NC} %s\n" "$_TAG" "$1"; }
34 changes: 34 additions & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/sh
# .githooks/commit-msg
# Validates the commit message against the Conventional Commits spec.
# Tip: use `npx czg` to compose a properly-formatted message interactively.
#
# Install: run .githooks/setup.sh (or: git config core.hooksPath .githooks)
# Skip: git commit --no-verify

HOOK_NAME=commit-msg
# shellcheck source=.githooks/_common.sh
. "$(dirname "$0")/_common.sh"

COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")

# Skip merge commits, reverts, fixups, and squash commits
case "$COMMIT_MSG" in
Merge\ *|Revert\ *|fixup!\ *|squash!\ *) exit 0 ;;
esac

# Conventional Commits pattern: type(scope?)[!]: description
PATTERN='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: .{1,}'

if ! echo "$COMMIT_MSG" | grep -Eq "$PATTERN"; then
print_error "Commit message does not follow Conventional Commits format."
print_error " Got: $COMMIT_MSG"
print_info " Expected: type(scope): description"
print_info " Types: feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert"
print_info " Tip: use 'npx czg' to generate a valid message interactively."
exit 1
fi

print_success "Commit message format OK."
exit 0
87 changes: 87 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/bin/sh
# .githooks/pre-commit
# Pre-commit hook: runs Biome checks per-directory for staged files,
# and zod schema tests when schema files are staged.
#
# Install: run .githooks/setup.sh (or: git config core.hooksPath .githooks)
# Skip: git commit --no-verify

HOOK_NAME=pre-commit
# shellcheck source=.githooks/_common.sh
. "$(dirname "$0")/_common.sh"

# ── Collect all staged files ─────────────────────────────────────────────────
STAGED=$(git diff --cached --name-only --diff-filter=ACMR)

# ── 1. Biome check per directory ─────────────────────────────────────────────
print_info "Running Biome checks on staged directories…"

BIOME_FAILED=0

run_biome_check() {
DIR="$1"
print_info " npx biome check $DIR"
npx biome check "$DIR"
if [ $? -ne 0 ]; then
print_error "Biome check failed for $DIR. Run 'npm run check' to auto-fix, then re-stage."
BIOME_FAILED=1
else
print_success " $DIR ✓"
fi
}

echo "$STAGED" | grep -q '^common/iso/' && run_biome_check common/iso
echo "$STAGED" | grep -q '^common/node/' && run_biome_check common/node
echo "$STAGED" | grep -q '^common/vue/' && run_biome_check common/vue
echo "$STAGED" | grep -q '^common/web/' && run_biome_check common/web
echo "$STAGED" | grep -q '^apps/' && run_biome_check apps
echo "$STAGED" | grep -q '^webs/' && run_biome_check webs
echo "$STAGED" | grep -q '^scripts/' && run_biome_check scripts

if [ $BIOME_FAILED -ne 0 ]; then
exit 1
fi

# ── 2. Schema (Zod) validation tests ─────────────────────────────────────────
# Detect staged files under the known schema folders:
# common/schemas, apps/*/schemas
SCHEMA_FAILED=0

run_schema_tests() {
SCHEMA_DIR="$1"
if node -e "const p=require('./package.json'); process.exit(p.scripts&&p.scripts['test:schemas']?0:1)" 2>/dev/null; then
print_info "Running schema validation tests for $SCHEMA_DIR…"
npm run test:schemas -- "$SCHEMA_DIR"
if [ $? -ne 0 ]; then
print_error "Schema validation tests failed for $SCHEMA_DIR."
return 1
fi
print_success "Schema validation tests passed for $SCHEMA_DIR."
else
print_warning "No 'test:schemas' script found – skipping schema tests for $SCHEMA_DIR."
fi
return 0
}

# Check common/schemas
if echo "$STAGED" | grep -q '^common/schemas/'; then
run_schema_tests common/schemas || SCHEMA_FAILED=1
fi

# Check apps/*/schemas (one call per matching app directory)
APP_SCHEMA_DIRS=$(echo "$STAGED" | grep -oE '^apps/[^/]+/schemas/' | sed 's|/$||' | sort -u)
for SCHEMA_DIR in $APP_SCHEMA_DIRS; do
run_schema_tests "$SCHEMA_DIR" || SCHEMA_FAILED=1
done

if [ $SCHEMA_FAILED -ne 0 ]; then
exit 1
fi

# Report skip only when no schema folders were matched at all
echo "$STAGED" | grep -qE '^common/schemas/|^apps/[^/]+/schemas/' \
|| print_info "No schema files staged – skipping schema tests."

# ── Done ──────────────────────────────────────────────────────────────────────
print_success "All pre-commit checks passed."
exit 0
116 changes: 116 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/bin/sh
# .githooks/pre-push
# Pre-push hook: runs unit tests and schema validation tests before allowing a push.
#
# Install: run .githooks/setup.sh (or: git config core.hooksPath .githooks)
# Skip: git push --no-verify

HOOK_NAME=pre-push
# shellcheck source=.githooks/_common.sh
. "$(dirname "$0")/_common.sh"

# ── 1. Unit tests for impacted workspaces ─────────────────────────────────────
print_info "Detecting impacted workspaces…"

EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
CHANGED_FILES=""

while read -r local_ref local_sha remote_ref remote_sha; do
# Deleting a branch – nothing to test
[ "$local_sha" = "0000000000000000000000000000000000000000" ] && continue
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
FILES=$(git diff --name-only "$EMPTY_TREE" "$local_sha" 2>/dev/null)
else
FILES=$(git diff --name-only "$remote_sha" "$local_sha" 2>/dev/null)
fi
CHANGED_FILES=$(printf '%s\n%s' "$CHANGED_FILES" "$FILES")
done

CHANGED_FILES=$(printf '%s\n' "$CHANGED_FILES" | sort -u | grep -v '^$')

IMPACTED_WS=""
for WS_DIR in apps/* common/*; do
[ -d "$WS_DIR" ] || continue
[ -f "$WS_DIR/package.json" ] || continue
if printf '%s\n' "$CHANGED_FILES" | grep -q "^${WS_DIR}/"; then
IMPACTED_WS="$IMPACTED_WS $WS_DIR"
fi
done

if [ -z "$IMPACTED_WS" ]; then
print_warning "No workspace files changed – skipping unit tests."
else
TEST_EXIT=0
for WS in $IMPACTED_WS; do
if node -e "const p=require('./${WS}/package.json'); process.exit(p.scripts&&p.scripts['test']?0:1)" 2>/dev/null; then
print_info "Running tests for workspace: $WS"
npm run test --workspace="$WS"
if [ $? -ne 0 ]; then
print_error "Tests failed for workspace: $WS"
TEST_EXIT=1
else
print_success "Tests passed for workspace: $WS"
fi
else
print_warning "No 'test' script in $WS – skipping."
fi
done

if [ $TEST_EXIT -ne 0 ]; then
print_error "Fix failing tests before pushing. Use 'git push --no-verify' to bypass."
exit 1
fi
fi

print_success "Unit tests passed."

# ── 2. Schema validation tests ────────────────────────────────────────────────
# Run schema tests for all known schema directories that exist in the repo.
if node -e "const p=require('./package.json'); process.exit(p.scripts&&p.scripts['test:schemas']?0:1)" 2>/dev/null; then
print_info "Running schema validation tests…"
SCHEMA_DIRS=""
[ -d "common/schemas" ] && SCHEMA_DIRS="$SCHEMA_DIRS common/schemas"
for DIR in apps/*/schemas; do
[ -d "$DIR" ] && SCHEMA_DIRS="$SCHEMA_DIRS $DIR"
done

if [ -z "$SCHEMA_DIRS" ]; then
print_warning "No schema directories found – skipping schema tests."
else
SCHEMA_EXIT=0
for SCHEMA_DIR in $SCHEMA_DIRS; do
npm run test:schemas -- "$SCHEMA_DIR"
if [ $? -ne 0 ]; then
print_error "Schema validation tests failed for $SCHEMA_DIR. Fix schema errors before pushing."
SCHEMA_EXIT=1
else
print_success "Schema validation tests passed for $SCHEMA_DIR."
fi
done
if [ $SCHEMA_EXIT -ne 0 ]; then
exit 1
fi
fi
else
print_warning "No 'test:schemas' script found – skipping schema tests."
fi

# ── 3. Security audit ────────────────────────────────────────────────────────
print_info "Running security audit…"
npm audit --audit-level=moderate
AUDIT_EXIT=$?
if [ $AUDIT_EXIT -ne 0 ]; then
print_warning "Security vulnerabilities found."
printf "Continue push anyway? (y/n) "
read -r REPLY
case "$REPLY" in
[Yy]*) print_info "Continuing push despite vulnerabilities." ;;
*) print_error "Push aborted. Fix vulnerabilities or use 'git push --no-verify' to bypass."; exit 1 ;;
esac
else
print_success "No security vulnerabilities found."
fi

# ── Done ──────────────────────────────────────────────────────────────────────
print_success "All pre-push checks passed. Proceeding with push."
exit 0
60 changes: 60 additions & 0 deletions .githooks/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/bin/sh
# .githooks/setup.sh
# Installs the project's native Git hooks by pointing git's hooksPath at
# the .githooks/ directory. Run this once after cloning the repository.
#
# Usage:
# chmod +x .githooks/setup.sh
# ./.githooks/setup.sh

HOOK_NAME=setup
# shellcheck source=.githooks/_common.sh
. "$(dirname "$0")/_common.sh"

# ── Verify we are inside a git repository ─────────────────────────────────────
if ! git rev-parse --git-dir >/dev/null 2>&1; then
print_error "Not a git repository. Please run this script from the project root."
exit 1
fi

REPO_ROOT=$(git rev-parse --show-toplevel)
HOOKS_DIR="$REPO_ROOT/.githooks"

if [ ! -d "$HOOKS_DIR" ]; then
print_error ".githooks/ directory not found at $HOOKS_DIR"
exit 1
fi

# ── Make hook scripts executable ─────────────────────────────────────────────
print_info "Making hook scripts executable…"
chmod +x "$HOOKS_DIR/pre-commit" 2>/dev/null && print_success " pre-commit ✓"
chmod +x "$HOOKS_DIR/commit-msg" 2>/dev/null && print_success " commit-msg ✓"
chmod +x "$HOOKS_DIR/pre-push" 2>/dev/null && print_success " pre-push ✓"

# ── Configure git to use .githooks/ ──────────────────────────────────────────
print_info "Configuring git hooksPath → .githooks"
git config core.hooksPath .githooks

if [ $? -eq 0 ]; then
print_success "Git hooks installed successfully."
print_info " pre-commit: Biome check per directory, schema tests"
print_info " commit-msg: Conventional Commits format check (use 'npx czg' to compose)"
print_info " pre-push: unit tests + schema validation"
print_info ""
print_info "To skip a hook temporarily:"
print_info " git commit --no-verify"
print_info " git push --no-verify"
else
print_error "Failed to configure git hooksPath."
exit 1
fi

# ── Optional: verify czg is available ────────────────────────────────────────
if command -v czg >/dev/null 2>&1; then
print_success "czg found: standardized commit messages are enabled."
print_info " Use 'czg' (or 'npx czg') instead of 'git commit -m …' for guided messages."
print_info " Use 'czg --ai' to generate an AI-assisted commit message."
else
print_warning "czg not found globally. Install it with: npm install -g czg"
print_info " You can still use 'npx czg' or 'npx czg --ai' without a global install."
fi
65 changes: 65 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,70 @@ Hello and thank you for your interest in helping make express-template better. P
## IMPORTANT INFORMATION
* For general questions, please join [Our Discussion Board](https://github.com/es-labs/express-template/discussions).

## Git Hooks Setup

This project uses native Git hooks stored in `.githooks/`. After cloning, run the setup script once to activate them:

```bash
# Make the setup script executable and run it
chmod +x .githooks/setup.sh
./.githooks/setup.sh
```

Or, if you prefer, configure the hooks path manually:

```bash
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit .githooks/pre-push
```

Running `npm install` will also run `npm prepare`, which configures the hooks path automatically.

### pre-commit hook

Runs automatically on every `git commit`:

| Check | Details |
|-------|---------|
| **Biome format & lint** | Runs `npx biome check` on each affected directory (`common/iso`, `common/node`, `common/vue`, `common/web`, `apps`, `webs`, `scripts`). Run `npm run check` to auto-fix. |
| **Schema validation tests** | Runs `npm run test:schemas -- <folder>` for each affected schema directory (`common/schema`, `common/schemas`, `apps/*/schema`, `apps/*/schemas`). |

To skip the pre-commit hook temporarily:
```bash
git commit --no-verify
```

### Commit messages with czg

For standardized [Conventional Commits](https://www.conventionalcommits.org/) messages, use **czg** instead of `git commit -m "…"`:

```bash
# Interactive prompt (guided commit message)
npx czg

# AI-generated commit message (requires API key configured in czg)
npx czg --ai
```

Install globally for convenience:
```bash
npm install -g czg
```

### pre-push hook

Runs automatically on every `git push`:

| Check | Details |
|-------|---------|
| **Unit tests** | Runs `npm run test:workspace` (or `npm test`). |
| **Schema validation tests** | Runs `npm run test:schemas` if the script exists. |

To skip the pre-push hook temporarily:
```bash
git push --no-verify
```

## Reporting Issues
* The issue list of this repo is **exclusively** for Bug Reports and Feature Requests.
* Bug reproductions should be as **concise** as possible.
Expand All @@ -19,3 +83,4 @@ Hello and thank you for your interest in helping make express-template better. P
* Use a descriptive title no more than 64 characters long. This will be used as the commit message when your PR is merged.
* For changes and feature requests, please include an example of what you are trying to solve and an example of the markup. It is preferred that you create an issue first however, as that will allow the team to review your proposal before you start.
* Please reference the issue # that the PR resolves, something like `Fixes #1234` or `Resolves #6458` (See [closing issues using keywords](https://help.github.com/articles/closing-issues-using-keywords/))

Loading
Loading