Chapter 11 — Front-End Fundamentals

Linting and Code Compliance

A linter is a tool that reads your code and flags problems: syntax errors, style inconsistencies, patterns that are likely bugs, rules you’ve decided the project should follow. It is automated code review that runs in milliseconds.

There are three linters to set up. ESLint covers JavaScript. Stylelint covers CSS and Sass. PHP_CodeSniffer with the WordPress Coding Standards ruleset covers PHP. You won’t write any PHP until Chapter 12, but setting up PHPCS now means it’s already wired into the pipeline when you need it.

The goal is to write compliant code from the first line, not to go back and fix it later. Refactoring non-compliant code at scale is not fun. Ask anyone who’s inherited a legacy codebase.


ESLint

Install

bash~/tutorials/
npm install --save-dev eslint @eslint/js

Configure

ESLint 9 uses a flat config file: eslint.config.js in the project root. Create it:

bash~/tutorials/
touch eslint.config.js

Add this configuration:

js~/tutorials/
const js = require('@eslint/js');

module.exports = [
  js.configs.recommended,
  {
    rules: {
      'no-console': 'warn',
      'no-unused-vars': 'error',
      'no-undef': 'error',
      'eqeqeq': ['error', 'always'],
      'curly': 'error',
    },
    languageOptions: {
      globals: {
        document: 'readonly',
        window: 'readonly',
        console: 'readonly',
      },
    },
  },
];

js.configs.recommended enables ESLint’s built-in recommended rules. The entries in rules extend or override them. 'warn' flags a problem without failing the lint check. 'error' fails it. eqeqeq enforces === instead of ==, which avoids JavaScript’s type coercion surprises. curly requires braces on all control flow statements, even single-line ones.

The globals block tells ESLint that document, window, and console are globals provided by the browser environment, so it won’t flag them as undefined variables.

Test it

Add a deliberate error to src/js/toggle.js: an unused variable.

js~/tutorials/
const unused = 'this should fail';

export default function setupToggle(buttonSelector, targetSelector) {
  // ...
}

Run ESLint manually:

bash~/tutorials/
npx eslint src/js/

It should flag unused as an error. Remove it, run again, and get a clean result.

Add a Gulp task

Install the current maintained Gulp ESLint plugin:

bash~/tutorials/
npm install --save-dev gulp-eslint-new

Add the task to gulpfile.js:

js~/tutorials/
const eslint = require('gulp-eslint-new');

function lintJs() {
  return src('src/js/**/*.js')
    .pipe(eslint())
    .pipe(eslint.format())
    .pipe(eslint.failAfterError());
}

exports.lintJs = lintJs;

failAfterError() stops the Gulp task if ESLint finds errors, but lets it finish reporting all problems first. Wire it into your build:

js~/tutorials/
exports.build = series(lintJs, parallel(html, css, js));

This runs the lint check before building. A failing lint check stops the build.


Stylelint

Install

bash~/tutorials/
npm install --save-dev stylelint stylelint-config-standard-scss

Configure

Create .stylelintrc.json in the project root:

json~/tutorials/
{
  "extends": ["stylelint-config-standard-scss"],
  "rules": {
    "color-named": "never",
    "declaration-no-important": true,
    "selector-max-id": 0,
    "max-nesting-depth": 3
  }
}

color-named bans color names like red or blue in favor of explicit hex or variable values. declaration-no-important bans !important, which is almost always a sign of a specificity problem that should be solved properly. selector-max-id bans ID selectors in your Sass entirely, enforcing the class-based approach from Chapter 5. max-nesting-depth enforces the three-level nesting limit covered in Chapter 9.

Test it

Add a named color to _base.scss:

scss~/tutorials/
h1 {
  color: red; // this should fail
}

Run Stylelint manually:

bash~/tutorials/
npx stylelint "src/scss/**/*.scss"

It flags the named color. Remove it, run again, clean result.

Add a Gulp task

The most reliable approach is to shell out directly using Node’s child_process:

js~/tutorials/
const { exec } = require('child_process');

function lintCss(done) {
  exec('npx stylelint "src/scss/**/*.scss"', (error, stdout, stderr) => {
    if (stdout) process.stdout.write(stdout);
    if (stderr) process.stderr.write(stderr);
    done(error);
  });
}

exports.lintCss = lintCss;

Wire it into the build alongside lintJs:

js~/tutorials/
exports.build = series(parallel(lintJs, lintCss), parallel(html, css, js));

PHP_CodeSniffer and WordPress Coding Standards

PHPCS is a PHP tool, not a Node tool. It requires PHP and Composer, PHP’s package manager. You don’t have any PHP yet, but setting this up now means the linter is ready when the WordPress chapters start.

Install PHP

macOS:

bash~/tutorials/
brew install php

Verify:

bash~/tutorials/
php --version

Ubuntu:

bash~/tutorials/
sudo apt update
sudo apt install php php-cli php-xml

Install Composer

macOS:

bash~/tutorials/
brew install composer

Ubuntu:

bash~/tutorials/
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
sudo mv composer.phar /usr/local/bin/composer

Verify:

bash~/tutorials/
composer --version

Install PHPCS and WordPress Coding Standards

bash~/tutorials/
composer global require squizlabs/php_codesniffer
composer global require wp-coding-standards/wpcs:"*"

Register the WordPress ruleset with PHPCS:

bash~/tutorials/
phpcs --config-set installed_paths $(composer global config home)/vendor/wp-coding-standards/wpcs

Verify the WordPress ruleset is available:

bash~/tutorials/
phpcs -i

You should see WordPress, WordPress-Core, WordPress-Docs, and WordPress-Extra in the list.

Make sure your global Composer bin directory is on your PATH. Add this to your shell configuration file (.zshrc on macOS, .bashrc on Ubuntu, or your Fish config if you use Fish):

bash~/tutorials/
export PATH="$PATH:$HOME/.composer/vendor/bin"

Restart your terminal or source the config file, then verify:

bash~/tutorials/
phpcs --version

Create a PHPCS configuration file

Create phpcs.xml in the project root:

xml~/tutorials/
<?xml version="1.0"?>
<ruleset name="WordPress Theme">
  <description>WordPress Coding Standards for this theme.</description>

  <rule ref="WordPress" />

  <arg name="extensions" value="php" />
  <arg name="colors" />
  <arg value="sp" />

  <file>./src/php</file>

  <exclude-pattern>*/node_modules/*</exclude-pattern>
  <exclude-pattern>*/vendor/*</exclude-pattern>
</ruleset>

Create the src/php/ directory:

bash~/tutorials/
mkdir src/php

Add a Gulp task

js~/tutorials/
function lintPhp(done) {
  exec('phpcs', (error, stdout, stderr) => {
    if (stdout) process.stdout.write(stdout);
    if (stderr) process.stderr.write(stderr);
    // PHPCS exits with code 1 for warnings, 2 for errors
    // Only fail the task on errors, not warnings
    done(error && error.code === 2 ? error : null);
  });
}

exports.lintPhp = lintPhp;

Add it to the build:

js~/tutorials/
exports.build = series(parallel(lintJs, lintCss, lintPhp), parallel(html, css, js));

With an empty src/php/, PHPCS finds nothing to check and exits cleanly.


VS Code extensions

Lint checks in the terminal are good. Lint checks as you type are better. Install these extensions so VS Code underlines problems before you even save:

  • ESLint: installed in Chapter 3, now picks up eslint.config.js automatically
  • Stylelint: search and install from the Extensions panel
  • PHP Sniffer & Beautifier: runs PHPCS against PHP files in the editor

Add Stylelint validation for .scss files to your VS Code settings.json:

json~/tutorials/
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "stylelint.validate": ["css", "scss"]
}

The full updated Gulpfile

Here’s gulpfile.js in its complete state after this chapter:

js~/tutorials/
const { src, dest, watch, series, parallel } = require('gulp');
const browserSync = require('browser-sync').create();
const sass = require('gulp-sass')(require('sass'));
const rollup = require('rollup');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const eslint = require('gulp-eslint-new');
const { exec } = require('child_process');

// HTML
function html() {
  return src('src/**/*.html')
    .pipe(dest('dist/'));
}

// CSS: compile Sass
function css() {
  return src('src/scss/main.scss')
    .pipe(sass({ outputStyle: 'expanded' }).on('error', sass.logError))
    .pipe(dest('dist/css/'));
}

// JS: bundle with Rollup
async function js() {
  const bundle = await rollup.rollup({
    input: 'src/js/main.js',
    plugins: [nodeResolve()],
  });

  await bundle.write({
    file: 'dist/js/main.js',
    format: 'iife',
    name: 'App',
    sourcemap: true,
  });
}

// Lint: JavaScript
function lintJs() {
  return src('src/js/**/*.js')
    .pipe(eslint())
    .pipe(eslint.format())
    .pipe(eslint.failAfterError());
}

// Lint: CSS/Sass
function lintCss(done) {
  exec('npx stylelint "src/scss/**/*.scss"', (error, stdout, stderr) => {
    if (stdout) process.stdout.write(stdout);
    if (stderr) process.stderr.write(stderr);
    done(error);
  });
}

// Lint: PHP
function lintPhp(done) {
  exec('phpcs', (error, stdout, stderr) => {
    if (stdout) process.stdout.write(stdout);
    if (stderr) process.stderr.write(stderr);
    done(error && error.code === 2 ? error : null);
  });
}

// Dev server and file watching
function serve() {
  browserSync.init({
    server: {
      baseDir: './dist',
    },
  });

  watch('src/**/*.html', series(html, reload));
  watch('src/scss/**/*.scss', series(css, reload));
  watch('src/js/**/*.js', series(js, reload));
}

function reload(done) {
  browserSync.reload();
  done();
}

// Exports
exports.lintJs = lintJs;
exports.lintCss = lintCss;
exports.lintPhp = lintPhp;
exports.lint = parallel(lintJs, lintCss, lintPhp);
exports.build = series(parallel(lintJs, lintCss, lintPhp), parallel(html, css, js));
exports.default = series(parallel(html, css, js), serve);

Note that the default (development) task doesn’t run linting before serving. Running lint checks on every file save during development is slow and disruptive. VS Code handles that feedback loop in real time. The build task is where linting gates the output. Before you ship anything, every check has to pass.

Update package.json to expose a lint script:

json~/tutorials/
"scripts": {
  "start": "gulp",
  "build": "gulp build",
  "lint": "gulp lint"
}

Commit your work

bash~/tutorials/
git add .
git commit -m "Add ESLint, Stylelint, and PHPCS with WordPress Coding Standards"

What you now know

You have automated code quality checks for JavaScript, Sass, and PHP, wired into both your editor and your build pipeline. Code that doesn’t meet the WordPress Coding Standards won’t make it into a build. In Chapter 12 you’ll use all of this: setting up a local WordPress environment and writing your first PHP.

Reference