Skip to content

About

Shared tooling that lets WordPress projects be checked, scoped and released the same way.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

wordpress-configs

Shared tooling that lets WordPress projects be checked, scoped and released the same way.

Requirements

  • PHP at the floor composer.json's require.php names, and Composer 2.
  • For the Node baselines only, the Node and npm versions package.json's engines names, and the tools its peerDependencies list.

The shared PHPCS profiles check code against the PHP and WordPress floors that php/quality-assurance/phpcs.base.dist.xml sets in testVersion and minimum_wp_version, and the PHPStan profile against the WordPress floor that php/quality-assurance/phpstan.dist.neon sets in WPCompat.requiresAtLeast; a project with lower floors lowers them as the scripts contract shows for PHPCS and its PHPStan section for PHPStan.

Installation

Add the VCS repository and require the package as a development dependency:

{
	"repositories": [
		{ "type": "vcs", "url": "https://github.com/ahegyes/wordpress-configs.git" }
	],
	"require-dev": {
		"ahegyes/wordpress-configs": "dev-trunk"
	}
}

The root composer.json also declares minimum-stability: dev with prefer-stable: true, because this package requires the development branch of roave/security-advisories, which fails a development install on any known advisory in the dependency graph, and an alpha release of phpcompatibility/phpcompatibility-wp. Composer honors stability flags only in the root package. The root package also allows the two Composer plugins that register the PHPCS standards and the PHPStan extensions; a non-interactive install fails without them:

{
	"minimum-stability": "dev",
	"prefer-stable": true,
	"config": {
		"allow-plugins": {
			"dealerdirect/phpcodesniffer-composer-installer": true,
			"phpstan/extension-installer": true
		}
	}
}

The Node baselines resolve through node_modules, so a project that uses them also installs the package with npm, as a Git dependency pinned to a commit SHA:

{
	"devDependencies": {
		"@ahegyes/wordpress-configs": "git+https://github.com/ahegyes/wordpress-configs.git#<commit-sha>"
	}
}

PHPCS

Two profiles share php/quality-assurance/phpcs.base.dist.xml, which runs the full WordPress standard and PHPCompatibilityWP:

Profile File Lints
Production php/quality-assurance/phpcs.dist.xml Everything except tests/
Tests php/quality-assurance/phpcs.tests.dist.xml tests/, without the docblock and WordPress-runtime sniffs

The production profile also requires @return on every named function and method docblock, @return void included, which WordPress-Docs leaves optional; constructors, destructors and docblocks that open with {@inheritDoc} need none.

Both profiles skip bin/, vendor/, vendor-prefixed/, node_modules/ and generated files. The patterns match case-sensitively, so a src/Vendor/ source directory is still linted. An index.php is linted like any other file, except that its file comment may be a // line, so a silence-is-golden placeholder stays clean.

Create a phpcs.dist.xml that extends the production profile and names the project's text domain and prefixes:

<?xml version="1.0"?>
<ruleset name="Project">
	<rule ref="./vendor/ahegyes/wordpress-configs/php/quality-assurance/phpcs.dist.xml"/>

	<rule ref="WordPress.WP.I18n">
		<properties>
			<property name="text_domain" type="array">
				<element value="your-text-domain"/>
			</property>
		</properties>
	</rule>

	<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
		<properties>
			<property name="prefixes" type="array">
				<element value="yourprefix_"/>
				<element value="YourVendor\YourPlugin"/>
			</property>
		</properties>
	</rule>
</ruleset>

Create a phpcs.tests.dist.xml beside it that extends the tests profile, php/quality-assurance/phpcs.tests.dist.xml, the same way. Keep the leading ./ in each ref. The scripts contract gives the two lint scripts and the two ways to lower the PHP and WordPress floors the profiles set.

PHPStan

php/quality-assurance/phpstan.dist.neon is the shared profile:

File Holds
phpstan.base.dist.neon The rules that need no WordPress: the level, the missing #[\Override] check, the strict-rules settings and the nested vendor exclusions
phpstan.dist.neon The base, plus the minimum WordPress version that johnbillion/wp-compat checks calls against and the types of documented globals
phpstan.dist.neon.php Opt-in discovery of a plugin's conventional paths

A global statement takes the types that the @global tags of its function's or method's docblock declare, as WordPress's documentation standard writes them, such as @global \wpdb $wpdb The database connection.; a global no tag declares stays mixed, and a statement with its own docblock keeps it. Code under a nested vendor/ or vendor-prefixed/ directory is scanned but not analyzed, so the code that uses it still resolves. This package requires php-stubs/wordpress-stubs; a project that scans or declares php-stubs/woocommerce-stubs, such as a WooCommerce extension, requires that itself.

Create a phpstan.dist.neon:

includes:
	- vendor/ahegyes/wordpress-configs/php/quality-assurance/phpstan.dist.neon

parameters:
	paths:
		- your-plugin.php
		- src

To add a plugin's conventional paths without listing them, include phpstan.dist.neon.php as well. It adds functions-bootstrap.php, functions.php, uninstall.php, src/, includes/, models/, blocks/ and templates/ when they exist in the directory PHPStan runs from, and scans vendor-prefixed/ there; the entry file still goes under paths.

To analyze code with no WordPress loaded, include phpstan.base.dist.neon alone and list szepeviktor/phpstan-wordpress, johnbillion/wp-compat and swissspidy/phpstan-no-private under extra.phpstan/extension-installer.ignore in the root composer.json, so a WordPress function is an unknown symbol. A WordPress config in the same repository then includes those extensions itself, because PHPStan rejects the minimum WordPress version when wp-compat is not loaded:

includes:
	- vendor/szepeviktor/phpstan-wordpress/extension.neon
	- vendor/johnbillion/wp-compat/extension.neon
	- vendor/swissspidy/phpstan-no-private/rules.neon
	- vendor/ahegyes/wordpress-configs/php/quality-assurance/phpstan.dist.neon

The package installs phpstan-deprecation-rules, phpstan-strict-rules, szepeviktor/phpstan-wordpress (WordPress signatures and types), johnbillion/wp-compat (WordPress version compatibility) and swissspidy/phpstan-no-private (private WordPress APIs).

Dependency scoping

php/php-scoper/scoper-base.inc.php and php/composer/ScopePhpDependencies.php prefix the packages a WordPress plugin bundles with the plugin's own namespace, so plugins that bundle different versions of the same library run side by side.

Require php-scoper in the plugin itself, because Composer does not install this package's development requirements. This package conflicts with any php-scoper outside 0.18, whose behavior the pipeline relies on:

{
	"require-dev": {
		"humbug/php-scoper": "^0.18"
	}
}

Declare the scoping prefix, load the generated autoloader, and scope after every development install:

{
	"autoload": {
		"files": [
			"vendor-prefixed/scoper-autoload.php"
		]
	},
	"scripts": {
		"post-autoload-dump": [
			"DeepWebSolutions\\Config\\Composer\\ScopePhpDependencies::postAutoloadDump"
		]
	},
	"extra": {
		"scoping-prefix": "YourVendor\\YourPlugin\\Scoped"
	}
}

Create scoper.inc.php at the project root and name the packages to scope:

<?php declare( strict_types=1 );

return ( require __DIR__ . '/vendor/ahegyes/wordpress-configs/php/php-scoper/scoper-base.inc.php' )( __DIR__, 'dompdf/dompdf', 'php-di/php-di' );

postAutoloadDump scopes only in development mode, so a production install keeps the scoped output it ships; composer dump-autoload scopes again by hand.

The scoping contract gives the packages a run scopes, what it prefixes, leaves global and rejects, and how a project declares host stubs, text domains, its own php-scoper settings and per-package recipes.

Node baselines

node/ holds one baseline per tool, which a project extends and overrides. The package declares the tools behind them as >= peer dependencies, floored at the versions it is tested against: @wordpress/scripts, @wordpress/eslint-plugin, @wordpress/stylelint-config, @playwright/test and typescript, whose defaults node/tsconfig.base.json builds on. typescript stays below 7, because typescript-eslint, which the ESLint base loads, refuses TypeScript 7.

The package depends on prettier as an alias of wp-prettier, so a consumer's wp-scripts format runs WordPress's fork of Prettier.

Baseline Holds
node/tsconfig.base.json TypeScript's strict defaults, tightened for returns and switch cases and set up for code that wp-scripts bundles and tsc only type-checks
node/eslint.config.base.mjs @wordpress/eslint-plugin's recommended flat config, with its unit-test and Playwright presets scoped to test globs, and console.warn and console.error allowed while other console calls warn
node/stylelint.config.base.js @wordpress/stylelint-config/scss-stylistic, WordPress's SCSS and formatting rules, with generated trees ignored, selector-class-pattern off, and reports on needless, descriptionless and invalid-scope disable comments
node/playwright.config.base.js A factory over the @wordpress/scripts Playwright config, taking the project's wp-env port

TypeScript extends the base by path:

{
	"extends": "@ahegyes/wordpress-configs/node/tsconfig.base.json",
	"include": ["client/**/*"]
}

The tsconfig takes effect only through the project's tsc script, conventionally lint:types, because wp-scripts builds TypeScript without type-checking it.

ESLint spreads the base:

import baseConfig from '@ahegyes/wordpress-configs/node/eslint.config.base.mjs';

export default [ ...baseConfig ];

Stylelint spreads the base, because Stylelint ignores the ignoreFiles of a config loaded through extends:

const baseConfig = require( '@ahegyes/wordpress-configs/node/stylelint.config.base.js' );

module.exports = { ...baseConfig };

Playwright calls the factory with the port the wp-env config serves on (8888 when it sets none) and spreads nested keys individually, or the top-level spread drops the WordPress defaults:

const { defineConfig } = require( '@playwright/test' );
const baseConfig = require( '@ahegyes/wordpress-configs/node/playwright.config.base.js' )( { port: 8888 } );

module.exports = defineConfig( { ...baseConfig, use: { ...baseConfig.use } } );

The factory reads the specs from tests/EndToEnd/ and writes artifacts under tests/.cache/artifacts/. Its webServer runs npm run wp-env start on the given port, so a project that starts wp-env through another script spreads webServer and sets command.

Reusable workflows

.github/workflows/reusable-*.yml holds reusable workflows a project calls with workflow_call, pinned to a commit SHA:

Workflow Runs
reusable-block-json-check.yml Validates every block.json against the WordPress schema
reusable-codeql.yml CodeQL analysis per language
reusable-php-lint.yml composer validate --strict and the named Composer scripts, each a parallel job
reusable-php-syntax-check.yml php -l across PHP versions over chosen paths
reusable-phpunit.yml A Composer test script, optionally inside the project's wp-env
reusable-playwright-e2e.yml The Playwright suite against the project's wp-env
reusable-plugin-check.yml WordPress Plugin Check against a built plugin directory
reusable-release.yml Builds, tests and publishes a plugin release on GitHub, with an opt-in wp.org deploy; installs its WP-CLI commands from this package's php/release/composer.lock, so the plugin requires this package under require-dev
reusable-scripts-styles-lint.yml The named npm scripts, each a parallel job
reusable-supply-chain-audit.yml composer audit and npm audit of the committed lockfiles
reusable-workflow-checks.yml actionlint and zizmor over the workflows

The workflow reference gives every input, the permissions a caller grants, and an example call. The scripts contract names the scripts and files the workflows expect.

Distribution and editor files

.distignore is the baseline of wp dist-archive exclusions: copy it to the project root and extend it with project-specific source paths. reusable-release.yml requires the file. It excludes tests, package-manager manifests, CI and IDE files, the agent, contributing and security documents, development configs such as every phpcs*.xml* ruleset, and secret material (.env*, root-anchored *.pem, *.key, id_rsa*, .npmrc, auth.json).

.editorconfig is WordPress core's with three changes: a tab is four columns wide, as PHPCS and Prettier measure it, every YAML extension indents with two spaces, and *.txt files keep LF line endings. Every other file, JSON and NEON included, indents with tabs, and Markdown keeps its trailing whitespace.

Development

CONTRIBUTING.md gives the setup and the checks to run before a pull request, and the tests guide describes the test tiers, the opt-in mutation run and the fixtures.

About

Shared tooling that lets WordPress projects be checked, scoped and released the same way.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages