๐Ÿ› ๏ธ Chapter 93: HTML Tooling, Linting & Quality Assurance

Pre-Commit Git Hooks with Husky & lint-staged

Automating local quality gates using Git hooks, repository-managed hooks with Husky, sub-second incremental file checks with `lint-staged`, and zero-defect commits.

LEARNING OBJECTIVES โŒต
  • Understand the Git hook architecture (.git/hooks/pre-commit) and why native hooks are not shared via version control.
  • Configure Husky to manage project-level Git hooks committed into the repository.
  • Implement lint-staged to execute linters only on staged files for sub-second developer feedback.
  • Build a multi-step pre-commit pipeline executing Prettier formatting, HTMLHint analysis, and Markuplint validation.
  • Handle Git staging states, stash management, and edge cases during pre-commit failures.
๐ŸŽฌ INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
๐ŸŒ
1. Input
Directives & Tags
โš™๏ธ
2. Parse
Tokenizer & AST
๐ŸŒณ
3. Layout
Box Model & Flow
๐ŸŽจ
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

๐Ÿ“– The Mental Model & Story (Intuitive Foundation)

Imagine an international airport with an automated security checkpoint. If security officers only inspected passengers after they had already boarded the aircraft and the plane was halfway across the Atlantic Ocean, removing an unauthorized passenger would require turning the entire airliner around at tremendous expense.

The same principle applies to software development. If code quality is only checked on the remote Continuous Integration (CI) server after a Pull Request is opened:

  • The developer has already switched contexts to another task.
  • Broken branches pollute the remote Git history.
  • CI compute minutes and cloud budgets are wasted on trivial formatting or unclosed tag errors.
Developer Workstation                          Remote Server
+-------------------------+                     +-------------------------+
|  git add index.html     |                     |                         |
|           |             |                     |                         |
|  git commit -m "..."    |                     |                         |
|           v             |                     |                         |
|   [ Husky Pre-Commit ]  |                     |                         |
|           |             |                     |                         |
|   [ lint-staged ]       |                     |                         |
|   -> Prettier (Format)  |                     |                         |
|   -> HTMLHint (Lint)    |                     |                         |
|   -> Markuplint (Spec)  |                     |                         |
|           |             |                     |                         |
|      (ALL PASS?)        |                     |                         |
|      /         \        |                     |                         |
|   [YES]       [NO]      |                     |                         |
|     |           |       |                     |                         |
| Commit Saved  Commit    |                     |  GitHub Actions CI      |
|     |         Aborted!  |  === git push ===>  |  (100% Clean PRs)       |
+-------------------------+                     +-------------------------+

By installing a Pre-Commit Quality Gate using Husky and lint-staged, invalid markup is intercepted and fixed locally in milliseconds before it ever leaves the developer's laptop.


Technical Deep Dive & Specifications

Why Native .git/hooks Fall Short

Git includes a native hook system inside .git/hooks/. However, Git specifically ignores the .git folder during commits. This means raw Git hooks cannot be shared with team members via git clone.

Husky solves this by:

  1. Creating a version-controlled .husky/ directory in the repository root.
  2. Configuring Git's core.hooksPath to point to .husky/ via an npm prepare script.
  3. Automatically installing hooks for all developers upon running npm install.

The Performance Problem & lint-staged

If a repository contains 5,000 HTML and JSX templates, running a full linter across the entire project on every single commit would take 20โ€“40 seconds. Developers would quickly bypass the hook using git commit --no-verify.

lint-staged solves this by executing linters only on files currently in the Git staging area (git diff --cached).

Working Tree (100 files changed)
       |
  [git add index.html]  --> Only 1 file staged!
       |
  [lint-staged]         --> Runs Prettier & HTMLHint ONLY on index.html (50ms execution!)
       |
  [Success]             --> Auto-adds formatted file back to index and completes commit.

Step-by-Step Production Setup

Step 1: Install Dependencies

npm install --save-dev husky lint-staged prettier htmlhint markuplint

Step 2: Initialize Husky

# Initializes .husky/ directory and adds "prepare": "husky" to package.json
npx husky init

Step 3: Configure the Pre-Commit Hook (.husky/pre-commit)

Edit the generated .husky/pre-commit file:

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

Step 4: Configure lint-staged (.lintstagedrc.json)

Create .lintstagedrc.json in your project root:

{
  "*.html": [
    "prettier --write",
    "htmlhint --config .htmlhintrc"
  ],
  "*.{jsx,tsx,vue,svelte}": [
    "prettier --write",
    "markuplint"
  ]
}

Execution Flow Sequence

  1. Developer runs git commit -m "Add new feature".
  2. Git triggers .husky/pre-commit.
  3. Husky invokes npx lint-staged.
  4. lint-staged collects staged files matching globs (e.g. src/index.html).
  5. Step 1: Prettier rewrites index.html with deterministic formatting.
  6. Step 2: lint-staged automatically re-stages the formatted file.
  7. Step 3: HTMLHint scans the staged file.
  8. If HTMLHint finds an unclosed tag, it exits with code 1. The commit is aborted immediately, preserving the staging area.
  9. If all checks pass with code 0, the Git commit completes normally.

๐Ÿ’ป Interactive Code Playground

Simulating the Pre-Commit Workflow in Node.js

Below is a complete, runnable demonstration script (test-git-hook.mjs) that illustrates how lint-staged intercepts broken HTML and blocks the commit:

Starter Code

Line-by-Line Code Breakdown

  • Lines 8โ€“19: Creates a temporary HTML file containing unclosed tags and missing alt attributes to simulate a developer's local changes.
  • Lines 26โ€“27: Step 1 executes Prettier to ensure formatting is normalized.
  • Lines 30โ€“31: Step 2 executes HTMLHint with mandatory tag-pair and alt-require rules.
  • Lines 34โ€“38: Catch block intercepts non-zero exit codes, mirroring Git's pre-commit abort behavior.

Expected Terminal Output


// test-git-hook.mjs
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

const testFile = path.resolve('./temp-staged.html');

console.log('--- Phase 1: Writing Broken HTML with Missing Alt & Unclosed Tag ---');
fs.writeFileSync(
  testFile,
  `<!DOCTYPE html>
<html lang="en">
<head><title>Staged Demo</title></head>
<body>
  <div>
    <!-- Error: Missing alt on image and unclosed p tag -->
    <img src="avatar.png">
    <p>Developer Profile
  </div>
</body>
</html>`
);

console.log('--- Phase 2: Simulating lint-staged Execution ---');

try {
  // 1. Run Prettier
  console.log('-> Running Prettier format check...');
  execSync(`npx prettier --write "${testFile}"`, { stdio: 'inherit' });

  // 2. Run HTMLHint with strict rules
  console.log('-> Running HTMLHint validation...');
  execSync(`npx htmlhint "${testFile}" --rules "tag-pair=true,alt-require=true"`, { stdio: 'inherit' });

  console.log('\nโœ… Staged check passed! Git commit approved.');
} catch (error) {
  console.error('\nโŒ PRE-COMMIT QUALITY GATE FAILED!');
  console.error('Commit aborted. Fix the lint violations above before committing.\n');
  process.exit(1);
} finally {
  // Cleanup demo file
  if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
}
--- Phase 1: Writing Broken HTML with Missing Alt & Unclosed Tag ---
--- Phase 2: Simulating lint-staged Execution ---
-> Running Prettier format check...
temp-staged.html 24ms (unchanged)
-> Running HTMLHint validation...

temp-staged.html:
  line 7, col 5: An alt attribute must be present on <img> elements. [alt-require]
  line 8, col 5: Tag must be paired, no start tag: [ </div> ] on line 9. [tag-pair]

Scanned 1 file, found 2 errors in 1 file.

โŒ PRE-COMMIT QUALITY GATE FAILED!
Commit aborted. Fix the lint violations above before committing.

๐Ÿ‹๏ธ Hands-On Exercise

๐ŸŽฏ The Challenge: Build a Resilient .lintstagedrc Multi-Stage Pipeline

Instructions:

  1. Author a production-grade .lintstagedrc.json configuration file that:
    • For all *.html files: Runs Prettier (prettier --write), HTMLHint (htmlhint --config .htmlhintrc), and Markuplint (markuplint).
    • For all React/Vue components (*.{jsx,tsx,vue}): Runs Prettier and Markuplint.
    • For all SVG vector graphics (*.svg): Runs SVGO or an SVG linter check.
  2. Ensure the commands run sequentially so files are formatted before being analyzed by the linters.

๐Ÿ Starter Code Sandbox (.lintstagedrc.json)

โš ๏ธ Common Pitfalls

  1. Overusing git commit --no-verify: Developers sometimes use --no-verify (or -n) to bypass pre-commit hooks during crunch periods. Doing so pushes broken markup that fails the CI build anyway, wasting team time.
  2. Running Slow Full-Repo Commands in Pre-Commit: Never run npm run test:e2e or full-project builds in a pre-commit hook. Pre-commit hooks must complete in under 2 seconds or developers will disable them.
  3. Modifying Non-Staged Lines in Working Tree: lint-staged automatically manages partially staged files via Git stashing. Ensure your version of lint-staged is updated to avoid stash conflict edge cases.

๐Ÿ’ก Pro Tips

  1. Add post-merge and post-checkout Hooks: Use Husky to automatically run npm install when switching branches or pulling upstream commits with new dependencies.
  2. Pair with Commitlint: Use Husky's commit-msg hook with @commitlint/cli to enforce Conventional Commits (e.g. feat(html): add accessible modal markup) alongside markup linting.

๐Ÿ“Œ Key Takeaways

  • Native Git hooks in .git/hooks/ are local-only and not tracked by version control.
  • Husky configures Git's core.hooksPath to .husky/, allowing Git hooks to be shared seamlessly across development teams.
  • lint-staged dramatically boosts developer productivity by executing formatters and linters exclusively on staged files.
  • Prettier should always execute before linters in lint-staged arrays so that formatted code is what gets analyzed.
  • Pre-commit hooks keep remote Continuous Integration (CI) pipelines green and eliminate formatting noise during code reviews.
  • --
โญ LEARN: HTML ๐ŸŒŸ โš”๏ธ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Why is running a full-project validation command like npm run validate:all inside a Git pre-commit hook considered an anti-pattern in large repositories?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

How does Husky ensure that all developers on a team have Git hooks installed automatically after running git clone and npm install?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

In a lint-staged configuration array for *.html, why should prettier --write be listed BEFORE htmlhint?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP